二叉搜索樹存在一個問題: 當往樹中插入的數據一大部分大於某個節點或小於某個節點,這樣就會致使樹的一條邊很是深。爲了解決這個問題就出現了自平衡樹這種解決方案。javascript
本文將詳解兩種自平衡樹:AVL樹和紅黑樹並用TypeScript將其實現,歡迎各位感興趣的開發者閱讀本文。java
本文講解的兩種自平衡樹均基於二叉搜索樹實現,對二叉搜索樹不瞭解的開發者請移步: TypeScript實現二叉搜索樹node
AVL樹(Adelson-Velskii-Landi 樹)是一種自平衡二叉搜索樹,任何一個節點左右兩側子樹的高度之差最多爲1,添加或刪除節點時,AVL樹會盡量嘗試轉換爲徹底樹。git
AVL樹是一顆二叉搜索樹,所以咱們能夠繼承二叉搜索樹,重寫二叉樹的部分方法便可。github
在AVL樹中插入或移除節點和二叉搜索樹徹底相同,然而AVL樹的不一樣之處在於咱們須要校驗它的平衡因子,根據平衡因子來判斷樹是否須要調整,接下來咱們就來看下AVL樹的相關術語:typescript
節點的高度是從節點到任意子節點的邊的最大值,下圖描述了一個包含節點高度的樹。瀏覽器
節點的平衡因子是指在AVL樹中,須要對每一個節點計算右子樹高度(hr)和左子樹高度(hl)之間的差值,該值(hr - hl)應爲0、1或-1。若是結果不是這三個值之一,則須要平衡該AVL樹,下圖中的樹描述了每一個節點的平衡因子。函數
平衡操做 - 樹的旋轉 添加或刪除節點後,咱們須要計算節點的高度獲取平衡因子,根據平衡因子判斷是否須要進行旋轉來平衡這顆樹。樹的平衡有如下場景:post
咱們根據計算出的平衡因子來進行以下相對應的旋轉測試
當節點的左側子節點的高度大於右側子節點的高度時,而且左側子節點也是平衡或左側較重,此時就須要對平衡樹進行LL操做,下圖描述了這個過程
當節點右側子節點的高度大於左側子節點的高度時,而且右側子節點也是平衡或右側較重,此時就須要對平衡樹進行RR操做,下圖描述了這個過程
當左側子節點的高度大於右側子節點的高度時,而且左側子節點右側較重,此時就須要對平衡樹進行左旋轉來修復,這樣就會造成左-左的狀況,而後在對不平衡的節點進行一個右旋轉來修復,下圖描述了須要進行LR的場景
當右側子節點的高度大於左側子節點的高度時,而且右側子節點左側較重,此時就須要對平衡樹進行右旋轉進行修復,這樣會造成右-右的狀況,而後在對不平衡的節點進行一個左旋轉來修復,下圖描述了須要進行RL的場景
向AVL樹中插入或移除節點的邏輯與二叉搜索樹同樣,惟一的不一樣之處在於插入後須要驗證樹是否平衡,若是不平衡則須要進行相應的旋轉操做。
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);
複製代碼
執行結果以下:
紅黑樹:故名思義,即樹中的節點不是紅的就是黑的,它也是一個自平衡二叉樹。上面咱們實現了AVL樹,咱們在向AVL樹中插入或移除節點可能會形成旋轉,因此咱們須要一個包含屢次插入和刪除的自平衡樹,紅黑樹是比較好的。插入或刪除頻率比較低,那麼AVL樹比紅黑樹更好。
紅黑樹的每一個節點都須要遵循如下原則:
向紅黑樹中插入節點的邏輯與二叉樹同樣,除了插入的邏輯外,咱們還須要在插入後給節點應用一種顏色,而且驗證樹是否知足紅黑樹的條件以及是否仍是自平衡的。
要驗證紅黑樹是否仍是平衡的以及知足它的全部要求,咱們須要使用兩個概念:從新填色和旋轉。
在向樹中插入節點後,新節點將會是紅色。這不會影響黑色節點數量的規則(規則6),但會影響規則5: 兩個後代紅節點不能共存。若是插入節點的父節點是黑色,那麼沒有問題。可是若是插入節點的父節點是紅色,那麼會違反規則5。要解決這個衝突,咱們就只須要改變父節點、祖父節點和叔節點,下圖描述了這個過程
驗證紅黑樹的屬性:
從插入的節點開始,咱們要驗證它的父節點是不是紅色,以及這個節點不爲黑色。
驗證父節點是祖父節點的左側子節點仍是右側子節點
若是父節點是祖父節點的左側子節點會有3種情形
若是父節點是祖父節點的右側子節點也會有3種情形
設置根節點顏色,因爲根節點必須爲黑色,咱們進行上述操做後,根節點的顏色可能會被改變,所以咱們須要將其設爲黑色
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(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);
複製代碼
執行結果以下:
至此,咱們學完了二叉搜索樹以及它兩種自平衡樹的實現。
接下來,咱們來玩個尋寶遊戲,這個遊戲規則以下:
友情提示: 尋找寶藏會用到二叉搜索樹,傳送門: TypeScript實現二叉搜索樹