1. 程式人生 > 其它 >資料結構與演算法(十一)

資料結構與演算法(十一)

樹的應用

二叉排序樹

給你一個數列 7, 3, 10, 12, 5, 1, 9,要求能夠高效的完成對資料的查詢和新增。

在 為什麼需要該資料結構 中講解了陣列、連結串列資料結構的優缺點,簡單說:

  • 陣列訪問快,增刪慢

    新增或移除時,需要整體移動資料

  • 連結串列增刪快,訪問慢

    只能從頭開始遍歷查詢

那麼利用 二叉排序樹(Binary Sort/Search Tree),既可以保證資料的檢索速度,同時也可以保證資料的插入刪除修改 的速度

二叉排序樹介紹

二叉排序樹(Binary Sort/Search Tree),簡稱 BST。

對於二叉排序樹的任何一個 非葉子節點,要求如下:

  • 左節點,比父節點小
  • 右節點,比父節點大

特殊說明:如果有有相同的值,可以將該節點放在左節點或右節點。當然,最理想的是沒有重複的值,比如 Mysql 中的 B 樹索引,就是以主鍵 ID 來排序的。

比如對下面這個二叉樹增加一個節點:

  1. 從根節點開始,發現比 7 小,直接往左子樹查詢,相當於直接折半了
  2. 比 3 小,再次折半
  3. 比 1 大:直接掛在 1 的右節點

建立、遍歷、刪除、查詢

//定義節點
class Node{
    int value;
    Node left;
    Node right;

    public Node(int value) {
        this.value = value;
    }
    //新增節點
    public void add(Node node){
        if(node == null){
            return;
        }
        if(node.value < this.value){
            if(this.left == null){
                this.left = node;
            }else{
                this.left.add(node);//遞迴查詢新增
            }
        }else{
            if(this.right == null){
                this.right = node;
            }else{
                this.right.add(node);
            }
        }
    }
    //查詢要刪除的父節點
    public Node searchParent(int value){
        if(this.left != null && this.left.value == value || this.right != null && this.right.value == value){
            return this;
        }else if(this.left != null && this.value > value){//左遞迴查詢
            return this.left.searchParent(value);
        }else if(this.right != null && this.value < value){//右遞迴查詢
            return this.right.searchParent(value);
        }else{
            return null;
        }
    }

    //查詢要刪除的節點
    public Node search(int value){
        if(this.value == value){
            return this;
        }else if(value < this.value){
            if(this.left != null){
                return this.left.search(value);//向左遞迴查詢
            }else{
                return null;
            }
        }else {
            if(this.right != null){
                return this.right.search(value);
            }else{
                return null;
            }
        }
    }

    //中序遍歷
    public void infixOrder(){
        if(this.left != null){
            this.left.infixOrder();
        }
        System.out.println(this);
        if(this.right != null){
            this.right.infixOrder();
        }
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }
}
class BinarySortTree{
    private Node root;

    public Node getRoot() {
        return root;
    }

    public void add(Node node){
        if(root == null){
            root = node;
        }else{
            root.add(node);
        }
    }

    public void infixOrder(){
        if(root != null){
            root.infixOrder();
        }else{
            System.out.println("二叉排序樹為空");
        }
    }
    //查詢要刪除的節點
    public Node search(int value){
        if(root != null){
            return root.search(value);
        }else{
            return null;
        }
    }
    //查詢要刪除的父節點
    public Node searchParent(int value){
        if(root != null){
            return root.searchParent(value);
        }{
            return null;
        }
    }
    //找到目標節點為根節點最小的那個節點,並刪除其節點,然後返回其值
    public int delRightMin(Node root){
        Node target = root;
        while(target.left != null){
            target = target.left;
        }
        delNode(target.value);
        return target.value;
    }

    //刪除節點
    public void delNode(int value){
        Node targetNode = search(value);
        //要刪除的節點是根節點
        if(targetNode == root){
            root = null;
            return;
        }
        Node parentNode = searchParent(value);

        if(targetNode.left == null && targetNode.right == null){//刪除的葉子節點
            if(parentNode.left == targetNode){//當目標節點為父節點的左子節點
                parentNode.left = null;
            }else{
                parentNode.right = null;
            }
        }else if(targetNode.left != null && targetNode.right != null){//刪除的節點為有兩個子節點的非葉子節點
            targetNode.value = delRightMin(targetNode.right);

        }else{//刪除的節點為只有一個節點的非葉子節點
            if(targetNode.left != null){
                if(parentNode != null){
                    if(parentNode.left == targetNode){
                        parentNode.left = targetNode.left;
                    }else{
                        parentNode.right = targetNode.left;
                    }
                }else{
                    root = targetNode.left;
                }
            }else {
                if(parentNode != null){
                    if(parentNode.left == targetNode){
                        parentNode.left = targetNode.right;
                    }else{
                        parentNode.right = targetNode.right;
                    }
                }else{
                    root = targetNode.right;
                }
            }
        }

    }
}

完整平衡二叉樹(AVL樹)

二叉排序樹可能的問題

一個數列 {1,2,3,4,5,6},建立一顆二叉排序樹(BST)

建立完成的樹如上圖所示,那麼它存在的問題有以下幾點:

  1. 左子樹全部為空,從形式上看,更像一個單鏈表

  2. 插入速度沒有影響

  3. 查詢速度明顯降低

    因為需要依次比較,不能利用二叉排序樹的折半優勢。而且每次都還要比較左子樹,可能比單鏈表查詢速度還慢。

那麼解決這個劣勢的方案就是:平衡二叉樹(AVL)

基本介紹

平衡二叉樹也叫 平衡二叉搜尋樹(Self-balancing binary search tree),又被稱為 AVL 樹,可以保證 查詢效率較高。它是解決 二叉排序

可能出現的查詢問題。

它的特點:是一顆空樹或它的 左右兩個子樹的高度差的絕對值不超過 1,並且左右兩個子樹都是一顆平衡二叉樹。

平衡二叉樹的常用實現方法有:

  • 紅黑樹
  • AVL(演算法)
  • 替罪羊樹
  • Treap
  • 伸展樹

如下所述,哪些是平衡二叉樹?

  1. 是平衡二叉樹:

    • 左子樹高度為 2
    • 右子樹高度為 1

    他們差值為 1

  2. 也是平衡二叉樹

  3. 不是平衡二叉樹

    1. 左子樹高度為 3
    2. 右子樹高度為 1

    他們差值為 2,所以不是

單旋轉(左旋轉)

一個數列 4,3,6,5,7,8 ,創建出它對應的平衡二叉樹。

思路分析:下圖紅線部分是調整流程。

按照規則調整完成之後,形成了下面這樣一棵樹

完整流程如下圖所示:

插入 8 時,發現左右子樹高度相差大於 1,則進行左旋轉:

  1. 建立一個新的節點 newNode,值等於當前 根節點 的值(以 4 建立)
  2. 把新節點的 左子樹 設定為當前節點的 左子樹
  3. 把新節點的 右子樹 設定為當前節點的 右子樹的左子樹
  4. 當前節點 的值換為 右子節點 的值
  5. 當前節點 的右子樹設定為 右子樹的右子樹
  6. 當前節點 的左子樹設定為新節點

注:左圖是調整期,右圖是調整後。注意調整期的 6 那個節點,調整之後,沒有節點指向他了。也就是說,遍歷的時候它是不可達的。那麼將會自動的被垃圾回收掉。

樹高度計算

前面說過,平衡二叉樹是為了解決二叉排序樹中可能出現的查詢效率問題,那麼基本上的程式碼都可以在之前的二叉排序樹上進行優化。那麼下面只給出與當前主題相關的程式碼,最後放出一份完整的程式碼。

樹的高度計算,我們需要得到 3 個高度:

  1. 這顆樹的整體高度
  2. 左子樹的高度
  3. 右子樹的高度
//Node
//獲得左子樹的高度
public int leftHeight(){
    if(left == null){
        return 0;
    }
    return left.height();
}
//獲得右子樹的高度
public int rightHeight(){
    if(right == null){
        return 0;
    }
    return right.height();
}
//獲得當前節點高度
public int height(){
    return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height())+1;
}
旋轉

說下旋轉的時機:也就是什麼時機採取做旋轉的操作?

當然是:當 右子樹高度 - 左子樹高度 > 1 時,才執行左旋轉。

這裡就得到一些資訊:

  1. 每次新增完一個節點後,就需要檢查樹的高度

  2. 滿足 右子樹高度 - 左子樹高度 > 1,那麼一定滿足下面的條件:

    1. 左子樹高度為 1
    2. 右子樹高度為 3

    也就是符合這張圖

//左旋轉
public void leftRotate(){
    //使用當前跟節點建立一個新節點
    Node newNode = new Node(value);
    //當前根節點的左子樹設定為新節點的左子樹
    newNode.left = left;
    //當前根節點的右子樹的左子樹設定為當前新節點的右子樹
    newNode.right = right.left;
    //將右子樹的值拷貝到當前根節點
    value = right.value;
    //將跟節點的右子樹設定為右子樹的右子樹
    right = right.right;
    //將根節點左子樹設定為新節點
    left = newNode;
}

右旋轉

其實這個就很好理解了:

  • 左旋轉:右 - 左 > 1,把右邊的往左邊旋轉一層
  • 右旋轉:左 - 右 > 1,把左邊的往右邊旋轉一層

他們其實是反著來的,那麼右旋轉的思路如下:

  1. 建立一個新的節點 newNode,值等於當前 根節點 的值(以 4 建立)
  2. 把新節點的 右子樹 設定為當前節點的 右子樹
  3. 把新節點的 左子樹 設定為當前節點的 左子樹的右子樹
  4. 當前節點 的值換為 左子節點 的值
  5. 當前節點 的左子樹設定為 左子樹的左子樹
  6. 當前節點 的右子樹設定為新節點
//右旋轉
public void rightRotate(){
    Node newNode = new Node(value);
    newNode.right = right;
    newNode.left = left.right;
    value = left.value;
    left = left.left;
    right = newNode;
}

雙旋轉

在前面的例子中,使用單旋轉(即一次旋轉)就可以將非平衡二叉樹轉換為平衡二叉樹。

但是在某些情況下,就無法做到。比如下面這兩組數列

左側這個樹滿足 leftHeight - rightHeight > 1 ,也就是滿足右旋轉,旋轉之後,樹結構變化了。但是還是一個非平衡二叉樹。

它的主要原因是:root 左子樹的 左子樹高度 小於 右子樹的高度。即:節點 7 的左子樹高度小於右子樹的高度。

解決辦法:

  1. 先將 7 這個節點為 root 節點,進行左旋轉
  2. 再將原始的 root 節點進行右旋轉

過程示意圖如下:

其實可以參考下前面兩個單旋轉的圖例,它有這樣一個特點:

  1. 右旋轉:
    • root 的 left 左子樹高度 大於 右子樹高度
    • 右旋轉的時候,會將 left.right 旋轉到 right.left 節點上
  2. 左旋轉:
    • root 的 right 右子樹高度 大於 左子樹高度
    • 左旋轉的時候,會將 right.left 旋轉到 left.right 上。

如果不滿足這個要求,在第二個操作的時候,就會導致 2 層的高度被旋轉到 1 層的節點下面,導致不平衡了。

//左旋轉
if(rightHeight() - leftHeight() > 1){
    //如果右子樹的左子樹高度大於右子樹的右子樹,先進行右旋轉,再進行左旋轉
    if(right != null && right.leftHeight() > right.rightHeight()){
        right.rightRotate();
        leftRotate();//左旋轉
    }else{
        leftRotate();
    }
    return;//進行了旋轉之後就不用進行下面的再次旋轉
}
//右旋轉
if(leftHeight() - rightHeight() > 1){
    //如果左子樹的右子樹高度,大於左子樹的左子樹先進行左旋轉,再進行右旋轉
    if(left != null && left.rightHeight() > left.leftHeight()){
        left.leftRotate();//左子樹左旋轉
        rightRotate();//右旋轉
    }else{
        rightRotate();
    }
}

完整程式碼

//AVL樹
class AVLTree{
    private Node root;

    public Node getRoot() {
        return root;
    }
    public void add(Node node){
        if(root == null){
            root = node;
        }else{
            root.add(node);
        }
    }
    public void infixOrder(){
        if(root != null){
            root.infixOrder();
        }else{
            System.out.println("二叉排序樹為空");
        }
    }
    //查詢要刪除的節點
    public Node search(int value){
        if(root != null){
            return root.search(value);
        }else{
            return null;
        }
    }
    //查詢要刪除的父節點
    public Node searchParent(int value){
        if(root != null){
            return root.searchParent(value);
        }{
            return null;
        }
    }
    //找到目標節點為根節點最小的那個節點,並刪除其節點,然後返回其值
    public int delRightMin(Node root){
        Node target = root;
        while(target.left != null){
            target = target.left;
        }
        delNode(target.value);
        return target.value;
    }

    //刪除節點
    public void delNode(int value){
        Node targetNode = search(value);
        //要刪除的節點是根節點
        if(targetNode == root){
            root = null;
            return;
        }
        Node parentNode = searchParent(value);

        if(targetNode.left == null && targetNode.right == null){//刪除的葉子節點
            if(parentNode.left == targetNode){//當目標節點為父節點的左子節點
                parentNode.left = null;
            }else{
                parentNode.right = null;
            }
        }else if(targetNode.left != null && targetNode.right != null){//刪除的節點為有兩個子節點的非葉子節點
            targetNode.value = delRightMin(targetNode.right);

        }else{//刪除的節點為只有一個節點的非葉子節點
            if(targetNode.left != null){
                if(parentNode != null){
                    if(parentNode.left == targetNode){
                        parentNode.left = targetNode.left;
                    }else{
                        parentNode.right = targetNode.left;
                    }
                }else{
                    root = targetNode.left;
                }
            }else {
                if(parentNode != null){
                    if(parentNode.left == targetNode){
                        parentNode.left = targetNode.right;
                    }else{
                        parentNode.right = targetNode.right;
                    }
                }else{
                    root = targetNode.right;
                }
            }
        }
    }
}
//節點
class Node{
    int value;
    Node left;
    Node right;

    public Node(int value) {
        this.value = value;
    }
    //獲得左子樹的高度
    public int leftHeight(){
        if(left == null){
            return 0;
        }
        return left.height();
    }
    //獲得右子樹的高度
    public int rightHeight(){
        if(right == null){
            return 0;
        }
        return right.height();
    }
    //獲得當前節點高度
    public int height(){
        return Math.max(left == null ? 0 : left.height(),right == null ? 0 : right.height())+1;
    }

    //左旋轉
    public void leftRotate(){
        //使用當前跟節點建立一個新節點
        Node newNode = new Node(value);
        //當前根節點的左子樹設定為新節點的左子樹
        newNode.left = left;
        //當前根節點的右子樹的左子樹設定為當前新節點的右子樹
        newNode.right = right.left;
        //將右子樹的值拷貝到當前根節點
        value = right.value;
        //將跟節點的右子樹設定為右子樹的右子樹
        right = right.right;
        //將根節點左子樹設定為新節點
        left = newNode;
    }
    //右旋轉
    public void rightRotate(){
        Node newNode = new Node(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }

    public void add(Node node){
        if(node == null){
            return;
        }
        if(node.value < this.value){
            if(this.left == null){
                this.left = node;
            }else{
                this.left.add(node);//遞迴查詢新增
            }
        }else{
            if(this.right == null){
                this.right = node;
            }else{
                this.right.add(node);
            }
        }
        //左旋轉
        if(rightHeight() - leftHeight() > 1){
            //如果右子樹的左子樹高度大於右子樹的右子樹,先進行右旋轉,再進行左旋轉
            if(right != null && right.leftHeight() > right.rightHeight()){
                right.rightRotate();
                leftRotate();//左旋轉
            }else{
                leftRotate();
            }
            return;//進行了旋轉之後就不用進行下面的再次旋轉
        }
        //右旋轉
        if(leftHeight() - rightHeight() > 1){
            //如果左子樹的右子樹高度,大於左子樹的左子樹先進行左旋轉,再進行右旋轉
            if(left != null && left.rightHeight() > left.leftHeight()){
                left.leftRotate();//左子樹左旋轉
                rightRotate();//右旋轉
            }else{
                rightRotate();
            }
        }
    }
}