在計算機科學中,樹是一種十分重要的數據結構。樹被描述爲一種分層數據抽象模型,經常使用來描述數據間的層級關係和組織結構。樹也是一種非順序的數據結構。下圖展現了樹的定義:html
在介紹如何用JavaScript實現樹以前,咱們先介紹一些和樹相關的術語。node
如上圖所示,一棵完整的樹包含一個位於樹頂部的節點,稱之爲根節點(11),它沒有父節點。樹中的每個元素都叫作一個節點,節點分爲內部節點(圖中顯示爲黃色的節點)和外部節點(圖中顯示爲灰色的節點),至少有一個子節點的節點稱爲內部節點,沒有子元素的節點稱爲外部節點或葉子節點。一個節點能夠有祖先(根節點除外)和後代。子樹由節點自己和它的後代組成,如上圖中三角虛框中的部分就是一棵子樹。節點擁有的子樹的個數稱之爲節點的度,如上圖中除葉子節點的度爲0外,其他節點的度都爲2。從根節點開始,根爲第1層,第一級子節點爲第2層,第二級子節點爲第3層,以此類推。樹的高度(深度)由樹中節點的最大層級決定(上圖中樹的高度爲4)。數據結構
在一棵樹中,具備相同父節點的一組節點稱爲兄弟節點,如上圖中的3和六、5和9等都是兄弟節點。ide
二叉樹中的節點最多隻能有兩個子節點,一個是左子節點,一個是右子節點。左右子節點的順序不能顛倒。所以,二叉樹中不存在度大於2的節點。函數
二叉搜索樹(BST——Binary Search Tree)是二叉樹的一種,它規定在左子節點上存儲小(比父節點)的值,在右子節點上(比父節點)存儲大(或等於)的值。上圖就是一個二叉搜索樹。post
下面咱們重點來看一下二叉搜索樹的實現。性能
根據二叉樹的描述,一個節點最多隻有兩個子節點,咱們可使用《JavaScript數據結構——鏈表的實現與應用》一文中的雙向鏈表來實現二叉搜索樹中的每個節點。下面是二叉搜索樹的數據結構示意圖:測試
如下是咱們要實現的BinarySearchTree類的骨架部分:this
class BinarySearchTree { constructor () { this.root = null; } // 向樹中插入一個節點 insert (key) {} // 在樹中查找一個節點 search (key) {} // 經過中序遍歷方式遍歷樹中的全部節點 inOrderTraverse () {} // 經過先序遍歷方式遍歷樹中的全部節點 preOrderTraverse () {} // 經過後序遍歷方式遍歷樹中的全部節點 postOrderTraverse () {} // 返回樹中的最小節點 min () {} // 返回樹中的最大節點 max () {} // 從樹中移除一個節點 remove (key) {} }
先來看看向樹中添加一個節點。咱們借用《JavaScript數據結構——鏈表的實現與應用》一文中的雙向鏈表DoubleLinkedList類來模擬樹中的節點,在DoubleLinkedList類中,每個節點有三個屬性:element、next和prev。咱們在這裏用element表示樹中節點的key,用next表示樹中節點的右子節點(right),用prev表示樹中節點的左子節點(left)。spa
insert (key) { let newNode = new Node(key); if (this.root === null) this.root = newNode; else insertNode(this.root, newNode); }
當樹的root爲null時,表示樹爲空,這時直接將新添加的節點做爲樹的根節點。不然,咱們須要藉助於私有函數insertNode()來完成節點的添加。在insertNode()函數中,咱們須要根據新添加節點的key的大小來遞歸查找樹的左側子節點或者右側子節點,由於根據咱們的二叉搜索樹的定義,值小的節點永遠保存在左側子節點上,值大的節點(包括值相等的狀況)永遠保存在右側子節點上。下面是insertNode()函數的實現代碼:
let insertNode = function (node, newNode) { if (newNode.element < node.element) { if (node.prev === null) node.prev = newNode; else insertNode(node.prev, newNode); } else { if (node.next === null) node.next = newNode; else insertNode(node.next, newNode); } };
全部新節點只能做爲葉子節點被添加到樹中。在本文一開始給出的樹的結構圖中,若是要添加節點2,對應的操做步驟以下:
咱們傳入樹的根節點,依次進行遞歸,找到對應的葉子節點,而後修改節點的prev(左子節點)或next(右子節點)指針,使其指向新添加的節點。在上例中,若是要添加節點4,它對應的位置應該是節點3的右子節點,由於4比3大。若是要添加節點21,對應的位置應該是節點25的左子節點......
下面咱們來看看樹的三種遍歷方式:
下面的三個方法對應樹的三種遍歷方式:
// 前序遍歷 let preOrderTraverseNode = function (node, callback) { if (node !== null) { callback(node.element); preOrderTraverseNode(node.prev, callback); preOrderTraverseNode(node.next, callback); } }; // 中序遍歷 let inOrderTraverseNode = function (node, callback) { if (node !== null) { inOrderTraverseNode(node.prev, callback); callback(node.element); inOrderTraverseNode(node.next, callback); } }; // 後續遍歷 let postOrderTraverseNode = function (node, callback) { if (node !== null) { postOrderTraverseNode(node.prev, callback); postOrderTraverseNode(node.next, callback); callback(node.element); } };
能夠看到,這三個函數的內容很類似,只是調整了左右子樹和根節點的遍歷順序。這裏的callback是一個回調函數,能夠傳入任何你想執行的函數,這裏咱們傳入的函數內容是打印樹的節點的key值。咱們將BinarySearchTree類的這三個遍歷方法的內容補充完整:
preOrderTraverse (callback) { preOrderTraverseNode(this.root, callback); } inOrderTraverse (callback) { inOrderTraverseNode(this.root, callback); } postOrderTraverse (callback) { postOrderTraverseNode(this.root, callback); }
爲了構建本文一開始的那棵樹,咱們執行下面的代碼,而後測試preOrderTraverse()方法:
let tree = new BinarySearchTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(5); tree.insert(9); tree.insert(13); tree.insert(20); tree.insert(3); tree.insert(6); tree.insert(8); tree.insert(10); tree.insert(12); tree.insert(14); tree.insert(18); tree.insert(25); tree.preOrderTraverse((value) => console.log(value));
注意節點插入的順序,順序不一樣,你可能會獲得不同的樹。preOrderTraverse()方法採用ES6的語法傳入了一個匿名函數做爲參數callback的值,這個匿名函數的主要做用就是打印樹中節點的key值,能夠對照上面三個遍歷樹節點的函數中的callback(node.element)語句,這裏的callback就是這個匿名函數,node.element就是節點的key值(還記得前面咱們說過,借用雙向鏈表類DoubleLinkedList來模擬樹的節點嗎?)下面是前序遍歷的執行結果:
11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
咱們參照前序遍歷的定義,借住下面的示意圖來理解整個遍歷過程:
在前序遍歷函數preOrderTraverseNode()中,先執行callback(node.element),而後再依次遞歸左子樹和右子樹。咱們將樹的根節點做爲第一個節點傳入,首先打印的就是根節點11,而後開始遍歷左子樹,這將依次打印左子樹中的全部左子節點,依次是七、五、3。當節點3的prev爲null時,遞歸返回,繼續查找節點3的右子節點,此時節點3的next值也爲null,因而繼續向上返回到節點5,開始遍歷節點5的右子節點,因而打印節點6......最終全部的節點就按照這個遞歸順序進行遍歷。
而後咱們再來看看中序遍歷的狀況。
tree.inOrderTraverse((value) => console.log(value));
3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
在中序遍歷函數inOrderTraverseNode()中,先遞歸左子樹,而後執行callback(node.element),最後再遞歸右子樹。一樣的,咱們將根節點做爲第一個節點傳入,遞歸到左子樹的最後一個左子節點3,因爲節點3的prev爲null,因此遞歸返回,打印節點3,而後繼續查找節點3的右子節點,節點3的next值也爲null,遞歸返回到上一層節點5,開始打印節點5,以後再查找節點5的右子節點......最終整棵樹按照這個順序完成遍歷。
最後再來看看後序遍歷的狀況。
tree.postOrderTraverse((value) => console.log(value));
3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
在後序遍歷函數postOrderTraverseNode()中,先遞歸左子樹,而後再遞歸右子樹,最後執行callback(node.element)。一樣的,咱們將根節點做爲第一個節點傳入,遞歸到左子樹的最後一個左子節點3,因爲節點3的prev爲null,因此遞歸返回,此時繼續查找節點3的右子節點,節點3的next值也爲null,遞歸返回並打印節點3,以後遞歸返回到上一層節點5,開始查找節點5的右子節點,節點5的右子節點是節點6,因爲節點6是葉子節點,因此直接打印節點6,而後遞歸返回並打印節點5。以後遞歸再向上返回到節點7並遞歸節點7的右子節點......按照這個順序最終完成對整棵樹的遍歷。
接下來咱們再來看看對樹的搜索。有三種要常常執行的搜索方式:
搜索樹中的最小值和最大值比較簡單,因爲咱們的二叉搜索樹規定了值小的節點永遠在左子樹(左子節點)中,值大(或相等)的節點永遠在右子樹(右子節點)中,因此,搜索最大值咱們只須要遞歸查找樹的右子樹直到葉子節點,就能找到值最大的節點。搜索最小值只須要遞歸查找樹的左子樹直到葉子節點,就能找到值最小的節點。下面是這兩個函數的實現:
let minNode = function (node) { if (node === null) return null; while (node && node.prev !== null) { node = node.prev; } return node; }; let maxNode = function (node) { if (node === null) return null; while (node && node.next !== null) { node = node.next; } return node; };
第三種方式是搜索特定的值,咱們須要比較要搜索的值與當前節點的值,若是要搜索的值小於當前節點的值,則從當前節點開始遞歸查找左子數(左子節點)。若是要搜索的值大於當前節點的值,則從當前節點開始遞歸查找右子樹(右子節點)。按照這個邏輯,咱們的searchNode()函數實現以下:
let searchNode = function (node, key) { if (node === null) return null; if (key < node.element) return searchNode(node.prev, key); else if (key > node.element) return searchNode(node.next, key); else return node; };
若是找到了對應的節點,就返回該節點,不然就返回null。咱們將BinarySearchTree類的這三個搜索方法的內容補充完整:
search (key) { return searchNode(this.root, key); } min () { return minNode(this.root); } max () { return maxNode(this.root); }
下面是一些測試用例及結果:
console.log(tree.min().element); // 3 console.log(tree.max().element); // 25 console.log(tree.search(1) ? 'Key 1 found.' : 'Key 1 not found.'); // Key 1 not found. console.log(tree.search(8) ? 'Key 8 found.' : 'Key 8 not found.'); // Key 8 found.
讓咱們來看一下search()方法的執行過程是怎樣的。
搜索key=1的節點,首先咱們傳入樹的根節點和key=1,因爲1小於根節點的值11,遞歸查找根節點的左子節點7,1<7,繼續查找節點7的左子節點,直到找到葉子節點3,1仍然小於3,可是節點3沒有左子節點了,因此返回false,整個遞歸開始向上返回,最終返回的結果是false,表示樹中沒有key=1的節點。
相應地,對於搜索key=8的節點,也是先遍歷根節點的左子節點7,因爲8>7,因此會遍歷節點7的右子節點,找到節點9,8<9,遍歷節點9的左子節點,此時找到節點9的左子節點正好是8,因此返回true,而後整個遞歸向上返回,最終的返回結果就是true,表示樹中找到了key=8的節點。
最後咱們再來看一下從樹中移除一個節點的過程,這個過程要稍微複雜一些。先來看看刪除樹節點的函數removeNode()的代碼,稍後咱們再來詳細講解整個執行過程。
let removeNode = function (node, key) { if (node === null) return null; if (key < node.element) { node.prev = removeNode(node.prev, key); return node; } else if (key > node.element) { node.next = removeNode(node.next, key); return node; } else { // 第一種狀況:一個葉子節點(沒有子節點) if (node.prev === null && node.next === null) { node = null; return node; } // 第二種狀況:只包含一個子節點 if (node.prev === null) { node = node.next; return node; } else if (node.next === null) { node = node.prev; return node; } // 第三種狀況:有兩個子節點 let aux = minNode(node.next); node.element = aux.element; node.next = removeNode(node.next, aux.element); return node; } };
首先要找到樹中待刪除的節點,這須要進行遞歸遍歷,從根節點開始,若是key值小於當前節點的值,則遍歷左子樹,若是key值大於當前節點的值,則遍歷右子樹。注意,在遞歸遍歷的過程當中,咱們將node(這裏的node傳入的是樹的根節點)的prev指針或next指針逐級指向下一級節點,而後返回整個node。當找到要刪除的節點後,咱們要處理三種狀況:
咱們先看第一種狀況:
假設咱們要刪除節點6,傳入根節點11,整個執行過程以下:
而後咱們來看只有一個子節點的狀況:
前面已經刪除了節點6,假設咱們如今要刪除節點5,它有一個左子節點3,咱們依然傳入根節點11,來看看整個執行過程:
咱們不須要將節點5從內存中刪除,它會自動被JavaScript的垃圾回收器清理掉,這個在《JavaScript數據結構——鏈表的實現與應用》一文中已經介紹過。以上步驟是針對目標節點有左子節點的狀況,對於有右子節點狀況,執行過程是相似的。
最後再來看第三種狀況:
前面已經刪除了節點6和節點5,如今咱們要刪除節點15,它有左右子樹,咱們傳入根節點11,來看下具體執行過程:
試想一下,當刪除節點15以後,爲了保證咱們的二叉搜索樹結構穩定,必須用節點15的右子樹中的最小節點來替換節點15,若是直接將11的next指向20,則20將會有三個子節點1三、1八、25,這顯然已經不符合咱們二叉樹的定義了。若是將節點25用來替換節點15,節點20的值比節點25的值小,不該該出如今右子節點,這也不符合咱們的二叉搜索樹的定義。因此,只有按照上述過程才能既保證不破壞樹的結構,又能刪除節點。
咱們已經完成了一開始咱們定義的二叉搜索樹BinarySearchTree類的全部方法,下面是它的完整代碼:
1 let insertNode = function (node, newNode) { 2 if (newNode.element < node.element) { 3 if (node.prev === null) node.prev = newNode; 4 else insertNode(node.prev, newNode); 5 } 6 else { 7 if (node.next === null) node.next = newNode; 8 else insertNode(node.next, newNode); 9 } 10 }; 11 12 let preOrderTraverseNode = function (node, callback) { 13 if (node !== null) { 14 callback(node.element); 15 preOrderTraverseNode(node.prev, callback); 16 preOrderTraverseNode(node.next, callback); 17 } 18 }; 19 20 let inOrderTraverseNode = function (node, callback) { 21 if (node !== null) { 22 inOrderTraverseNode(node.prev, callback); 23 callback(node.element); 24 inOrderTraverseNode(node.next, callback); 25 } 26 }; 27 28 let postOrderTraverseNode = function (node, callback) { 29 if (node !== null) { 30 postOrderTraverseNode(node.prev, callback); 31 postOrderTraverseNode(node.next, callback); 32 callback(node.element); 33 } 34 }; 35 36 let minNode = function (node) { 37 if (node === null) return null; 38 39 while (node && node.prev !== null) { 40 node = node.prev; 41 } 42 return node; 43 }; 44 45 let maxNode = function (node) { 46 if (node === null) return null; 47 48 while (node && node.next !== null) { 49 node = node.next; 50 } 51 return node; 52 }; 53 54 let searchNode = function (node, key) { 55 if (node === null) return false; 56 57 if (key < node.element) return searchNode(node.prev, key); 58 else if (key > node.element) return searchNode(node.next, key); 59 else return true; 60 }; 61 62 let removeNode = function (node, key) { 63 if (node === null) return null; 64 65 if (key < node.element) { 66 node.prev = removeNode(node.prev, key); 67 return node; 68 } 69 else if (key > node.element) { 70 node.next = removeNode(node.next, key); 71 return node; 72 } 73 else { 74 // 第一種狀況:一個葉子節點(沒有子節點) 75 if (node.prev === null && node.next === null) { 76 node = null; 77 return node; 78 } 79 // 第二種狀況:只包含一個子節點 80 if (node.prev === null) { 81 node = node.next; 82 return node; 83 } 84 else if (node.next === null) { 85 node = node.prev; 86 return node; 87 } 88 89 // 第三種狀況:有兩個子節點 90 let aux = minNode(node.next); 91 node.element = aux.element; 92 node.next = removeNode(node.next, aux.element); 93 return node; 94 } 95 }; 96 97 class BinarySearchTree { 98 constructor () { 99 this.root = null; 100 } 101 102 // 向樹中插入一個節點 103 insert (key) { 104 let newNode = new Node(key); 105 106 if (this.root === null) this.root = newNode; 107 else insertNode(this.root, newNode); 108 } 109 110 // 在樹中查找一個節點 111 search (key) { 112 return searchNode(this.root, key); 113 } 114 115 // 經過先序遍歷方式遍歷樹中的全部節點 116 preOrderTraverse (callback) { 117 preOrderTraverseNode(this.root, callback); 118 } 119 120 // 經過中序遍歷方式遍歷樹中的全部節點 121 inOrderTraverse (callback) { 122 inOrderTraverseNode(this.root, callback); 123 } 124 125 // 經過後序遍歷方式遍歷樹中的全部節點 126 postOrderTraverse (callback) { 127 postOrderTraverseNode(this.root, callback); 128 } 129 130 // 返回樹中的最小節點 131 min () { 132 return minNode(this.root); 133 } 134 135 // 返回樹中的最大節點 136 max () { 137 return maxNode(this.root); 138 } 139 140 // 從樹中移除一個節點 141 remove (key) { 142 this.root = removeNode(this.root, key); 143 } 144 }
上面的BST樹(二叉搜索樹)存在一個問題,樹的一條邊可能會很是深,而其它邊卻只有幾層,這會在這條很深的分支上添加、移除和搜索節點時引發一些性能問題。以下圖所示:
爲了解決這個問題,咱們引入了自平衡二叉搜索樹(AVL——Adelson-Velskii-Landi)。在AVL中,任何一個節點左右兩棵子樹的高度之差最多爲1,添加或移除節點時,AVL樹會嘗試自平衡。對AVL樹的操做和對BST樹的操做同樣,不一樣點在於咱們還須要從新平衡AVL樹,在講解對AVL樹的平衡操做以前,咱們先看一下什麼是AVL樹的平衡因子。
前面咱們介紹過什麼是樹(子樹)的高度,對於AVL樹來講,每個節點都保存一個平衡因子。
節點的平衡因子 = 左子樹的高度 - 右子樹的高度
觀察下面這棵樹,咱們在上面標註了每一個節點的平衡因子的值:
全部子節點的平衡因子都爲0,由於子節點沒有子樹。節點5的左右子樹的高度都爲1,因此節點5的平衡因子是0。節點9的左子樹高度爲1,右子樹高度爲0,因此節點9的平衡因子是+1。節點13的左子樹高度爲0,右子樹高度爲1,因此節點13的平衡因子是-1......AVL樹的全部節點的平衡因子保持三個值:0、+1或-1。同時,咱們也注意到,當某個節點的平衡因子爲+1時,它的子樹是向左傾斜的(left-heavy);而當某個節點的平衡因子爲-1時,它的子樹是向右傾斜的(right-heavy);當節點的平衡因子爲0時,該節點是平衡的。一顆子樹的根節點的平衡因子表明了該子樹的平衡性。
爲了使AVL樹從新達到平衡狀態,咱們須要對AVL樹中的部分節點進行從新排列,使其既符合二叉搜索樹的定義,又符合自平衡二叉樹的定義,這個過程叫作AVL樹的旋轉。
AVL樹的旋轉一共分爲四種:
下面是這四種旋轉的操做示意圖,後面咱們會詳細介紹每一種旋轉的操做過程:
對於LL旋轉,在節點5的右子節點上添加節點4與在左子節點上添加節點3等同。對於LR旋轉,在節點9的左子節點上添加節點8與在右子節點上添加節點10等同。對於RR旋轉,在節點20的右子節點上添加節點25與在左子節點上添加節點18等同。對於RL旋轉,在節點13的右子節點上添加節點14與在左子節點上添加節點12等同。
咱們的自平衡二叉樹AVLTree類將從BinarySearchTree類繼承,同時咱們須要新增一個方法getNodeHeight()用來獲取任意節點的高度。
class AVLTree extends BinarySearchTree { constructor () { super(); } // 計算節點的高度 getNodeHeight (node) { if (node === null) return 0; return Math.max(this.getNodeHeight(node.prev), this.getNodeHeight(node.next)) + 1; }; }
測試一下getNodeHeight()方法,咱們仍是以本文一開始的那棵樹爲例,而後看一下不一樣節點的高度。
let tree = new AVLTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(5); tree.insert(9); tree.insert(13); tree.insert(20); tree.insert(3); tree.insert(6); tree.insert(8); tree.insert(10); tree.insert(12); tree.insert(14); tree.insert(18); tree.insert(25); console.log(tree.getNodeHeight(tree.root)); // 4 console.log(tree.getNodeHeight(tree.search(7))); // 3 console.log(tree.getNodeHeight(tree.search(5))); // 2 console.log(tree.getNodeHeight(tree.min(7))); // 1
根節點的高度爲4,最小節點3的高度爲1,節點5和節點7的高度分別爲2和3。
下面是四種旋轉對應的實現代碼:
/** * LL旋轉: 向右旋轉 * * b a * / \ / \ * a e -> rotationLL(b) -> c b * / \ / / \ * c d f d e * / * f * * @param node Node<T> */ rotationLL(node) { let tmp = node.prev; node.prev = tmp.next; tmp.next = node; return tmp; } /** * RR旋轉: 向左旋轉 * * a b * / \ / \ * c b -> rotationRR(a) -> a e * / \ / \ \ * d e c d f * \ * f * * @param node Node<T> */ rotationRR(node) { let tmp = node.next; node.next = tmp.prev; tmp.prev = node; return tmp; } /** * LR旋轉: 先向左旋轉,而後再向右旋轉 * @param node Node<T> */ rotationLR(node) { node.prev = this.rotationRR(node.prev); return this.rotationLL(node); } /** * RL旋轉: 先向右旋轉,而後再向左旋轉 * @param node Node<T> */ rotationRL(node) { node.next = this.rotationLL(node.next); return this.rotationRR(node); }
對於LL旋轉和RR旋轉,咱們能夠按照上面的示意圖來看下執行過程。
LL旋轉,node=11,node.prev是7,因此tmp=7。而後將node.prev指向tmp.next,即將11的prev指向9。接着將tmp.next指向node,即將7的next指向11。即完成了圖中所示的旋轉。
RR旋轉,node=11,node.next是15,因此tmp=15。而後將node.next指向tmp.prev,即將11的next指向13。接着將tmp.prev指向node,即將15的prev指向11。即完成了圖中所示的旋轉。
LR旋轉是RR旋轉和LL旋轉的組合:
RL旋轉是LL旋轉和RR旋轉的組合:
按照上面給出的示意圖,咱們的AVLTree類的insert()方法的實現以下:
insert (key) { super.insert(key); // 左子樹高度大於右子樹高度 if (this.getNodeHeight(this.root.prev) - this.getNodeHeight(this.root.next) > 1) { if (key < this.root.prev.element) { this.root = this.rotationLL(this.root); } else { this.root = this.rotationLR(this.root); } } // 右子樹高度大於左子樹高度 else if (this.getNodeHeight(this.root.next) - this.getNodeHeight(this.root.prev) > 1) { if (key > this.root.next.element) { this.root = this.rotationRR(this.root); } else { this.root = this.rotationRL(this.root); } } }
咱們依次測試一下這四種狀況。按照上面示意圖中樹的結構添加節點,而後按照前序遍歷的方式打印節點的key。
LL旋轉的結果:
let tree = new AVLTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(5); tree.insert(9); tree.insert(3); tree.preOrderTraverse((value) => console.log(value));
7 5 3 11 9 15
LR旋轉的結果:
let tree = new AVLTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(5); tree.insert(9); tree.insert(8); tree.preOrderTraverse((value) => console.log(value));
9 7 5 8 11 15
RR旋轉的結果:
let tree = new AVLTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(13); tree.insert(20); tree.insert(25); tree.preOrderTraverse((value) => console.log(value));
15 11 7 13 20 25
RL旋轉的結果:
let tree = new AVLTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(13); tree.insert(20); tree.insert(14); tree.preOrderTraverse((value) => console.log(value));
13 11 7 15 14 20
咱們用一樣的方式修改remove()方法,而後測試下面兩種狀況下的節點刪除:
let tree = new AVLTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(5); tree.insert(9); tree.remove(15); tree.preOrderTraverse((value) => console.log(value));
9 7 5 11
let tree = new AVLTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(13); tree.insert(20); tree.remove(7); tree.preOrderTraverse((value) => console.log(value));
13 11 15 20
完整的自平衡二叉搜索樹AVLTree類的代碼以下:
1 class AVLTree extends BinarySearchTree { 2 constructor () { 3 super(); 4 } 5 6 // 計算節點的高度 7 getNodeHeight (node) { 8 if (node === null) return 0; 9 return Math.max(this.getNodeHeight(node.prev), this.getNodeHeight(node.next)) + 1; 10 }; 11 12 // 獲取節點的平衡因子 13 14 /** 15 * LL旋轉: 向右旋轉 16 * 17 * b a 18 * / \ / \ 19 * a e -> rotationLL(b) -> c b 20 * / \ / / \ 21 * c d f d e 22 * / 23 * f 24 * 25 * @param node Node<T> 26 */ 27 rotationLL(node) { 28 let tmp = node.prev; 29 node.prev = tmp.next; 30 tmp.next = node; 31 return tmp; 32 } 33 34 /** 35 * RR旋轉: 向左旋轉 36 * 37 * a b 38 * / \ / \ 39 * c b -> rotationRR(a) -> a e 40 * / \ / \ \ 41 * d e c d f 42 * \ 43 * f 44 * 45 * @param node Node<T> 46 */ 47 rotationRR(node) { 48 let tmp = node.next; 49 node.next = tmp.prev; 50 tmp.prev = node; 51 return tmp; 52 } 53 54 /** 55 * LR旋轉: 先向左旋轉,而後再向右旋轉 56 * @param node Node<T> 57 */ 58 rotationLR(node) { 59 node.prev = this.rotationRR(node.prev); 60 return this.rotationLL(node); 61 } 62 63 /** 64 * RL旋轉: 先向右旋轉,而後再向左旋轉 65 * @param node Node<T> 66 */ 67 rotationRL(node) { 68 node.next = this.rotationLL(node.next); 69 return this.rotationRR(node); 70 } 71 72 insert (key) { 73 super.insert(key); 74 75 // 左子樹高度大於右子樹高度 76 if (this.getNodeHeight(this.root.prev) - this.getNodeHeight(this.root.next) > 1) { 77 if (key < this.root.prev.element) { 78 this.root = this.rotationLL(this.root); 79 } 80 else { 81 this.root = this.rotationLR(this.root); 82 } 83 } 84 // 右子樹高度大於左子樹高度 85 else if (this.getNodeHeight(this.root.next) - this.getNodeHeight(this.root.prev) > 1) { 86 if (key > this.root.next.element) { 87 this.root = this.rotationRR(this.root); 88 } 89 else { 90 this.root = this.rotationRL(this.root); 91 } 92 } 93 } 94 95 remove (key) { 96 super.remove(key); 97 98 // 左子樹高度大於右子樹高度 99 if (this.getNodeHeight(this.root.prev) - this.getNodeHeight(this.root.next) > 1) { 100 if (key < this.root.prev.element) { 101 this.root = this.rotationLL(this.root); 102 } 103 else { 104 this.root = this.rotationLR(this.root); 105 } 106 } 107 // 右子樹高度大於左子樹高度 108 else if (this.getNodeHeight(this.root.next) - this.getNodeHeight(this.root.prev) > 1) { 109 if (key > this.root.next.element) { 110 this.root = this.rotationRR(this.root); 111 } 112 else { 113 this.root = this.rotationRL(this.root); 114 } 115 } 116 } 117 }
儘管自平衡二叉搜索樹AVL能夠頗有效地幫助咱們解決許多樹節點的操做問題,可是在插入和移除節點時其性能並非最好的。更好的選擇是紅黑樹,紅黑樹也是一種自平衡二叉搜索樹,可是它對其中的節點作了不少特殊的規定,使得在操做樹節點的性能上要優於AVL。
下一章咱們將介紹如何用JavaScript來實現圖這種非線性數據結構。