JavaScript實現樹結構(二)
阿新 • • 發佈:2020-03-11
## JavaScript實現樹結構(二)
### 一、二叉搜尋樹的封裝
**二叉樹搜尋樹的基本屬性**:
如圖所示:二叉搜尋樹有四個最基本的屬性:指向節點的**根**(root),節點中的**鍵**(key)、**左指標**(right)、**右指標**(right)。
![image-20200301162706755](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/1.png)
所以,二叉搜尋樹中除了定義root屬性外,還應定義一個節點內部類,裡面包含每個節點中的left、right和key三個屬性:
```javascript
//封裝二叉搜尋樹
function BinarySearchTree(){
//節點內部類
function Node(key){
this.key = key
this.left = null
this.right = null
}
//屬性
this.root = null
}
```
**二叉搜尋樹的常見操作:**
* insert(key):向樹中插入一個新的鍵;
* search(key):在樹中查詢一個鍵,如果節點存在,則返回true;如果不存在,則返回false;
* inOrderTraverse:通過中序遍歷方式遍歷所有節點;
* preOrderTraverse:通過先序遍歷方式遍歷所有節點;
* postOrderTraverse:通過後序遍歷方式遍歷所有節點;
* min:返回樹中最小的值/鍵;
* max:返回樹中最大的值/鍵;
* remove(key):從樹中移除某個鍵;
#### 1.插入資料
**實現思路:**
* 首先根據傳入的key建立節點物件;
* 然後判斷根節點是否存在,不存在時通過:this.root = newNode,直接把新節點作為二叉搜尋樹的根節點。
* 若存在根節點則重新定義一個內部方法insertNode()用於查詢插入點。
```JavaScript
//insert方法:對外向使用者暴露的方法
BinarySearchTree.prototype.insert = function(key){
//1.根據key建立節點
let newNode = new Node(key)
//2.判斷根節點是否存在
if (this.root == null) {
this.root = newNode
//根節點存在時
}else {
this.insertNode(this.root, newNode)
}
}
```
**內部方法insertNode()的實現思路**:
根據比較傳入的兩個節點,一直查詢新節點適合插入的位置,直到成功插入新節點為止。
當newNode.key < node.key向左查詢:
* 情況1:當node無左子節點時,直接插入:
* 情況2:當node有左子節點時,遞迴呼叫insertNode(),直到遇到無左子節點成功插入newNode後,不再符合該情況,也就不再呼叫insertNode(),遞迴停止。
![image-20200301191640632](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/2.png)
當newNode.key >= node.key向右查詢,與向左查詢類似:
* 情況1:當node無右子節點時,直接插入:
* 情況2:當node有右子節點時,依然遞迴呼叫insertNode(),直到遇到傳入insertNode方法的node無右子節點成功插入newNode為止:
![image-20200301191507181](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/3.png)
**insertNode()程式碼實現:**
```
//內部使用的insertNode方法:用於比較節點從左邊插入還是右邊插入
BinarySearchTree.prototype.insertNode = function(node, newNode){
//當newNode.key < node.key向左查詢
/*----------------------分支1:向左查詢--------------------------*/
if(newNode.key < node.key){
//情況1:node無左子節點,直接插入
/*----------------------分支1.1--------------------------*/
if (node.left == null) {
node.left = newNode
//情況2:node有左子節點,遞迴呼叫insertNode(),直到遇到無左子節點成功插入newNode後,不再符合該情況,也就不再呼叫insertNode(),遞迴停止。
/*----------------------分支1.2--------------------------*/
}else{
this.insertNode(node.left, newNode)
}
//當newNode.key > = node.key向右查詢
/*-----------------------分支2:向右查詢--------------------------*/
}else{
//情況1:node無右子節點,直接插入
/*-----------------------分支2.1--------------------------*/
if(node.right == null){
node.right == newNode
//情況2:node有右子節點,依然遞迴呼叫insertNode(),直到遇到無右子節點成功插入newNode為止
/*-----------------------分支2.2--------------------------*/
}else{
this.insertNode(node.right, newNode)
}
}
}
```
**過程詳解:**
為了更好理解以下列二叉搜尋樹為例:
![image-20200301193104003](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/4.png)
想要上述的二叉搜尋樹(藍色)中插入資料10:
* 先把key = 10 傳入insert方法,由於存在根節點 9,所以直接呼叫insetNode方法,傳入的引數:node = 9,newNode = 10;
* 由於10 > 9,進入分支2,向右查詢適合插入的位置;
* 由於根節點 9 的右子節點存在且為 13 ,所以進入分支2.2,遞迴呼叫insertNode方法,傳入的引數:node = 13,newNode = 10;
* 由於 10 < 13 ,進入分支1,向左查詢適合插入的位置;
* 由於父節點 13 的左子節點存在且為11,所以進入分支1.2,遞迴呼叫insertNode方法,傳入的引數:node = 11,newNode = 10;
* 由於 10 < 11,進入分支1,向左查詢適合插入的位置;
* 由於父節點 11 的左子節點不存在,所以進入分支1.1,成功插入節點 10 。由於不符合分支1.2的條件所以不會繼續呼叫insertNode方法,遞迴停止。
**測試程式碼:**
```
//測試程式碼
//1.建立BinarySearchTree
let bst = new BinarySearchTree()
//2.插入資料
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(9);
console.log(bst);
```
應得到下圖所示的二叉搜尋樹:
![image-20200302002708576](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/5.png)
**測試結果**
![image-20200302002409735](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/6.png)
#### 2.遍歷資料
這裡所說的樹的遍歷不僅僅針對二叉搜尋樹,而是適用於所有的二叉樹。由於樹結構不是線性結構,所以遍歷方式有多種選擇,常見的三種二叉樹遍歷方式為:
* 先序遍歷;
* 中序遍歷;
* 後序遍歷;
還有層序遍歷,使用較少。
##### 2.1.先序遍歷
先序遍歷的過程為:
* 首先,遍歷根節點;
* 然後,遍歷其左子樹;
* 最後,遍歷其右子樹;
![image-20200301213506159](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/7.png)
如上圖所示,二叉樹的節點遍歷順序為:A -> B -> D -> H -> I -> E -> C -> F -> G。
**程式碼實現:**
```
//先序遍歷
//摻入一個handler函式方便之後對得到的key進行處理
BinarySearchTree.prototype.preOrderTraversal = function(handler){
this.preOrderTraversalNode(this.root, handler)
}
//封裝內部方法,對某個節點進行遍歷
BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler){
if (node != null) {
//1.處理經過的節點
handler(node.key)
/*----------------------遞迴1----------------------------*/
//2.遍歷左子樹中的節點
this.preOrderTraversalNode(node.left, handler)
/*----------------------遞迴2----------------------------*/
//3.遍歷右子樹中的節點
this.preOrderTraversalNode(node.right, handler)
}
}
```
**過程詳解:**
以遍歷以下二叉搜尋樹為例:
![image-20200301221450001](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/8.png)
首先呼叫preOrderTraversal方法,在方法裡再呼叫preOrderTraversalNode方法用於遍歷二叉搜尋樹。在preOrderTraversalNode方法中,遞迴1負責遍歷左子節點,遞迴2負責遍歷右子節點。先執行遞迴1,執行過程如下圖所示:
> **記:preOrderTraversalNode() 為 A()**
![image-20200302000248291](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/9.png)
可以看到一共遞迴呼叫了4次方法A,分別傳入11、7、5、3,最後遇到null不滿足 node != null 條件結束遞迴1;注意此時只是執行完最開始的遞迴1,並沒有執行遞迴2,並且遞迴1執行到null停止後要一層層地往上返回,按順序將呼叫的函式壓出函式呼叫棧。
關於函式呼叫棧:之前的四次遞迴共把4個函式壓入了函式呼叫棧,現在遞迴執行完了一層層地把函式壓出棧。
值得注意的是:每一層函式都只是執行完了遞迴1,當返回到該層函式時,比如A(3)要繼續執行遞迴2遍歷二叉搜尋樹中的右子節點;
在執行遞迴2的過程中會不斷呼叫方法A,並依次執行遞迴1和遞迴2,以此類推直到遇到null不滿足 node != null 條件為止,才停止遞歸併一層層返回,如此迴圈。同理A(5)層、A(7)層、A(11)層都要經歷上述迴圈,直到將二叉搜尋樹中的節點全部遍歷完為止。
具體過程如下圖所示:
![image-20200302000007414](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/10.png)
**測試程式碼:**
```
//測試程式碼
//1.建立BinarySearchTree
let bst = new BinarySearchTree()
//2.插入資料
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(6);
//3.測試遍歷
let resultString = ""
//摻入處理節點值的處理函式
bst.preOrderTraversal(function(key){
resultString += key + "->"
})
alert(resultString)
```
應輸出這樣的順序:11 -> 7 -> 5 -> 3 -> 6 -> 9 -> 8 -> 10 -> 15 -> 13 ->12 -> 14 -> 20 -> 18 -> 25 。
**測試結果:**
![image-20200302003244874](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/11.png)
##### 2.2.中序遍歷
實現思路:與先序遍歷原理相同,只不過是遍歷的順序不一樣了。
* 首先,遍歷其左子樹;
* 然後,遍歷根(父)節點;
* 最後,遍歷其右子樹;
**程式碼實現:**
``` //2.中序遍歷
//中序遍歷
BinarySearchTree.prototype.midOrderTraversal = function(handler){
this.midOrderTraversalNode(this.root, handler)
}
BinarySearchTree.prototype.midOrderTraversalNode = function(node, handler){
if (node != null) {
//1.遍歷左子樹中的節點
this.midOrderTraversalNode(node.left, handler)
//2.處理節點
handler(node.key)
//3.遍歷右子樹中的節點
this.midOrderTraversalNode(node.right, handler)
}
}
```
**過程詳解:**
遍歷的順序應如下圖所示:
![image-20200302112920295](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/12.png)
首先呼叫midOrderTraversal方法,在方法裡再呼叫midOrderTraversalNode方法用於遍歷二叉搜尋樹。先使用遞迴1遍歷左子樹中的節點;然後,處理父節點;最後,遍歷右子樹中的節點。
**測試程式碼:**
```
//測試程式碼
//1.建立BinarySearchTree
let bst = new BinarySearchTree()
//2.插入資料
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(6);
//3.測試中序遍歷
let resultString2 =""
bst.midOrderTraversal(function(key){
resultString2 += key + "->"
})
alert(resultString2)
```
輸出節點的順序應為:3 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15 -> 18 -> 20 -> 25 。
**測試結果:**
![image-20200302112326786](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/13.png)
##### 2.3.後續遍歷
實現思路:與先序遍歷原理相同,只不過是遍歷的順序不一樣了。
* 首先,遍歷其左子樹;
* 然後,遍歷其右子樹;
* 最後,遍歷根(父)節點;
**程式碼實現:**
```
//後序遍歷
BinarySearchTree.prototype.postOrderTraversal = function(handler){
this.postOrderTraversalNode(this.root, handler)
}
BinarySearchTree.prototype.postOrderTraversalNode = function(node, handler){
if (node != null) {
//1.遍歷左子樹中的節點
this.postOrderTraversalNode(node.left, handler)
//2.遍歷右子樹中的節點
this.postOrderTraversalNode(node.right, handler)
//3.處理節點
handler(node.key)
}
}
```
**過程詳解:**
遍歷的順序應如下圖所示:
![image-20200302120246366](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/14.png)
首先呼叫postOrderTraversal方法,在方法裡再呼叫postOrderTraversalNode方法用於遍歷二叉搜尋樹。先使用遞迴1遍歷左子樹中的節點;然後,遍歷右子樹中的節點;最後,處理父節點。
**測試程式碼:**
```javascript
//測試程式碼
//1.建立BinarySearchTree
let bst = new BinarySearchTree()
//2.插入資料
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(6);
//3.測試後序遍歷
let resultString3 =""
bst.postOrderTraversal(function(key){
resultString3 += key + "->"
})
alert(resultString3)
```
輸出節點的順序應為:3 -> 6 -> 5 -> 8 -> 10 -> 9 -> 7 -> 12 -> 14 -> 13 -> 18 -> 25 -> 20 -> 15 -> 11 。
**測試結果:**
![image-20200302115446608](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/15.png)
**總結:**以遍歷根(父)節點的順序來區分三種遍歷方式。比如:先序遍歷先遍歷根節點、中序遍歷第二遍歷根節點、後續遍歷最後遍歷根節點。
#### 3.查詢資料
##### 3.1.查詢最大值&最小值
在二叉搜尋樹中查詢最值非常簡單,最小值在二叉搜尋樹的最左邊,最大值在二叉搜尋樹的最右邊。只需要一直向左/右查詢就能得到最值,如下圖所示:
![image-20200302125521501](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/16.png)
**程式碼實現:**
```javascript
//尋找最大值
BinarySearchTree.prototype.max = function () {
//1.獲取根節點
let node = this.root
//2.定義key儲存節點值
let key = null
//3.依次向右不斷查詢,直到節點為null
while (node != null) {
key = node.key
node = node.right
}
return key
}
//尋找最小值
BinarySearchTree.prototype.min = function(){
//1.獲取根節點
let node = this.root
//2.定義key儲存節點值
let key = null
//3.依次向左不斷查詢,直到節點為null
while (node != null) {
key = node.key
node = node.left
}
return key
}
```
**測試程式碼:**
```javascript
//測試程式碼
//1.建立BinarySearchTree
let bst = new BinarySearchTree()
//2.插入資料
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(6);
//4.測試最值
console.log(bst.max());
console.log(bst.min());
```
**測試結果:**
![image-20200302133028801](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/17.png)
##### 3.2.查詢特定值
查詢二叉搜尋樹當中的特定值效率也非常高。只需要從根節點開始將需要查詢節點的key值與之比較,若**node.key < root**則向左查詢,若**node.key > root**就向右查詢,直到找到或查詢到null為止。這裡可以使用遞迴實現,也可以採用迴圈來實現。
**實現程式碼:**
```javascript
//查詢特定的key
BinarySearchTree.prototype.search = function(key){
//1.獲取根節點
let node = this.root
//2.迴圈搜尋key
while(node != null){
if (key < node.key) {
//小於根(父)節點就往左邊找
node = node.left
//大於根(父)節點就往右邊找
}else if(key > node.key){
node = node.right
}else{
return true
}
}
return false
}
```
**測試程式碼:**
```javascript
//測試程式碼
//1.建立BinarySearchTree
let bst = new BinarySearchTree()
//2.插入資料
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(6);
//3.測試搜尋方法
console.log(bst.search(24));//false
console.log(bst.search(13));//true
console.log(bst.search(2));//false
```
**測試結果:**
![image-20200302141031370](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/18.png)
#### 4.刪除資料
**實現思路:**
**第一步:**先找到需要刪除的節點,若沒找到,則不需要刪除;
首先定義變數current用於儲存需要刪除的節點、變數parent用於儲存它的父節點、變數isLeftChild儲存current是否為parent的左節點,這樣方便之後刪除節點時改變相關節點的指向。
**實現程式碼:**
```javascript
//1.1.定義變數
let current = this.root
let parent = null
let isLeftChild = true
//1.2.開始尋找刪除的節點
while (current.key != key) {
parent = current
// 小於則往左查詢
if (key < current.key) {
isLeftChild = true
current = current.left
} else{
isLeftChild = false
current = current.rigth
}
//找到最後依然沒有找到相等的節點
if (current == null) {
return false
}
}
//結束while迴圈後:current.key = key
```
**第二步:**刪除找到的指定節點,後分3種情況:
* 刪除葉子節點;
* 刪除只有一個子節點的節點;
* 刪除有兩個子節點的節點;
##### 4.1.情況1:沒有子節點
沒有子節點時也有兩種情況:
當該葉子節點為根節點時,如下圖所示,此時**current == this.root**,直接通過:**this.root = null**,刪除根節點。
![image-20200302154316749](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/19.png)
當該葉子節點不為根節點時也有兩種情況,如下圖所示:
![image-20200302154019653](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/20.png)
若current = 8,可以通過:parent.left = null,刪除節點8;
若current = 10,可以通過:parent.right = null,刪除節點10;
**程式碼實現:**
```javas
//情況1:刪除的是葉子節點(沒有子節點)
if (current.left == null && current.right ==null) {
if (current == this.root) {
this.root = null
}else if(isLeftChild){
parent.left = null
}else {
parent.right =null
}
}
```
##### 4.2.情況2:有一個子節點
有六種情況分別是:
當current存在左子節點時(current.right == null):
* 情況1:current為根節點(current == this.root),如節點11,此時通過:this.root = current.left,刪除根節點11;
* 情況2:current為父節點parent的左子節點(isLeftChild == true),如節點5,此時通過:parent.left = current.left,刪除節點5;
* 情況3:current為父節點parent的右子節點(isLeftChild == false),如節點9,此時通過:parent.right = current.left,刪除節點9;
![image-20200302172806401](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/21.png)
當current存在右子節點時(current.left = null):
* 情況4:current為根節點(current == this.root),如節點11,此時通過:this.root = current.right,刪除根節點11。
* 情況5:current為父節點parent的左子節點(isLeftChild == true),如節點5,此時通過:parent.left = current.right,刪除節點5;
* 情況6:current為父節點parent的右子節點(isLeftChild == false),如節點9,此時通過:parent.right = current.right,刪除節點9;
![image-20200302172527722](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/22.png)
**實現程式碼:**
```javascript
//情況2:刪除的節點有一個子節點
//當current存在左子節點時
else if(current.right == null){
if (current == this.root) {
this.root = current.left
} else if(isLeftChild) {
parent.left = current.left
} else{
parent.right = current.left
}
//當current存在右子節點時
} else if(current.left == null){
if (current == this.root) {
this.root = current.rigth
} else if(isLeftChild) {
parent.left = current.right
} else{
parent.right = current.right
}
}
```
##### 4.3.情況3:有兩個子節點
這種情況**十分複雜**,首先依據以下二叉搜尋樹,討論這樣的問題:
![image-20200302181849832](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/23.png)
**刪除節點9**
在保證刪除節點9後原二叉樹仍為二叉搜尋樹的前提下,有兩種方式:
* 方式1:從節點9的左子樹中選擇一合適的節點替代節點9,可知節點8符合要求;
* 方式2:從節點9的右子樹中選擇一合適的節點替代節點9,可知節點10符合要求;
![image-20200302190601622](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/24.png)
**刪除節點7**
在保證刪除節點7後原二叉樹仍為二叉搜尋樹的前提下,也有兩種方式:
* 方式1:從節點7的左子樹中選擇一合適的節點替代節點7,可知節點5符合要求;
* 方式2:從節點7的右子樹中選擇一合適的節點替代節點7,可知節點8符合要求;
![image-20200302183058326](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/25.png)
**刪除節點15**
在保證刪除節點15後原樹二叉樹仍為二叉搜尋樹的前提下,同樣有兩種方式:
* 方式1:從節點15的左子樹中選擇一合適的節點替代節點15,可知節點14符合要求;
* 方式2:從節點15的右子樹中選擇一合適的節點替代節點15,可知節點18符合要求;
![image-20200302184038470](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/26.png)
相信你已經發現其中的規律了!
**規律總結:**如果要刪除的節點有兩個子節點,甚至子節點還有子節點,這種情況下需要從要刪除節點**下面的子節點中找到一個合適的節點**,來替換當前的節點。
若用current表示需要刪除的節點,則合適的節點指的是:
* current左子樹中比current**小一點點的節點**,即current**左子樹**中的**最大值**;
* current右子樹中比current**大一點點的節點**,即current**右子樹**中的**最小值**;
**前驅&後繼**
在二叉搜尋樹中,這兩個特殊的節點有特殊的名字:
* 比current小一點點的節點,稱為current節點的**前驅**。比如下圖中的節點5就是節點7的前驅;
* 比current大一點點的節點,稱為current節點的**後繼**。比如下圖中的節點8就是節點7的後繼;
![image-20200302210841453](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/27.png)
**程式碼實現:**
* 查詢需要被刪除的節點current的後繼時,需要在current的**右子樹**中查詢**最小值**,即在current的**右子樹**中一直**向左遍歷**查詢;
* 查詢前驅時,則需要在current的**左子樹**中查詢**最大值**,即在current的**左子樹**中一直**向右**遍歷查詢。
下面只討論查詢current後繼的情況,查詢前驅的原理相同,這裡暫不討論。
##### 4.4.完整實現
```
//刪除節點
BinarySearchTree.prototype.remove = function(key){
/*------------------------------1.尋找要刪除的節點---------------------------------*/
//1.1.定義變數current儲存刪除的節點,parent儲存它的父節點。isLeftChild儲存current是否為parent的左節點
let current = this.root
let parent = null
let isLeftChild = true
//1.2.開始尋找刪除的節點
while (current.key != key) {
parent = current
// 小於則往左查詢
if (key < current.key) {
isLeftChild = true
current = current.left
} else{
isLeftChild = false
current = current.right
}
//找到最後依然沒有找到相等的節點
if (current == null) {
return false
}
}
//結束while迴圈後:current.key = key
/*------------------------------2.根據對應情況刪除節點------------------------------*/
//情況1:刪除的是葉子節點(沒有子節點)
if (current.left == null && current.right ==null) {
if (current == this.root) {
this.root = null
}else if(isLeftChild){
parent.left = null
}else {
parent.right =null
}
}
//情況2:刪除的節點有一個子節點
//當current存在左子節點時
else if(current.right == null){
if (current == this.root) {
this.root = current.left
} else if(isLeftChild) {
parent.left = current.left
} else{
parent.right = current.left
}
//當current存在右子節點時
} else if(current.left == null){
if (current == this.root) {
this.root = current.right
} else if(isLeftChild) {
parent.left = current.right
} else{
parent.right = current.right
}
}
//情況3:刪除的節點有兩個子節點
else{
//1.獲取後繼節點
let successor = this.getSuccessor(current)
//2.判斷是否根節點
if (current == this.root) {
this.root = successor
}else if (isLeftChild){
parent.left = successor
}else{
parent.right = successor
}
//3.將後繼的左子節點改為被刪除節點的左子節點
successor.left = current.left
}
}
//封裝查詢後繼的方法
BinarySearchTree.prototype.getSuccessor = function(delNode){
//1.定義變數,儲存找到的後繼
let successor = delNode
let current = delNode.right
let successorParent = delNode
//2.迴圈查詢current的右子樹節點
while(current != null){
successorParent = successor
successor = current
current = current.left
}
//3.判斷尋找到的後繼節點是否直接就是刪除節點的right節點
if(successor != delNode.right){
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
```
**測試程式碼:**
```
//測試程式碼
//1.建立BinarySearchTree
let bst = new BinarySearchTree()
//2.插入資料
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
bst.insert(6);
bst.insert(19);
//3.測試刪除程式碼
//刪除沒有子節點的節點
bst.remove(3)
bst.remove(8)
bst.remove(10)
//刪除有一個子節點的節點
bst.remove(5)
bst.remove(19)
//刪除有兩個子節點的節點
bst.remove(9)
bst.remove(7)
bst.remove(15)
//遍歷二叉搜尋樹並輸出
let resultString = ""
bst.midOrderTraversal(function(key){
resultString += key + "->"
})
alert(resultString)
```
**測試結果:**
![image-20200302225943296](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/28.png)
可見三種情況的節點都被成功刪除了。
#### 5.二叉搜尋樹完整封裝
```
//封裝二叉搜尋樹
function BinarySearchTree(){
//節點內部類
function Node(key){
this.key = key
this.left = null
this.right = null
}
//屬性
this.root = null
//方法
//一.插入資料:insert方法:對外向使用者暴露的方法
BinarySearchTree.prototype.insert = function(key){
//1.根據key建立節點
let newNode = new Node(key)
//2.判斷根節點是否存在
if (this.root == null) {
this.root = newNode
//根節點存在時
}else {
this.insertNode(this.root, newNode)
}
}
//內部使用的insertNode方法:用於比較節點從左邊插入還是右邊插入
BinarySearchTree.prototype.insertNode = function(node, newNode){
//當newNode.key < node.key向左查詢
if(newNode.key < node.key){
//情況1:node無左子節點,直接插入
if (node.left == null) {
node.left = newNode
//情況2:node有左子節點,遞迴呼叫insertNode(),直到遇到無左子節點成功插入newNode後,不再符合該情況,也就不再呼叫insertNode(),遞迴停止。
}else{
this.insertNode(node.left, newNode)
}
//當newNode.key >= node.key向右查詢
}else{
//情況1:node無右子節點,直接插入
if(node.right == null){
node.right = newNode
//情況2:node有右子節點,依然遞迴呼叫insertNode(),直到遇到無右子節點成功插入newNode為止
}else{
this.insertNode(node.right, newNode)
}
}
}
//二.樹的遍歷
//1.先序遍歷
//摻入一個handler函式對得到的key進行處理
BinarySearchTree.prototype.preOrderTraversal = function(handler){
this.preOrderTraversalNode(this.root, handler)
}
//封裝內部方法,對某個節點進行遍歷
BinarySearchTree.prototype.preOrderTraversalNode = function(node,handler){
if (node != null) {
//1.處理經過的節點
handler(node.key)
//2.遍歷經過節點的左子節點
this.preOrderTraversalNode(node.left, handler)
//3.遍歷經過節點的右子節點
this.preOrderTraversalNode(node.right, handler)
}
}
//2.中序遍歷
BinarySearchTree.prototype.midOrderTraversal = function(handler){
this.midOrderTraversalNode(this.root, handler)
}
BinarySearchTree.prototype.midOrderTraversalNode = function(node, handler){
if (node != null) {
//1.遍歷左子樹中的節點
this.midOrderTraversalNode(node.left, handler)
//2.處理節點
handler(node.key)
//3.遍歷右子樹中的節點
this.midOrderTraversalNode(node.right, handler)
}
}
//3.後序遍歷
BinarySearchTree.prototype.postOrderTraversal = function(handler){
this.postOrderTraversalNode(this.root, handler)
}
BinarySearchTree.prototype.postOrderTraversalNode = function(node, handler){
if (node != null) {
//1.遍歷左子樹中的節點
this.postOrderTraversalNode(node.left, handler)
//2.遍歷右子樹中的節點
this.postOrderTraversalNode(node.right, handler)
//3.處理節點
handler(node.key)
}
}
//三.尋找最值
//尋找最大值
BinarySearchTree.prototype.max = function () {
//1.獲取根節點
let node = this.root
//2.定義key儲存節點值
let key = null
//3.依次向右不斷查詢,直到節點為null
while (node != null) {
key = node.key
node = node.right
}
return key
}
//尋找最小值
BinarySearchTree.prototype.min = function(){
//1.獲取根節點
let node = this.root
//2.定義key儲存節點值
let key = null
//3.依次向左不斷查詢,直到節點為null
while (node != null) {
key = node.key
node = node.left
}
return key
}
//查詢特定的key
BinarySearchTree.prototype.search = function(key){
//1.獲取根節點
let node = this.root
//2.迴圈搜尋key
while(node != null){
if (key < node.key) {
//小於根(父)節點就往左邊找
node = node.left
//大於根(父)節點就往右邊找
}else if(key > node.key){
node = node.right
}else{
return true
}
}
return false
}
//四.刪除節點
BinarySearchTree.prototype.remove = function(key){
/*------------------------------1.尋找要刪除的節點---------------------------------*/
//1.1.定義變數current儲存刪除的節點,parent儲存它的父節點。isLeftChild儲存current是否為parent的左節點
let current = this.root
let parent = null
let isLeftChild = true
//1.2.開始尋找刪除的節點
while (current.key != key) {
parent = current
// 小於則往左查詢
if (key < current.key) {
isLeftChild = true
current = current.left
} else{
isLeftChild = false
current = current.right
}
//找到最後依然沒有找到相等的節點
if (current == null) {
return false
}
}
//結束while迴圈後:current.key = key
/*------------------------------2.根據對應情況刪除節點------------------------------*/
//情況1:刪除的是葉子節點(沒有子節點)
if (current.left == null && current.right ==null) {
if (current == this.root) {
this.root = null
}else if(isLeftChild){
parent.left = null
}else {
parent.right =null
}
}
//情況2:刪除的節點有一個子節點
//當current存在左子節點時
else if(current.right == null){
if (current == this.root) {
this.root = current.left
} else if(isLeftChild) {
parent.left = current.left
} else{
parent.right = current.left
}
//當current存在右子節點時
} else if(current.left == null){
if (current == this.root) {
this.root = current.right
} else if(isLeftChild) {
parent.left = current.right
} else{
parent.right = current.right
}
}
//情況3:刪除的節點有兩個子節點
else{
//1.獲取後繼節點
let successor = this.getSuccessor(current)
//2.判斷是否根節點
if (current == this.root) {
this.root = successor
}else if (isLeftChild){
parent.left = successor
}else{
parent.right = successor
}
//3.將後繼的左子節點改為被刪除節點的左子節點
successor.left = current.left
}
}
//封裝查詢後繼的方法
BinarySearchTree.prototype.getSuccessor = function(delNode){
//1.定義變數,儲存找到的後繼
let successor = delNode
let current = delNode.right
let successorParent = delNode
//2.迴圈查詢current的右子樹節點
while(current != null){
successorParent = successor
successor = current
current = current.left
}
//3.判斷尋找到的後繼節點是否直接就是刪除節點的right節點
if(successor != delNode.right){
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
}
```
### 二、平衡樹
**二叉搜尋樹的缺陷:**
當插入的資料是有序的資料,就會造成二叉搜尋樹的深度過大。比如原二叉搜尋樹右 11 7 15 組成,如下圖所示:
![image-20200302231503639](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/29.png)
當插入一組有序資料:6 5 4 3 2就會變成深度過大的搜尋二叉樹,會嚴重影響二叉搜尋樹的效能。
![image-20200302231745864](https://gitee.com/ahuntsun/BlogImgs/raw/master/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/%E6%A0%91%E4%BA%8C/30.png)
**非平衡樹**
* 比較好的二叉搜尋樹,它的資料應該是**左右均勻分佈**的;
* 但是插入**連續資料**後,二叉搜尋樹中的資料分佈就變得**不均勻**了,我們稱這種樹為**非平衡樹**;
* 對於一棵**平衡二叉樹**來說,插入/查詢等操作的效率是**O(logN)**;
* 而對於一棵**非平衡二叉樹**來說,相當於編寫了一個連結串列,查詢效率變成了**O(N)**;
**樹的平衡性**
為了能以**較快的時間O(logN)**來操作一棵樹,我們需要**保證樹總是平衡**的:
* 起碼大部分是平衡的,此時的時間複雜度也是接近O(logN)的;
* 這就要求樹中**每個節點左邊的子孫節點**的個數,應該儘可能地等於**右邊的子孫節點**的個數;
**常見的平衡樹**
* **AVL樹**:是最早的一種平衡樹,它通過在每個節點多儲存一個額外的資料來保持樹的平衡。由於AVL樹是平衡樹,所以它的時間複雜度也是O(logN)。但是它的整體效率不如紅黑樹,開發中比較少用。
* **紅黑樹**:同樣通過**一些特性**來保持樹的平衡,時間複雜度也是O(logN)。進行插入/刪除等操作時,效能優於AVL樹,所以平衡樹的應用基本都是紅黑樹。
> 參考資料:[JavaScript資料結構與演算法](https://www.bilibili.com/video/av86801505?from=search&seid=496776141191