前一篇文章我們學會了第一個非順序數據結構hashMap,那么這一篇我們來學學樹,包括樹的概念和一些相關的術語以及二叉搜索樹的實現。唉?為什么不是樹的實現,不是二叉樹的實現。偏偏是二叉搜索樹的實現?嗯,別急。我們一點一點循序漸進。
我們先來了解一下什么是樹。樹是一種非線性數據結構,直觀的看,它是數據元素(在樹中稱為節點)按分支關系組織起來的結構,很像自然界中的樹那樣。在現實生活中,最常見的例子就是家譜或者公司的組織架構圖。就像是這樣:

那么我們還要知道樹的一些相關術語,比較多,大家要仔細閱讀,不然后面就完全懵逼了。我們先來看一下樹這種數據結構的圖示。

這是我在百度上找到的一張圖,還算清晰明了。這就是樹數據結構了。
首先,一個樹結構,存在一系列的父子關系,除了頂部的第一個節點以外,每一個結點都有一個父節點以及零個或多個子節點。位於樹頂部的節點叫做根節點。看上圖,根節點就是A。樹中的每一個元素(A,B,C,D,E,F這些)都叫做節點。節點又分為內部節點和外部節點。至少有一個子節點的節點稱為內部節點(如上圖的A,B,C,E)。沒有子節點的節點稱為外部節點或葉節點(如上圖的D,F,G)。
另一個概念是子樹,定義是這樣的:子樹由節點和它的后代構成。也就是說,把樹中的一部分剖離出來,它仍舊可以看作是是一顆單獨的樹,那么就可以稱之為子樹。
節點還有一個屬性,叫做度,也可以叫做深度,節點的深度取決於它有多少個祖先節點。如上圖的H,深度就是3,因為它有E,B,A三個祖先節點。E的深度就是2。
除了節點的深度,一棵樹還可以被分解層級。根節點是第0層,根節點的子節點是第1層。以此類推。
那么我們對樹的概念有了簡單的了解,那么什么是二叉樹呢?其實不論是二叉樹,還是二叉搜索樹,又或者是其它什么樹,只不過是在樹的基礎上加上一個限制條件以便更高效率的操作。
在二叉樹中,一個節點的子節點最多只能有兩個節點,一個左節點,一個右節點,二叉樹只能是左右分叉的,所以叫做二叉樹。
那二叉搜索樹(BST)呢?不過是在二叉樹的基礎上,又加了一個插入元素的條件,就是,只允許你在左側節點存儲比父節點小的值,在右側節點存儲大於或等於父節點的值。這里要注意的一點是,二叉樹的子節點最多只能有兩個節點,也就說不一定非要有兩個節點,只有一個左節點,或者只有一個右節點都是可以的可能的允許的。
那么似乎我們不去實現樹,也不去實現二叉樹,而是直接實現二叉搜索樹的原因就出來了。只要我們學會了二叉搜索樹,自然樹和二叉樹的實現也就會了。
來,我們來看圖說話,在開始實現二叉搜索樹之前,先給大家放張圖(圖片百度的),以便大家更好的理解。

既然圖有了,我們就來看看如何實現一個BinarySearchTree。首先,要告訴大家的是,在鏈表中,我們稱每一個節點本身稱作節點,但是在樹中,我們叫它鍵。唉?我好像看到了鏈表?樹跟鏈表有毛關系?嗯。。。確實沒關系,但是我們要實現樹的方式卻跟鏈表有關系。我們之前學習過雙向鏈表,雙向鏈表中有prev和next,分別指向當前節點的上一個和下一個節點。樹的實現我們也要借用類似的方式,只不過是一個指向左側子節點,另一個指向右側子節點。
那么我們都要實現哪些方法呢?
1、insert(key):像樹中插入一個新的鍵。
2、search(key):在樹中查找一個鍵。
3、inOrderTraverse:中序遍歷。
4、preOrderTraverse:先序遍歷。
5、postOrderTraverse:后序遍歷。
6、min:返回樹中最小的值/鍵。
7、max:返回樹中最大的值/鍵。
8、remove(key):從樹中移除某個鍵。
我們知道了基本的實現方式和BinarySearchTree需要的方法。我們開始吧。
// 這個二叉搜索樹的實現根本其實並沒有多復雜,復雜的其實是概念。 // 但是,我會盡量給大家解釋清楚。不至於讓大家一臉懵逼。 function BinarySearchTree() { // 首先這里,聲明一個node節點,也就是我們樹結構中所代表的每一個節點,包括根節點在內。 var Node = function(key) { // 節點的key,也就是鍵,大家要記住一個事情,我們所有的數據結構,都是為了應對合理適當的場景。 // 而無論何種數據結構,都需要檢索,我記得前面說過,也就是增刪改查這種萬年不變的操作。 // 而這里的鍵(也就是key),是為了依照一定的規則來設置鍵,以便我們更快速的檢索到,提取其值。 // 當然,我們實現的這個二叉搜索樹貌似並沒有value,但是我們可以自己去設置一個鍵值對的映射關系。 // 既然能檢索到key,也就可以找到其對應的值。當然,這里就不都說了。 // 比如說這里,我們就可以給Node私有構造函數加一個this.value = value。來形成一個映射關系。 this.key = key; // left和right,也就是指向當前節點的左右子節點的指針。 this.left = null; this.right = null; }; //初始化一個二叉搜索樹,聲明一個私有變量root代表根節點。 var root = null; // 這是插入節點的私有屬性,我們會在insert方法中直接調用。那么我們先去看insert方法。 // 其實這里也不復雜,但是用到了遞歸,如果大家對遞歸不太了解,可以去百度搜一下。后面的文章我也會寫一些算法的相關內容。 // 我們回到這里,insertNode有兩個參數,在insert方法調用的時候我們傳入了root,newNode。以便我們從根節點去查找。 var insertNode = function (node,newNode) { // 這里就分為了兩種情況,其實后面的方法也是這樣,新插入的key和node(第一次執行的時候是root)相對比。 // 如果新插入的key小於node的key,我們要插入到left里,如果是大於等於node的key,就插入到right。這是我們二叉搜索樹的規則。 if(newNode.key < node.key) { // 那么這里,如果(或者說是‘直到’)node.left是空,也就是沒有元素,那么就插入到node.left中。 // 否則,再調用一下這個函數自身(也就是遞歸了,這就是為什么上面也可以說是‘直到’,遞歸必須有遞歸終止的條件,不然會陷入死循環)。 // 那么下面的else情況也是同理。 if (node.left === null) { node.left = newNode; } else { insertNode(node.left,newNode); } } else { if (node.right === null) { node.right = newNode; } else { insertNode(node.right,newNode); } } } // 中序遍歷,首先需要說一下什么是中序遍歷。 // 中序遍歷是一種以上行順序訪問BST所有節點的遍歷方式(也就是從小到大的順序訪問所有節點),BST就是binary search tree。 // 那么該方法有兩個參數,一個是node,一個是回調函數(這個回調函數,在本文的應用是下面的console每一個節點的值,當然,你也可以用回調函數做一些羞羞的事)。 var inOrderTraverseNode = function (node,callback) { // 我們要遞歸使用該方法,前面說了,必須有一個終止回調的條件。這里就是如果節點為空,我們就認為元素遍歷完成,停止遞歸。 if(node !== null) { // 這里,遞歸調用相同的函數來訪問左側子節點,然后對這個節點進行一些操作,最后訪問右側子節點。 // 到這里,其實中序遍歷可以說是,左(左側子節點),中(該節點),右(右側子節點)的訪問方式。 inOrderTraverseNode(node.left,callback); callback(node.key); inOrderTraverseNode(node.right,callback); } } // 先序遍歷,其實我們看代碼就可以知道了,先序遍歷就是中,左,右。也就是先訪問節點本身,再訪問左側然后是右側子節點。 var preOrderTraverseNode = function (node,callback) { if(node !== null) { callback(node.key); preOrderTraverseNode(node.left,callback); preOrderTraverseNode(node.right,callback); } } // 那么后序遍歷呢?額......可想而知,也就是左右中的方式,先訪問節點的后代節點,再訪問節點本身。 var postOrderTraverseNode = function (node,callback) { if(node !== null) { postOrderTraverseNode(node.left,callback); postOrderTraverseNode(node.right,callback); callback(node.key); } } // 搜索樹中的最小值,嗯......我們根據前面了解的內容,猜猜看最小的值是哪一個?如果你說不知道,請從頭再來! // 樹中最小的值,就是在樹的最底層最左側的節點。那么最大值就是右側的節點了。 // 為什么會這樣呢?如果你還是不知道。請從頭再......再來! var minNode = function (node) { // 如果該節點是否是合法值,是->繼續,不是,返回null。 if(node) { // 這里就是循環判斷node.left是否存在,知道不存在的時候(說明已經得到最左側的子節點了)就直接返回上一次賦值的node.key。 while(node && node.left !== null) { node = node.left; } return node.key; } return null; } // 同上 var maxNode = function (node) { if(node) { while(node && node.right !== null) { node = node.right; } return node.key; } return null; } // 其實這里,我真的不想說......但是我還是要'磨嘰'一下...... // 這里第一個的node參數,在search中傳入的是root,因為要從root開始執行邏輯。 // 還有,后面就幾乎所有的傳入node參數的私有方法,傳入的都是root,因為要從root開始。 var searchNode = function (node,key) { // 如果是null了,返回false if(node === null) { return false; } // 這里,其實也就是根據不同的值得大小來判斷遞歸時所需要傳入的參數是什么,如果即不大也不小。bingo,說明找到了。 if(key < node.key) { return searchNode(node.left,key); } else if(key > node.key) { return searchNode(node.right,key); } else { return true; } } // 這個方法稍微復雜並且有意思一點,我們詳細來說說。 var removeNode = function (node,key) { // 這個判斷沒什么好說的了,如果是null說明在樹中沒有這個鍵,直接返回null就可以了。 if(node === null) { return null; } // 那么這里會有三種情況的判斷,如果要找的key小於當前的node.key,就遞歸調用函數,沿着樹的左邊一直找下去。 // 那么如果要找的key大於當前的node.key,就沿着樹的右邊一直找下去。直到找到為止。 if(key < node.key) { node.left = removeNode(node.left,key); return node; } else if(key > node.key) { node.right = removeNode(node.right,key); return node; // 這里就是找到了匹配的key的時候所處理的邏輯了 } else { // 第一種情況就是該節點是沒有左右子節點的,我們直接賦值null來移除就可以了。 // 雖然該節點沒有子節點,但是有一個父節點,我們需要通過返回null,來使對應的父節點的指針指向null。 // 要記得我們在remove方法中有一個root = removeNode(root,key);賦值語句,可以到下面查看。 // 就是為了讓我們父節點接收到更改的指針。 if(node.left === null && node.right === null) { node = null; return node; } // 這里是第二種情況,移除有一個左側節點或者右側節點的節點。 // 我們只要跳過這個節點,直接將父節點的指針指向子節點就可以了。 if(node.left === null) { node = node.right; return node; } else if(node.right === null) { node = node.left; return node; } // 最后是第三種情況,稍微復雜些,其實也就是我們要做的操作多一些。 // 首先,我們在找到了需要移除的節點后,需要找到它右邊子樹種最小的節點。(要移除節點的繼承者,也就是說在移除了匹配的節點后,這個值會替換被移除的節點) // 這里findMinNode跟min方法是一樣的,只不過返回值稍有不同 var aux = findMinNode(node.right); // 然后用aux去更新要移除節點的值,這個時候,我們已經改變了要移除節點的值,也就相應的移除了該節點。 node.key = aux.key; // 但是這個時候就有兩個相同的鍵了,所以我們要移除aux也就是node.right指向的節點。 node.right = removeNode(node.right,aux.key); // 返回更新后的引用。 return node; // 最后,要提醒大家一個需要注意的地方,移除一個樹中的節點,並沒有移除該節點下的所有子樹或者子節點,這是一個比較容易讓人迷惑的誤區。 // 比如說,我有一棵下面這樣的樹 /* A B C D E F G */ // 我想要移除C,並沒有把F,G也同時移除,只是單純的移除了C這個節點,所以我們需要依照二叉搜索樹的規則,找到一個合理的值代替這個位置(也就是F)。 // 那么我們用F替換C,並把C移除,更改對應的指針。也就完成了第三種情況的移除操作。 } } var findMinNode = function (node) { while(node && node.left !== null) { node = node.left; } return node; } // 其實這個方法很簡單,一看就明白了。 // 如果root是null,說明是一個空樹,我們直接讓newNode為root就可以了,如果不是,我們再調用insertNode那個私有方法。 this.insert = function (key) { var newNode = new Node(key); if(root === null) { root = newNode; } else { insertNode(root,newNode); } } this.inOrderTraverse = function (callback) { inOrderTraverseNode(root,callback); } this.preOrderTraverse = function (callback) { preOrderTraverseNode(root,callback); } this.postOrderTraverse = function (callback) { postOrderTraverseNode(root,callback); } this.min = function () { return minNode(root); } this.max = function () { return maxNode(root); } this.search = function (key) { return searchNode(root,key); } this.remove = function (key) { root = removeNode(root,key); } } //這個就是callback了 function printNode (value) { console.log(value); } var tree = new BinarySearchTree(); tree.insert(11); tree.insert(7); tree.insert(15); tree.insert(5); tree.insert(3); tree.insert(9); tree.insert(8); tree.insert(10); tree.insert(13); tree.insert(12); tree.insert(14); tree.insert(20); tree.insert(18); tree.insert(25); tree.insert(6); tree.inOrderTraverse(printNode);//3,5,6,7,8,9,10,11,12,13,14,15,18,20,25 tree.remove(15); console.log("--------------") tree.inOrderTraverse(printNode);//3,5,6,7,8,9,10,11,12,13,14,18,20,25 tree.insert(100); console.log("--------------"); tree.inOrderTraverse(printNode);//3,5,6,7,8,9,10,11,12,13,14,18,20,25,100 console.log(tree.min(),"min");//3,“min” console.log(tree.max(),"max");//100,"max" console.log(tree.search(66))//false console.log(tree.search(8))//true console.log("--------------"); tree.preOrderTraverse(printNode);//11,7,5,3,6,9,8,10,18,13,12,14,20,25,100 console.log("--------------"); tree.postOrderTraverse(printNode);//3,6,5,8,10,9,7,12,14,13,100,25,20,18,11
那么我們二叉搜索樹就實現完成了。其實如果大家看過前面的文章,這里的BinarySearchTree的實現其實並沒有多復雜,而需要注意的是remove一個節點時在對不同的情況的處理方法。說到底,二叉搜索樹也就是在插入元素也就是節點的時候要按照必要的規則,所以我們在對樹進行操作的時候依照這種規則就可以了。
本來到這里就應該結束了,但是我覺得有必要給大家上幾幅圖片,解釋解釋我們上面代碼中每一步的執行,在BinarySearchTree是如何操作的。

上圖展示了我們依次插入各個數字的時候,二叉搜索樹會根據數值的大小來安排它的位置,之后我們做了一個移除15這個節點的操作,那么在移除之后它看起來就是這樣的:

具體的解釋在代碼的注釋中已經有了,這里就不再重復的去啰嗦了。
好了,到這里我們已經基本完成了二叉搜索樹的基本實現,那么下一篇文章我們會簡單的介紹一下其它類型的樹結構。比如自平衡二叉搜索樹,紅黑樹,堆積樹等。
最后,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!
