1. 程式人生 > 其它 >用JavaScript實現二叉搜尋樹

用JavaScript實現二叉搜尋樹

電腦科學中最常用和討論最多的資料結構之一是二叉搜尋樹。這通常是引入的第一個具有非線性插入演算法的資料結構。二叉搜尋樹類似於雙鏈表,每個節點包含一些資料,以及兩個指向其他節點的指標;它們在這些節點彼此相關聯的方式上有所不同。二叉搜尋樹節點的指標通常被稱為“左”和“右”,用來指示與當前值相關的子樹。這種節點的簡單JavaScript實現如下:

varnode={
value:125,
left:null,
right:null
};

從名稱中可以看出,二叉搜尋樹被組織成分層的樹狀結構。第一個專案成為根節點,每個附加值作為該根的祖先新增到樹中。但是,二叉搜尋樹節點上的值是唯一的,根據它們包含的值進行排序:作為節點左子樹的值總是小於節點的值,右子樹中的值都是大於節點的值。通過這種方式,在二叉搜尋樹中查詢值變得非常簡單,只要你要查詢的值小於正在處理的節點則向左,如果值更大,則向右移動。二叉搜尋樹中不能有重複項,因為重複會破壞這種關係。下圖表示一個簡單的二叉搜尋樹。

上圖表示一個二叉搜尋樹,其根的值為 8。當新增值 3 時,它成為根的左子節點,因為 3 小於 8。當新增值 1 時,它成為 3 的左子節點,因為 1 小於 8(所以向左)然後 1 小於3(再向左)。當新增值 10 時,它成為跟的右子節點,因為 10 大於 8。不斷用此過程繼續處理值 6,4,7,14 和 13。此二叉搜尋樹的深度為 3,表示距離根最遠的節點是三個節點。

二叉搜尋樹以自然排序的順序結束,因此可用於快速查詢資料,因為你可以立即消除每個步驟的可能性。通過限制需要查詢的節點數量,可以更快地進行搜尋。假設你要在上面的樹中找到值 6。從根開始,確定 6 小於 8,因此前往根的左子節點。由於 6 大於 3,因此你將前往右側節點。你就能找到正確的值。所以你只需訪問三個而不是九個節點來查詢這個值。

要在JavaScript中實現二叉搜尋樹,第一步要先定義基本介面:

functionBinarySearchTree(){
this._root=null;
}

BinarySearchTree.prototype={

//restoreconstructor
constructor:BinarySearchTree,

add:function(value){
},

contains:function(value){
},

remove:function(value){
},

size:function(){
},

toArray:function(){
},

toString:function(){
}

};

基本接與其他資料結構類似,有新增和刪除值的方法。我還添加了一些方便的方法,size(),toArray()和toString(),它們對 JavaScript 很有用。

要掌握使用二叉搜尋樹的方法,最好從contains()方法開始。contains()方法接受一個值作為引數,如果值存在於樹中則返回true,否則返回false。此方法遵循基本的二叉搜尋演算法來確定該值是否存在:

BinarySearchTree.prototype={

//morecode

contains:function(value){
varfound=false,
current=this._root

//makesurethere'sanodetosearch
while(!found&&current){

//ifthevalueislessthanthecurrentnode's,goleft
if(value<current.value){
current=current.left;

//ifthevalueisgreaterthanthecurrentnode's,goright
}elseif(value>current.value){
current=current.right;

//valuesareequal,foundit!
}else{
found=true;
}
}

//onlyproceedifthenodewasfound
returnfound;
},

//morecode

};

搜尋從樹的根開始。如果沒有新增資料,則可能沒有根,所以必須要進行檢查。遍歷樹遵循前面討論的簡單演算法:如果要查詢的值小於當前節點則向左移動,如果值更大則向右移動。每次都會覆蓋current指標,直到找到要找的值(在這種情況下found設定為true)或者在那個方向上沒有更多的節點了(在這種情況下,值不在樹上)。

在contains()中使用的方法也可用於在樹中插入新值。主要區別在於你要尋找放置新值的位置,而不是在樹中查詢值:

BinarySearchTree.prototype={

//morecode

add:function(value){
//createanewitemobject,placedatain
varnode={
value:value,
left:null,
right:null
},

//usedtotraversethestructure
current;

//specialcase:noitemsinthetreeyet
if(this._root===null){
this._root=node;
}else{
current=this._root;

while(true){

//ifthenewvalueislessthanthisnode'svalue,goleft
if(value<current.value){

//ifthere'snoleft,thenthenewnodebelongsthere
if(current.left===null){
current.left=node;
break;
}else{
current=current.left;
}

//ifthenewvalueisgreaterthanthisnode'svalue,goright
}elseif(value>current.value){

//ifthere'snoright,thenthenewnodebelongsthere
if(current.right===null){
current.right=node;
break;
}else{
current=current.right;
}

//ifthenewvalueisequaltothecurrentone,justignore
}else{
break;
}
}
}
},

//morecode

};

在二叉搜尋樹中新增值時,特殊情況是在沒有根的情況。在這種情況下,只需將根設定為新值即可輕鬆完成工作。對於其他情況,基本演算法與contains()中使用的基本演算法完全相同:新值小於當前節點向左,如果值更大則向右。主要區別在於,當你無法繼續前進時,這就是新值的位置。所以如果你需要向左移動但沒有左側節點,則新值將成為左側節點(與右側節點相同)。由於不存在重複項,因此如果找到具有相同值的節點,則操作將停止。

在繼續討論size()方法之前,我想深入討論樹遍歷。為了計算二叉搜尋樹的大小,必須要訪問樹中的每個節點。二叉搜尋樹通常會有不同型別的遍歷方法,最常用的是有序遍歷。通過處理左子樹,然後是節點本身,然後是右子樹,在每個節點上執行有序遍歷。由於二叉搜尋樹以這種方式排序,從左到右,結果是節點以正確的排序順序處理。對於size()方法,節點遍歷的順序實際上並不重要,但它對toArray()方法很重要。由於兩種方法都需要執行遍歷,我決定新增一個可以通用的traverse()方法:

BinarySearchTree.prototype={

//morecode

traverse:function(process){

//helperfunction
functioninOrder(node){
if(node){

//traversetheleftsubtree
if(node.left!==null){
inOrder(node.left);
}

//calltheprocessmethodonthisnode
process.call(this,node);

//traversetherightsubtree
if(node.right!==null){
inOrder(node.right);
}
}
}

//startwiththeroot
inOrder(this._root);
},

//morecode

};

此方法接受一個引數process,這是一個應該在樹中的每個節點上執行的函式。該方法定義了一個名為inOrder()的輔助函式用於遞迴遍歷樹。注意,如果當前節點存在,則遞迴僅左右移動(以避免多次處理null)。然後traverse()方法從根節點開始按順序遍歷,process()函式處理每個節點。然後可以使用此方法實現size()、toArray()、toString():

BinarySearchTree.prototype={

//morecode

size:function(){
varlength=0;

this.traverse(function(node){
length++;
});

returnlength;
},

toArray:function(){
varresult=[];

this.traverse(function(node){
result.push(node.value);
});

returnresult;
},

toString:function(){
returnthis.toArray().toString();
},

//morecode

};

size()和toArray()都呼叫traverse()方法並傳入一個函式來在每個節點上執行。在使用size()的情況下,函式只是遞增長度變數,而toArray()使用函式將節點的值新增到陣列中。toString()方法在呼叫toArray()之前把返回的陣列轉換為字串,並返回 。

刪除節點時,你需要確定它是否為根節點。根節點的處理方式與其他節點類似,但明顯的例外是根節點需要在結尾處設定為不同的值。為簡單起見,這將被視為 JavaScript程式碼中的一個特例。

刪除節點的第一步是確定節點是否存在:

BinarySearchTree.prototype={

//morecodehere

remove:function(value){

varfound=false,
parent=null,
current=this._root,
childCount,
replacement,
replacementParent;

//makesurethere'sanodetosearch
while(!found&&current){

//ifthevalueislessthanthecurrentnode's,goleft
if(value<current.value){
parent=current;
current=current.left;

//ifthevalueisgreaterthanthecurrentnode's,goright
}elseif(value>current.value){
parent=current;
current=current.right;

//valuesareequal,foundit!
}else{
found=true;
}
}

//onlyproceedifthenodewasfound
if(found){
//continue
}

},
//morecodehere

};

remove()方法的第一部分是用二叉搜尋定位要被刪除的節點,如果值小於當前節點的話則向左移動,如果值大於當前節點則向右移動。當遍歷時還會跟蹤parent節點,因為你最終需要從其父節點中刪除該節點。當found等於true時,current的值是要刪除的節點。

刪除節點時需要注意三個條件:

葉子節點

只有一個孩子的節點

有兩個孩子的節點

http://www.xihuanfan.com 手機遊戲下載

從二叉搜尋樹中刪除除了葉節點之外的內容意味著必須移動值來對樹正確的排序。前兩個實現起來相對簡單,只刪除了一個葉子節點,刪除了一個帶有一個子節點的節點並用其子節點替換。最後一種情況有點複雜,以便稍後訪問。

在瞭解如何刪除節點之前,你需要知道節點上究竟存在多少個子節點。一旦知道了,你必須確定節點是否為根節點,留下一個相當簡單的決策樹:

BinarySearchTree.prototype={

//morecodehere

remove:function(value){

varfound=false,
parent=null,
current=this._root,
childCount,
replacement,
replacementParent;

//findthenode(removedforspace)

//onlyproceedifthenodewasfound
if(found){

//figureouthowmanychildren
childCount=(current.left!==null?1:0)+
(current.right!==null?1:0);

//specialcase:thevalueisattheroot
if(current===this._root){
switch(childCount){

//nochildren,justerasetheroot
case0:
this._root=null;
break;

//onechild,useoneastheroot
case1:
this._root=(current.right===null?
current.left:current.right);
break;

//twochildren,littleworktodo
case2:

//TODO

//nodefault

}

//non-rootvalues
}else{

switch(childCount){

//nochildren,justremoveitfromtheparent
case0:
//ifthecurrentvalueislessthanits
//parent's,nullouttheleftpointer
if(current.value<parent.value){
parent.left=null;

//ifthecurrentvalueisgreaterthanits
//parent's,nullouttherightpointer
}else{
parent.right=null;
}
break;

//onechild,justreassigntoparent
case1:
//ifthecurrentvalueislessthanits
//parent's,resettheleftpointer
if(current.value<parent.value){
parent.left=(current.left===null?
current.right:current.left);

//ifthecurrentvalueisgreaterthanits
//parent's,resettherightpointer
}else{
parent.right=(current.left===null?
current.right:current.left);
}
break;

//twochildren,abitmorecomplicated
case2:

//TODO

//nodefault

}

}

}

},

//morecodehere

};

處理根節點時,這是一個覆蓋它的簡單過程。對於非根節點,必須根據要刪除的節點的值設定parent上的相應指標:如果刪除的值小於父節點,則left指標必須重置為null(對於沒有子節點的節點)或刪除節點的left指標;如果刪除的值大於父級,則必須將right指標重置為null或刪除的節點的right指標。

如前所述,刪除具有兩個子節點的節點是最複雜的操作。下圖是兒茶搜尋樹的一種表示。

根為 8,左子為 3,如果 3 被刪除會發生什麼?有兩種可能性:1(3 左邊的孩子,稱為有序前身)或4(右子樹的最左邊的孩子,稱為有序繼承者)都可以取代 3。

這兩個選項中的任何一個都是合適的。要查詢有序前驅,即刪除值之前的值,請檢查要刪除的節點的左子樹,並選擇最右側的子節點;找到有序後繼,在刪除值後立即出現的值,反轉程序並檢查最左側的右子樹。其中每個都需要另一次遍歷樹來完成操作:

BinarySearchTree.prototype={

//morecodehere

remove:function(value){

varfound=false,
parent=null,
current=this._root,
childCount,
replacement,
replacementParent;

//findthenode(removedforspace)

//onlyproceedifthenodewasfound
if(found){

//figureouthowmanychildren
childCount=(current.left!==null?1:0)+
(current.right!==null?1:0);

//specialcase:thevalueisattheroot
if(current===this._root){
switch(childCount){

//othercasesremovedtosavespace

//twochildren,littleworktodo
case2:

//newrootwillbetheoldroot'sleftchild
//...maybe
replacement=this._root.left;

//findtheright-mostleafnodetobe
//therealnewroot
while(replacement.right!==null){
replacementParent=replacement;
replacement=replacement.right;
}

//it'snotthefirstnodeontheleft
if(replacementParent!==null){

//removethenewrootfromit's
//previousposition
replacementParent.right=replacement.left;

//givethenewrootalloftheold
//root'schildren
replacement.right=this._root.right;
replacement.left=this._root.left;
}else{

//justassignthechildren
replacement.right=this._root.right;
}

//officiallyassignnewroot
this._root=replacement;

//nodefault

}

//non-rootvalues
}else{

switch(childCount){

//othercasesremovedtosavespace

//twochildren,abitmorecomplicated
case2:

//resetpointersfornewtraversal
replacement=current.left;
replacementParent=current;

//findtheright-mostnode
while(replacement.right!==null){
replacementParent=replacement;
replacement=replacement.right;
}

replacementParent.right=replacement.left;

//assignchildrentothereplacement
replacement.right=current.right;
replacement.left=current.left;

//placethereplacementintherightspot
if(current.value<parent.value){
parent.left=replacement;
}else{
parent.right=replacement;
}

//nodefault

}

}

}

},

//morecodehere

};

具有兩個子節點的根節點和非根節點的程式碼幾乎相同。此實現始終通過檢視左子樹並查詢最右側子節點來查詢有序前驅。遍歷是使用while迴圈中的replacement和replacementParent變數完成的。replacement中的節點最終成為替換current的節點,因此通過將其父級的right指標設定為替換的left指標,將其從當前位置移除。對於根節點,當replacement是根節點的直接子節點時,replacementParent將為null,因此replacement的right指標只是設定為 root 的right指標。最後一步是將替換節點分配到正確的位置。對於根節點,替換設定為新根;對於非根節點,替換被分配到原始parent上的適當位置。

說明:始終用有序前驅替換節點可能導致不平衡樹,其中大多數值會位於樹的一側。不平衡樹意味著搜尋效率較低,因此在實際場景中應該引起關注。在二叉搜尋樹實現中,要確定是用有序前驅還是有序後繼以使樹保持適當平衡(通常稱為自平衡二叉搜尋樹)。