『資料結構與演算法』二叉查詢樹(BST)

碼農StayUp發表於2021-02-15

微信搜尋:碼農StayUp
主頁地址:https://gozhuyinglong.github.io
原始碼分享:https://github.com/gozhuyinglong/blog-demos

1. 二叉查詢樹(Binary Search Tree)

二叉查詢樹又叫二叉排序樹(Binary Sort Tree),或叫二叉搜尋樹,簡稱BST。BST是一種節點值之間有次序的二叉樹。其特性是:

  • 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
  • 若任意節點的右子樹不空,則右子樹上所有節點的值均大於或等於它的根節點的值;
  • 任意節點的左、右子樹也分別為二叉查詢樹;

是否二叉查詢樹

二叉查詢樹相比於其他資料結構的優勢在於查詢、插入的時間複雜度較低,為$O(logN)$。用大$O$符號表示的時間複雜度:

演算法 平均 最差
空間 $O(N)$ $O(N)$
搜尋 $O(logN)$ $O(N)$
插入 $O(logN)$ $O(N)$
刪除 $O(logN)$ $O(N)$

2. BST的實現

二叉查詢樹要求所有的節點元素都能夠排序,所以我們的Node節點類需要實現Comparable介面,樹中的兩個元素可以使用compareTo方法進行比較。
我們節點中元素的型別為int型,所以該介面泛型為Comparable<Integer>,下面是具體實現:

2.1 節點類

  • element 為資料元素
  • left 為左子節點
  • right 為右子節點
class Node implements Comparable<Integer> {
    private final int element; // 資料元素
    private Node left; // 左子樹
    private Node right; // 右子樹

    private Node(Integer element) {
        this.element = element;
    }

    @Override
    public int compareTo(Integer o) {
        return o.compareTo(element);
    }
}

2.2 二叉查詢樹類

  • root 為樹根,所有的操作均始於此

後面會在該類中增加其他方法,如新增、查詢、刪除等

class BinarySearchTree {
        private Node root; // 樹根
}

3. 插入節點

向二叉查詢樹中插入的節點總是葉子節點,插入過程如下:

  1. root為空,則將插入節點設為root
  2. 當前元素與插入元素通過compareTo進行比較,若插入元素值小,並且左子節點left為空,則插入至當前節點左子節點;否則繼續遞迴
  3. 若插入元素值大,且右子節點right為空,則插入至當前節點右子節點;否則繼續遞迴。
  4. 若插入元素等於當前節點元素,則插入失敗。注:也可以將其插入到右子節點,我這裡為了方便直接放棄插入。

具體實現:
BinarySearchTree類中新增兩個方法:

  • public boolean add(int element) 為公開方法
  • private boolean add(Node node, int element)為私有方法,內部遞迴使用
       // 新增元素
       public boolean add(int element) {
            if (root == null) {
                root = new Node(element);
                return true;
            }
            return add(root, element);
        }
        // 新增元素(遞迴)
        private boolean add(Node node, int element) {
            if (node.compareTo(element) < 0) {
                if (node.left == null) {
                    node.left = new Node(element);
                    return true;
                } else {
                    return add(node.left, element);
                }
            } else if (node.compareTo(element) > 0) {
                if (node.right == null) {
                    node.right = new Node(element);
                    return true;
                } else {
                    return add(node.right, element);
                }
            } else {
                return false;
            }
        }

4. 查詢節點

通過二叉查詢樹查詢元素,其過程如下:

  1. root為空,則查詢失敗
  2. 將當前元素與目標元素對比,若相等則查詢成功。
  3. 若不相等,則繼續遞迴查詢:若目標值小於當前節點值,則查詢左子樹;否則,查詢右子樹。

具體實現:
BinarySearchTree類中新增兩個方法:

  • public Node find(int element) 為公開方法
  • private Node find(Node node, int element) 為私有方法,內部遞迴使用
      // 查詢元素
      public Node find(int element) {
            if (root == null) {
                return null;
            }
            return find(root, element);
        }

        // 查詢元素(遞迴)
        private Node find(Node node, int element) {
            if (node == null) {
                return null;
            }
            int compareResult = node.compareTo(element);
            if (compareResult < 0) {
                return find(node.left, element);
            } else if (compareResult > 0) {
                return find(node.right, element);
            } else {
                return node;
            }
        }

5. 遍歷節點

BST是一個有序二叉樹,通過中序遍歷可順序輸出樹中節點。
中序遍歷過程如下:

  1. 遞迴遍歷左子節點
  2. 輸出當前節點
  3. 遞迴遍歷右子節點

具體實現:
BinarySearchTree類中新增兩個方法:

  • public void orderPrint() 為公開方法
  • private void orderPrint(Node node) 為私有方法,內部遞迴使用
      // 遍歷節點
      public void orderPrint() {
            orderPrint(root);
        }

        // 遍歷節點(遞迴)
        private void orderPrint(Node node) {

            if (node == null) {
                return;
            }

            // 遞迴左子節點
            if (node.left != null) {
                orderPrint(node.left);
            }

            // 輸出當前節點
            System.out.println(node.element);

            // 遞迴右子節點
            if (node.right != null) {
                orderPrint(node.right);
            }

        }

6. 刪除節點

刪除節點最為複查,共有三種情況:

6.1 目標元素為葉子節點

葉子節點最容易刪除,過程如下:

  1. 找到目標節點的父節點
  2. 判斷目標節點是父節點的左子樹還是右子樹
  3. 若是左子樹,將父節點的left設為空;否則將父節點的right設為空

6.2 目標元素即有左子樹,也有右子樹

該情況刪除操作最為複雜,過程如下:

  1. 找到目標節點的父節點
  2. 判斷目標節點是父節點的左子樹還是右子樹
  3. 找到右子樹中最小元素(葉子節點),將其賦給臨時變數minNode,再將該元素從樹中刪除
  4. 將目標元素的屬性賦予minNode
  5. 若目標元素是父節點的左子樹,將父節點的left設為minNode;否則將父節點的right設為minNode

6.3 目標元素只有左子樹,或只有右子樹

刪除過程如下

  1. 找到目標節點的父節點
  2. 判斷目標節點是父節點的左子樹還是右子樹
  3. 若是左子樹,將父節點的left設為目標節點不為空的子樹;否則將父節點的right設為目標節點不為空的子樹

具體實現
BinarySearchTree類中新增兩個方法:

  • public boolean remove(int element) 為公開方法
  • private boolean remove(Node parentNode, Node node, int element)為私有方法,內部遞迴使用
      // 刪除節點
      public boolean remove(int element) {
            if (root == null) {
                return false;
            }
            // 如果刪除的元素是root
            if (root.compareTo(element) == 0) {
                if (root.right == null) {
                    root = root.left;
                } else {
                    root.right.left = root.left;
                    root = root.right;
                }
                return true;
            }
            return remove(null, root, element);
        }

        // 刪除節點(遞迴)
        private boolean remove(Node parentNode, Node node, int element) {
            if (node == null) {
                return false;
            }
            // 先找到目標元素
            int compareResult = node.compareTo(element);
            if (compareResult < 0) {
                return remove(node, node.left, element);
            }
            if (compareResult > 0) {
                return remove(node, node.right, element);
            }

            // 找到目標元素,判斷該節點是父節點的左子樹還是右子樹
            boolean isLeftOfParent = false;
            if (parentNode.left != null && parentNode.left.compareTo(element) == 0) {
                isLeftOfParent = true;
            }

            // 刪除目標元素
            if (node.left == null && node.right == null) { // (1)目標元素為葉子節點,直接刪除
                if (isLeftOfParent) {
                    parentNode.left = null;
                } else {
                    parentNode.right = null;
                }
            } else if (node.left != null && node.right != null) { // (2)目標元素即有左子樹,也有右子樹
                // 找到右子樹最小值(葉子節點),並將其刪除
                Node minNode = findMin(node.right);
                remove(minNode.element);
                // 將該最小值替換要刪除的目標節點
                minNode.left = node.left;
                minNode.right = node.right;
                if(isLeftOfParent) {
                    parentNode.left = minNode;
                } else {
                    parentNode.right = minNode;
                }

            } else { // (3)目標元素只有左子樹,或只有右子樹
                if (isLeftOfParent) {
                    parentNode.left = node.left != null ? node.left : node.right;
                } else {
                    parentNode.right = node.left != null ? node.left : node.right;
                }
            }
            return true;
        }
    }

7. 完整程式碼

該程式碼根據下圖二叉查詢樹實現,其操作包括:新增、查詢、遍歷、刪除、查詢最小值、查詢最大值。

二叉查詢樹

public class BinarySearchTreeDemo {

    public static void main(String[] args) {
        BinarySearchTree tree = new BinarySearchTree();

        System.out.println("----------------------新增元素");
        Integer[] array = {5, 2, 7, 1, 4, 3, 7, 6, 9, 8};
        for (Integer element : array) {
            System.out.printf("新增元素[%s] --> %s\n", element, tree.add(element));
        }

        System.out.println("----------------------順序輸出(中序遍歷)");
        tree.orderPrint();

        System.out.println("----------------------查詢元素");
        System.out.println(tree.find(7));

        System.out.println("----------------------查詢最小元素");
        System.out.println(tree.findMin());

        System.out.println("----------------------查詢最大元素");
        System.out.println(tree.findMax());

        System.out.println("----------------------是否包含元素");
        System.out.println("是否包含[0] --> \t" + tree.contains(0));
        System.out.println("是否包含[2] --> \t" + tree.contains(2));

        System.out.println("----------------------刪除目標元素");
        System.out.println("刪除[0] --> \t" + tree.remove(0));
        tree.orderPrint();
        System.out.println("刪除[1] --> \t" + tree.remove(1));
        tree.orderPrint();
        System.out.println("刪除[4] --> \t" + tree.remove(4));
        tree.orderPrint();
        System.out.println("刪除[7] --> \t" + tree.remove(7));
        tree.orderPrint();

    }

    private static class BinarySearchTree {
        private Node root; // 樹根

        /**
         * 新增元素
         *
         * @param element
         * @return
         */
        public boolean add(int element) {
            if (root == null) {
                root = new Node(element);
                return true;
            }
            return add(root, element);
        }

        /**
         * 新增元素(遞迴)
         *
         * @param node
         * @param element
         * @return
         */
        private boolean add(Node node, int element) {
            if (node.compareTo(element) < 0) {
                if (node.left == null) {
                    node.left = new Node(element);
                    return true;
                } else {
                    return add(node.left, element);
                }
            } else if (node.compareTo(element) > 0) {
                if (node.right == null) {
                    node.right = new Node(element);
                    return true;
                } else {
                    return add(node.right, element);
                }
            } else {
                return false;
            }
        }

        /**
         * 查詢元素
         *
         * @param element
         * @return
         */
        public Node find(int element) {
            if (root == null) {
                return null;
            }
            return find(root, element);
        }

        /**
         * 查詢元素(遞迴)
         *
         * @param node
         * @param element
         * @return
         */
        private Node find(Node node, int element) {
            if (node == null) {
                return null;
            }
            int compareResult = node.compareTo(element);
            if (compareResult < 0) {
                return find(node.left, element);
            } else if (compareResult > 0) {
                return find(node.right, element);
            } else {
                return node;
            }
        }

        /**
         * 查詢最大值
         *
         * @return
         */
        public Node findMax() {
            return findMax(root);
        }

        /**
         * 查詢最大值(遞迴)
         *
         * @param node
         * @return
         */
        private Node findMax(Node node) {
            if (node.right == null) {
                return node;
            }
            return findMax(node.right);
        }

        /**
         * 查詢最小值
         *
         * @return
         */
        private Node findMin() {
            return findMin(root);
        }

        /**
         * 查詢最小值(遞迴)
         *
         * @param node
         * @return
         */
        private Node findMin(Node node) {
            if (node.left == null) {
                return node;
            }
            return findMin(node.left);
        }

        /**
         * 順序輸出
         */
        public void orderPrint() {
            orderPrint(root);
        }


        /**
         * 順序輸出(遞迴)
         *
         * @param node
         */
        private void orderPrint(Node node) {

            if (node == null) {
                return;
            }

            // 遞迴左子節點
            if (node.left != null) {
                orderPrint(node.left);
            }

            // 輸出當前節點
            System.out.println(node.element);

            // 遞迴右子節點
            if (node.right != null) {
                orderPrint(node.right);
            }

        }

        /**
         * 是否包含某值
         *
         * @param element
         * @return
         */
        public boolean contains(int element) {
            if (find(element) == null) {
                return false;
            }
            return true;
        }

        /**
         * 刪除目標元素
         *
         * @param element
         * @return
         */
        public boolean remove(int element) {
            if (root == null) {
                return false;
            }
            // 如果刪除的元素是root
            if (root.compareTo(element) == 0) {
                if (root.right == null) {
                    root = root.left;
                } else {
                    root.right.left = root.left;
                    root = root.right;
                }
                return true;
            }
            return remove(null, root, element);
        }

        /**
         * 刪除目標元素(遞迴),有三種情況:
         * (1)目標元素為葉子節點
         * (2)目標元素即有左子樹,也有右子樹
         * (3)目標元素只有左子樹,或只有右子樹
         *
         * @param parentNode 當前節點的父節點
         * @param node       當前節點(若當前節點上的元素與要刪除的元素匹配,則刪除當前節點)
         * @param element    要刪除的元素
         * @return
         */
        private boolean remove(Node parentNode, Node node, int element) {
            if (node == null) {
                return false;
            }
            // 先找到目標元素
            int compareResult = node.compareTo(element);
            if (compareResult < 0) {
                return remove(node, node.left, element);
            }
            if (compareResult > 0) {
                return remove(node, node.right, element);
            }

            // 找到目標元素,判斷該節點是父節點的左子樹還是右子樹
            boolean isLeftOfParent = false;
            if (parentNode.left != null && parentNode.left.compareTo(element) == 0) {
                isLeftOfParent = true;
            }

            // 刪除目標元素
            if (node.left == null && node.right == null) { // (1)目標元素為葉子節點,直接刪除
                if (isLeftOfParent) {
                    parentNode.left = null;
                } else {
                    parentNode.right = null;
                }
            } else if (node.left != null && node.right != null) { // (2)目標元素即有左子樹,也有右子樹
                // 找到右子樹最小值(葉子節點),並將其刪除
                Node minNode = findMin(node.right);
                remove(minNode.element);
                // 將該最小值替換要刪除的目標節點
                minNode.left = node.left;
                minNode.right = node.right;
                if(isLeftOfParent) {
                    parentNode.left = minNode;
                } else {
                    parentNode.right = minNode;
                }

            } else { // (3)目標元素只有左子樹,或只有右子樹
                if (isLeftOfParent) {
                    parentNode.left = node.left != null ? node.left : node.right;
                } else {
                    parentNode.right = node.left != null ? node.left : node.right;
                }
            }
            return true;
        }
    }

    private static class Node implements Comparable<Integer> {
        private final Integer element; // 資料元素
        private Node left; // 左子樹
        private Node right; // 右子樹

        private Node(Integer element) {
            this.element = element;
        }

        @Override
        public int compareTo(Integer o) {
            return o.compareTo(element);
        }

        @Override
        public String toString() {
            return "Node{" +
                    "element=" + element +
                    '}';
        }
    }
}

輸出結果:

----------------------新增元素
新增元素[5] --> true
新增元素[2] --> true
新增元素[7] --> true
新增元素[1] --> true
新增元素[4] --> true
新增元素[3] --> true
新增元素[7] --> false
新增元素[6] --> true
新增元素[9] --> true
新增元素[8] --> true
----------------------順序輸出(中序遍歷)
1
2
3
4
5
6
7
8
9
----------------------查詢元素
Node{element=7}
----------------------查詢最小元素
Node{element=1}
----------------------查詢最大元素
Node{element=9}
----------------------是否包含元素
是否包含[0] --> 	false
是否包含[2] --> 	true
----------------------刪除目標元素
刪除[0] --> 	false
1
2
3
4
5
6
7
8
9
刪除[1] --> 	true
2
3
4
5
6
7
8
9
刪除[4] --> 	true
2
3
5
6
7
8
9
刪除[7] --> 	true
2
3
5
6
8
9

推薦閱讀

相關文章