TypeScript實現AVL樹與紅黑樹

前言

二叉搜索樹存在一個問題: 當往樹中插入的數據一大部分大於某個節點或小於某個節點,這樣就會致使樹的一條邊很是深。爲了解決這個問題就出現了自平衡樹這種解決方案。javascript

本文將詳解兩種自平衡樹:AVL樹和紅黑樹並用TypeScript將其實現,歡迎各位感興趣的開發者閱讀本文。java

寫在前面

本文講解的兩種自平衡樹均基於二叉搜索樹實現,對二叉搜索樹不瞭解的開發者請移步: TypeScript實現二叉搜索樹node

AVL自平衡樹

AVL樹(Adelson-Velskii-Landi 樹)是一種自平衡二叉搜索樹,任何一個節點左右兩側子樹的高度之差最多爲1,添加或刪除節點時,AVL樹會盡量嘗試轉換爲徹底樹。git

實現思路

AVL樹是一顆二叉搜索樹,所以咱們能夠繼承二叉搜索樹,重寫二叉樹的部分方法便可。github

AVL樹的術語

在AVL樹中插入或移除節點和二叉搜索樹徹底相同,然而AVL樹的不一樣之處在於咱們須要校驗它的平衡因子,根據平衡因子來判斷樹是否須要調整,接下來咱們就來看下AVL樹的相關術語:typescript

  • 節點的高度和平衡因子
  • 平衡操做 - 樹的旋轉

節點的高度是從節點到任意子節點的邊的最大值,下圖描述了一個包含節點高度的樹。瀏覽器

  • 節點35左子節點高度是1,右子節點高度是2
  • 節點45左子節點高度是2,右子節點高度是1
  • 節點45取最大子節點邊的高度即2
  • 節點35到節點45的高度是1而節點45的高度是2,所以節點35的高度是3

cbc49be1691ce1ef9474eb728a9b90e7

節點的平衡因子是指在AVL樹中,須要對每一個節點計算右子樹高度(hr)和左子樹高度(hl)之間的差值,該值(hr - hl)應爲0、1或-1。若是結果不是這三個值之一,則須要平衡該AVL樹,下圖中的樹描述了每一個節點的平衡因子。函數

  • 當前節點只有左子節點時,平衡因子爲-1
  • 當前節點只有右子節點時。平衡因子爲+1
  • 當前節左、右子節點都擁有時,平衡因子爲+1

26ebe531626732bc1d98c757df889b80

平衡操做 - 樹的旋轉 添加或刪除節點後,咱們須要計算節點的高度獲取平衡因子,根據平衡因子判斷是否須要進行旋轉來平衡這顆樹。樹的平衡有如下場景:post

  • 左-左(LL): 向右的單旋轉
  • 右-右(RR): 向左的單旋轉
  • 左-右(LR): 向右的雙旋轉
  • 右-左(RL): 向左的雙旋轉

節點高度計算

  • 聲明一個方法,該方法接收一個參數: 要獲取高度的節點
  • 遞歸獲取當前節點左子樹的高度和右子樹的高度,返回較大一方的值
  • 遞歸基線條件:節點爲null

平衡因子計算

  • 聲明一個方法,該方法接收一個參數:要獲取平衡因子的節點
  • 計算當前節點左子樹和右子樹的高度,計算他們的差值
  • 根據差值返回不一樣的條件

樹的旋轉

咱們根據計算出的平衡因子來進行以下相對應的旋轉測試

左-左(LL): 向右的單旋轉

當節點的左側子節點的高度大於右側子節點的高度時,而且左側子節點也是平衡或左側較重,此時就須要對平衡樹進行LL操做,下圖描述了這個過程

  • 與平衡樹操做相關的節點有三個(X、Y、Z)
    29efdee0ff6836f94ef51919d9a240f0
  • 將節點X置與Y
    8568b80c447964de8c5ae8ea44891b95
  • 將節點Y的左子節點置爲節點X的右子節點
    95b0c4e36d484f79aff2acdb6d2a565f
  • 將X的右子節點置爲節點Y
    11c699dd202cf3be43a8551de6d5229b
  • 更新節點
    b8f780eea5394a773190bfaf7ab7ca75
右-右(RR): 向左的單旋轉

當節點右側子節點的高度大於左側子節點的高度時,而且右側子節點也是平衡或右側較重,此時就須要對平衡樹進行RR操做,下圖描述了這個過程

  • 與平衡樹操做相關的節點有三個(X、Y、Z)
    17413b001055473c3aa97b7dace7915a
  • 將節點X至於節點Y
    1a4d78d0fe68cf7aaff9c482433af852
  • 將節點Y的右子節點置爲節點X的左子節點
    d0d882a8bb71d50a611d9a20f9eda91b
  • 將節點X的左子節點置爲節點Y
    7d70cb1e2d2daad50a7f44555b3eead2
左-右(LR): 向右的雙旋轉

當左側子節點的高度大於右側子節點的高度時,而且左側子節點右側較重,此時就須要對平衡樹進行左旋轉來修復,這樣就會造成左-左的狀況,而後在對不平衡的節點進行一個右旋轉來修復,下圖描述了須要進行LR的場景

1ac165b5591422b5c037e88d47746231

  • 進行RR旋轉
  • 進行LL旋轉

665af59d89c0389a7c724fe116db4caa

右-左(RL): 向左的雙旋轉

當右側子節點的高度大於左側子節點的高度時,而且右側子節點左側較重,此時就須要對平衡樹進行右旋轉進行修復,這樣會造成右-右的狀況,而後在對不平衡的節點進行一個左旋轉來修復,下圖描述了須要進行RL的場景

4a0aadc16d3db86065c9020b8dda006f

  • 進行LL旋轉
  • 進行RR旋轉

460e0e0bc5fdbb55e6c6c03ddb740a4c

插入和移除節點

向AVL樹中插入或移除節點的邏輯與二叉搜索樹同樣,惟一的不一樣之處在於插入後須要驗證樹是否平衡,若是不平衡則須要進行相應的旋轉操做。

  • 獲取當前插入樹節點的平衡因子
  • 若是在向左側子樹插入節點後樹不平衡了,咱們須要比較插入的鍵是否小於左側子節點的鍵。若是是則進行LL旋轉,不然進行LR旋轉
  • 若是在向右側子樹插入節點後樹不平衡了,咱們須要比較插入的鍵是否小於右側子節點的鍵。若是是則進行RR旋轉,不然進行RL旋轉

實現代碼

  • 新建AVLTree.ts文件
  • 聲明AVLTree類,繼承BinarySearchTree類
export default class AVLTree<T> extends BinarySearchTree<T>{
    constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {
        super(compareFn);
    }
}
複製代碼
  • 聲明平衡因子枚舉
enum BalanceFactor {
    UNBALANCED_RIGHT = 1,
    SLIGHTLY_UNBALANCED_RIGHT = 2,
    BALANCED = 3,
    SLIGHTLY_UNBALANCED_LEFT = 4,
    UNBALANCED_LEFT = 5
}
複製代碼
  • 實現計算節點高度函數
private getNodeHeight(node: Node<T>): number{
    if (node == null) {
        return -1;
    }
    return Math.max(this.getNodeHeight(<Node<T>>node.left), this.getNodeHeight(<Node<T>>node.right)) + 1;
}
複製代碼
  • 實現平衡因子計算函數
private getBalanceFactor(node: Node<T>) {
    // 計算差值
    const heightDifference = this.getNodeHeight(<Node<T>>node.left) - this.getNodeHeight(<Node<T>>node.right);
    switch (heightDifference) {
        case -2:
            return BalanceFactor.UNBALANCED_RIGHT;
        case -1:
            return BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT;
        case 1:
            return BalanceFactor.SLIGHTLY_UNBALANCED_LEFT;
        case 2:
            return BalanceFactor.UNBALANCED_LEFT;
        default:
            return BalanceFactor.BALANCED;
    }
}
複製代碼
  • 實現樹的四種旋轉
/** * 左左狀況: 向右的單旋轉 * * b a * / \ / \ * a e -> rotationLL(b) -> c b * / \ / \ * c d d e * * @param node */
private rotationLL(node: Node<T>) {
    // 建立tmp變量, 存儲node的左子節點
    const tmp = <Node<T>>node.left;
    // node的左子節點修改成tmp的右子節點
    node.left = tmp.right;
    // tmp的右子節點修改成node
    tmp.right = node;
    // 更新節點
    return tmp;
}

/** * 右右狀況: 向左的單旋轉 * * a b * / \ / \ * c b -> rotationRR(a) -> a e * / \ / \ * d e c d * @param node */
private rotationRR(node: Node<T>) {
    // 將節點X置於節點Y
    const tmp = <Node<T>>node.right;
    // 將Y的右子節點置爲X的左子節點
    node.right = tmp.left;
    // 將X的左子節點置爲Y
    tmp.left = node;
    // 更新節點
    return tmp;
}

/** * 左右狀況: 向右的雙旋轉, 先向右旋轉而後向左旋轉 * @param node */
private rotationLR(node: Node<T>) {
    node.left = this.rotationRR(<Node<T>>node.left);
    return this.rotationLL(node);
}

/** * 右左狀況: 向左的雙旋轉,先向左旋轉而後向右旋轉 * @param node */
private rotationRL(node: Node<T>) {
    node.right = this.rotationLL(<Node<T>>node.right);
    return this.rotationRR(node);
}
複製代碼
  • 重寫插入和移除節點函數
// 向樹AVL樹中插入節點
insert(key: T) {
    this.root = this.insertNode(<Node<T>>this.root, key)
}

protected insertNode(node: Node<T>, key:T) {
    if (node == null) {
        return new Node(key);
    }else if(this.compareFn(key, node.key) === Compare.LESS_THAN) {
        node.left = this.insertNode(<Node<T>>node.left, key);
    }else if(this.compareFn(key, node.key) === Compare.BIGGER_THAN) {
        node.right = this.insertNode(<Node<T>>node.right, key);
    }else {
        return node; // 重複的鍵
    }

    // 計算平衡因子判斷樹是否須要平衡操做
    const balanceState = this.getBalanceFactor(node);

    // 向左側子樹插入節點後樹失衡
    if (balanceState === BalanceFactor.UNBALANCED_LEFT) {
        // 判斷插入的鍵是否小於左側子節點的鍵
        if (this.compareFn(key, <T>node.left?.key) === Compare.LESS_THAN) {
            // 小於則進行LL旋轉
            node = this.rotationLL(node);
        } else {
            // 不然進行LR旋轉
            return this.rotationLR(node);
        }
    }
    // 向右側子樹插入節點後樹失衡
    if (balanceState === BalanceFactor.UNBALANCED_RIGHT) {
        // 判斷插入的鍵是否小於右側子節點的鍵
        if (this.compareFn(key, <T>node.right?.key) === Compare.BIGGER_THAN) {
            // 小於則進行RR旋轉
            node = this.rotationRR(node);
        } else {
            // 不然進行RL旋轉
            return this.rotationRL(node);
        }
    }
    // 更新節點
    return node;
}

// 移除節點
protected removeNode(node: Node<T>, key: T) {
    node = <Node<T>>super.removeNode(node, key);
    if (node == null) {
        return node;
    }

    // 獲取樹的平衡因子
    const balanceState = this.getBalanceFactor(node);
    // 左樹失衡
    if (balanceState === BalanceFactor.UNBALANCED_LEFT) {
        // 計算左樹的平衡因子
        const balanceFactorLeft = this.getBalanceFactor(<Node<T>>node.left);
        // 左側子樹向左不平衡
        if (balanceFactorLeft === BalanceFactor.BALANCED || balanceFactorLeft === BalanceFactor.UNBALANCED_LEFT) {
            // 進行LL旋轉
            return this.rotationLL(node);
        }
        // 右側子樹向右不平衡
        if (balanceFactorLeft === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT) {
            // 進行LR旋轉
            return this.rotationLR(<Node<T>>node.left);
        }
    }
    // 右樹失衡
    if (balanceState === BalanceFactor.UNBALANCED_RIGHT) {
        // 計算右側子樹平衡因子
        const balanceFactorRight = this.getBalanceFactor(<Node<T>>node.right);
        // 右側子樹向右不平衡
        if (balanceFactorRight === BalanceFactor.BALANCED || balanceFactorRight === BalanceFactor.SLIGHTLY_UNBALANCED_RIGHT) {
            // 進行RR旋轉
            return this.rotationRR(node);
        }
        // 右側子樹向左不平衡
        if (balanceFactorRight === BalanceFactor.SLIGHTLY_UNBALANCED_LEFT) {
            // 進行RL旋轉
            return this.rotationRL(<Node<T>>node.right);
        }
    }
    return node;
}
複製代碼

完整代碼請移步: AVLTree.ts

編寫測試代碼

上面咱們實現AVL樹,接下來咱們經過一個例子來測試下上述代碼是否正確執行

const avlTree = new AVLTree();
const printNode = value=>{
    console.log(value);
}
/** * 測試樹失衡 * 30 30 30 * / \ / \ / \ * 27 60 -> 12 60 -> remove(10) 12 60 * / / \ \ * 12 10 27 27 * / * 10 */
avlTree.insert(30);
avlTree.insert(27);
avlTree.insert(60);
avlTree.insert(12);
avlTree.insert(10);
avlTree.remove(10);
// 後序遍歷
avlTree.preOrderTraverse(printNode);
複製代碼

執行結果以下:

c9a07e0ac807f92e7c48dc85ad61bbe2

紅黑樹

紅黑樹:故名思義,即樹中的節點不是紅的就是黑的,它也是一個自平衡二叉樹。上面咱們實現了AVL樹,咱們在向AVL樹中插入或移除節點可能會形成旋轉,因此咱們須要一個包含屢次插入和刪除的自平衡樹,紅黑樹是比較好的。插入或刪除頻率比較低,那麼AVL樹比紅黑樹更好。

實現思路

紅黑樹的每一個節點都須要遵循如下原則:

  1. 節點不是紅的就是黑的
  2. 樹的根節點是黑的
  3. 全部葉節點都是黑的
  4. 若是一個節點是紅的,那麼它的兩個子節點都是黑的
  5. 不能有兩個相鄰的紅節點,一個紅節點不能有紅的父節點或子節點
  6. 從給定的節點找到它的後代節點(NULL葉節點)的全部路徑包含相同數量的黑色節點。

插入節點

向紅黑樹中插入節點的邏輯與二叉樹同樣,除了插入的邏輯外,咱們還須要在插入後給節點應用一種顏色,而且驗證樹是否知足紅黑樹的條件以及是否仍是自平衡的。

  • 咱們須要建立一個新的節點類用來描述紅黑樹: 節點的顏色、父節點的引用
  • 重寫insert方法,若是樹是空的則建立一個新的紅黑樹節點做爲根節點,將根節點的顏色設爲黑色
  • 若是插入時,樹不爲空咱們會像二叉搜索樹同樣在正確的位置插入節點,節點插入完成後判斷紅黑樹的規則是否獲得知足
  • 重寫插入節點函數,插入時保存父節點的引用,返回節點的引用驗證插入後樹的屬性

驗證紅黑樹屬性

要驗證紅黑樹是否仍是平衡的以及知足它的全部要求,咱們須要使用兩個概念:從新填色和旋轉。

在向樹中插入節點後,新節點將會是紅色。這不會影響黑色節點數量的規則(規則6),但會影響規則5: 兩個後代紅節點不能共存。若是插入節點的父節點是黑色,那麼沒有問題。可是若是插入節點的父節點是紅色,那麼會違反規則5。要解決這個衝突,咱們就只須要改變父節點、祖父節點和叔節點,下圖描述了這個過程

3531ed12959d51f49e47cf79f8fef9b2

驗證紅黑樹的屬性:

  • 從插入的節點開始,咱們要驗證它的父節點是不是紅色,以及這個節點不爲黑色。

  • 驗證父節點是祖父節點的左側子節點仍是右側子節點

  • 若是父節點是祖父節點的左側子節點會有3種情形

    1. 叔節點是紅色,此時咱們只須要從新進行填色便可
    2. 節點是父節點的右側子節點,此時咱們須要進行左旋轉
    3. 節點是父節點的左側子節點,此時咱們須要進行右旋轉
  • 若是父節點是祖父節點的右側子節點也會有3種情形

    1. 叔節點是紅色,從新填色便可
    2. 節點是左側子節點,右旋轉
    3. 節點是右側子節點,左旋轉
  • 設置根節點顏色,因爲根節點必須爲黑色,咱們進行上述操做後,根節點的顏色可能會被改變,所以咱們須要將其設爲黑色

實現代碼

  • 新建RedBlackTree.ts文件
  • 聲明RedBlackTree類,建立RedBlackNode輔助類,繼承BinarySearchTree類
export class RedBlackNode<K> extends Node<K> {
    public left: RedBlackNode<K> | undefined;
    public right: RedBlackNode<K> | undefined;
    public parent: RedBlackNode<K> | undefined;
    public color: number;
    constructor(public key: K) {
        super(key);
        this.color = Colors.RED;
    }

    isRed() {
        return this.color === Colors.RED;
    }
}
複製代碼
export default class RedBlackTree<T> extends BinarySearchTree<T> {
    protected root: RedBlackNode<T> | undefined;
    constructor(protected compareFn: ICompareFunction<T> = defaultCompare) {
        super(compareFn);
    }
}
複製代碼
  • 重寫insert方法
insert(key: T) {
    if (this.root == null) {
        // 樹爲空,建立一個紅黑樹節點
        this.root = new RedBlackNode(key);
        // 紅黑樹的特色2: 根節點的顏色爲黑色
        this.root.color = Colors.BLACK;
    } else {
        // 在合適的位置插入節點, insertNode方法返回新插入的節點
        const newNode = this.insertNode(this.root, key);
        // 節點插入後,驗證紅黑樹屬性
        this.fixTreeProperties(newNode);
    }
}


protected insertNode(node: RedBlackNode<T>, key: T): RedBlackNode<T> {
    // 當前插入key小於當前節點的key
    if (this.compareFn(key, node.key) === Compare.LESS_THAN) {
    if (node.left == null) { // 當前節點的左子節點爲null
        // 在當前節點的左子節點建立一個紅黑樹節點
        node.left = new RedBlackNode(key);
        // 保存父節點的引用
        node.left.parent = node;
        // 返回節點的引用
        return node.left;
    } else {
        // 當前節點的左子節點不爲null, 遞歸尋找合適的位置將其插入
        return this.insertNode(node.left,key);
    }
    } else if (node.right == null) { // 右子節點爲null
        // 在當前節點的右子節點建立一個紅黑樹節點
        node.right = new RedBlackNode(key);
        // 保存父節點的引用
        node.right.parent = node;
        // 返回節點的引用
        return node.right;
    } else {
        // 遞歸尋找合適的位置將其插入
        return this.insertNode(node.right, key);
    }
}
複製代碼
  • 實現紅黑樹屬性校驗方法
private fixTreeProperties(node: RedBlackNode<T>) {
    /** * 從插入的節點開始驗證: * 1. 當前節點存在且節點的父節點顏色是紅色 * 2. 當前節點的顏色不爲黑色 */
    while (node && node.parent && node.parent.color === Colors.RED && node.color !== Colors.BLACK) {
        // 保證代碼可讀性: 保存當前節點的父節點引用以及祖父節點引用
        let parent = node.parent;
        const grandParent = <RedBlackNode<T>>parent.parent;
        // 父節點是祖父節點的左側子節點: 情形A
        if (grandParent && grandParent.left === parent) {
            // 獲取叔節點
            const uncle =grandParent.left;
            // 情形1A: 叔節點也是紅色 -- 只須要從新填色
            if (uncle && uncle.color === Colors.RED) {
                grandParent.color = Colors.RED;
                parent.color = Colors.BLACK;
                uncle.color = Colors.BLACK;
                node = grandParent;
            } else {
                // 情形2A: 節點是父節點是右側子節點 -- 左旋轉
                if (node === parent.right) {
                    this.rotationRR(parent);
                    node = parent;
                    parent = <RedBlackNode<T>>node.parent;
                }
                // 情形3A: 節點是父節點的左側子節點 -- 右旋轉
                this.rotationLL(grandParent);
                parent.color = Colors.BLACK;
                grandParent.color = Colors.RED;
                node = parent;
            }
        } else { // 父節點是右側子節點: 情形B
            // 獲取叔節點
            const uncle = grandParent.left;
            // 情形1B: 叔節點是紅色 -- 只須要填色
            if (uncle && uncle.color === Colors.RED) {
                grandParent.color = Colors.RED;
                parent.color = Colors.BLACK;
                uncle.color = Colors.BLACK;
                node = grandParent;
            } else {
                // 情形2B: 節點是左側子節點 -- 右旋轉
                if (node === parent.left) {
                    this.rotationLL(parent);
                    node = parent;
                    parent = <RedBlackNode<T>>node.parent;
                }
                // 情形3B: 節點是右側子節點 -- 左旋轉
                this.rotationRR(grandParent);
                parent.color = Colors.BLACK;
                grandParent.color = Colors.RED;
                node = parent;
            }
        }
    }
    // 設置根節點顏色
    this.root != null? this.root.color = Colors.BLACK : this.root;
}
複製代碼
  • 實現左旋轉與右旋轉
// 向右的單旋轉
private rotationLL(node: RedBlackNode<T>) {
    const tmp = <RedBlackNode<T>>node.left;
    node.left = tmp.right;
    if (tmp.right && tmp.right.key) {
        tmp.right.parent = node;
    }
    tmp.parent = node.parent;
    if (!node.parent) {
        this.root = tmp;
    } else {
        if (node === node.parent.left) {
            node.parent.left = tmp;
        } else {
            node.parent.right = tmp;
        }
        tmp.right = node;
        node.parent = tmp;
    }
}

// 向左的單旋轉
private rotationRR(node: RedBlackNode<T>) {
    const tmp = <RedBlackNode<T>>node.right;
    node.right = tmp.left;
    if (tmp.left && tmp.left.key) {
        tmp.left.parent = node;
    }
    tmp.parent = node.parent;
    if (!node.parent) {
        this.root = tmp;
    } else {
        if (node === node.parent.left) {
            node.parent.left = tmp;
        } else {
            node.parent.right = tmp;
        }
    }
    tmp.left = node;
    node.parent = tmp;
}
複製代碼

完整代碼請移步: RedBlackTree.ts

編寫測試代碼

上面咱們實現了紅黑樹,接下來咱們來測試下咱們實現的方法是否都能正確執行

const redBlackTree = new RedBlackTree();
redBlackTree.insert(1);
redBlackTree.insert(2);
redBlackTree.insert(3);
redBlackTree.insert(4);
redBlackTree.insert(5);
redBlackTree.insert(6);
redBlackTree.insert(7);
redBlackTree.insert(8);
redBlackTree.insert(9);
const printNode = value => console.log(value);
redBlackTree.preOrderTraverse(printNode);
複製代碼

執行結果以下:

314a102b08e51e5868f00a739894b8c7

文末彩蛋

至此,咱們學完了二叉搜索樹以及它兩種自平衡樹的實現。

接下來,咱們來玩個尋寶遊戲,這個遊戲規則以下:

  • 上圖藏着這個寶藏的文件名
  • 已知這個文件的開頭爲: 24e9e
  • 解出文件名後,拼上 www.kaisir.cn/uploads/ 這個地址(訪問時用https),在瀏覽器訪問便可獲取寶藏

友情提示: 尋找寶藏會用到二叉搜索樹,傳送門: TypeScript實現二叉搜索樹

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊
  • 本文首發於掘金,未經許可禁止轉載💌
相關文章
相關標籤/搜索