1. 程式人生 > >JavaScript實現樹結構(一)

JavaScript實現樹結構(一)

## JavaScript實現樹結構(一) ### 一、樹結構簡介 #### 1.1.簡單瞭解樹結構 **什麼是樹?** 真實的樹: ![image-20200229205530929](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%B8%80/1.png) **樹的特點:** * 樹一般都有一個**根**,連線著根的是**樹幹**; * 樹幹會發生分叉,形成許多**樹枝**,樹枝會繼續分化成更小的**樹枝**; * 樹枝的最後是**葉子**; 現實生活中很多結構都是樹的抽象,模擬的樹結構相當於旋轉`180°`的樹。 ![image-20200229205630945](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%B8%80/2.png) **樹結構對比於陣列/連結串列/雜湊表有哪些優勢呢:** **陣列:** * 優點:可以通過**下標值訪問**,效率高; * 缺點:查詢資料時需要先對資料進行**排序**,生成**有序陣列**,才能提高查詢效率;並且在插入和刪除元素時,需要大量的**位移操作**; **連結串列:** * 優點:資料的插入和刪除操作效率都很高; * 缺點:**查詢**效率低,需要從頭開始依次查詢,直到找到目標資料為止;當需要在連結串列中間位置插入或刪除資料時,插入或刪除的效率都不高。 **雜湊表:** * 優點:雜湊表的插入/查詢/刪除效率都非常高; * 缺點:**空間利用率不高**,底層使用的陣列中很多單元沒有被利用;並且雜湊表中的元素是**無序**的,不能按照固定順序遍歷雜湊表中的元素;而且不能快速找出雜湊表中**最大值或最小值**這些特殊值。 **樹結構:** 優點:樹結構綜合了上述三種結構的優點,同時也彌補了它們存在的缺點(雖然效率不一定都比它們高),比如樹結構中資料都是有序的,查詢效率高;空間利用率高;並且可以快速獲取最大值和最小值等。 總的來說:**每種資料結構都有自己特定的應用場景** **樹結構:** * **樹(Tree)**:由 n(n ≥ 0)個節點構成的**有限集合**。當 n = 0 時,稱為**空樹**。 對於任一棵非空樹(n > 0),它具備以下性質: * 數中有一個稱為**根(Root)**的特殊節點,用 **r **表示; * 其餘節點可分為 m(m > 0)個互不相交的有限集合 T~1~,T~2~,...,T~m~,其中每個集合本身又是一棵樹,稱為原來樹的**子樹(SubTree)**。 **樹的常用術語:** ![image-20200229221126468](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%B8%80/3.png) * **節點的度(Degree)**:節點的**子樹個數**,比如節點B的度為2; * **樹的度**:樹的所有節點中**最大的度數**,如上圖樹的度為2; * **葉節點(Leaf)**:**度為0的節點**(也稱為葉子節點),如上圖的H,I等; * **父節點(Parent)**:度不為0的節點稱為父節點,如上圖節點B是節點D和E的父節點; * **子節點(Child)**:若B是D的父節點,那麼D就是B的子節點; * **兄弟節點(Sibling)**:具有同一父節點的各節點彼此是兄弟節點,比如上圖的B和C,D和E互為兄弟節點; * **路徑和路徑長度**:路徑指的是一個節點到另一節點的通道,路徑所包含邊的個數稱為路徑長度,比如A->H的路徑長度為3; * **節點的層次(Level)**:規定**根節點在1層**,其他任一節點的層數是其父節點的**層數加1**。如B和C節點的層次為2; * **樹的深度(Depth)**:樹種所有節點中的**最大層次**是這棵樹的深度,如上圖樹的深度為4; #### 1.2.樹結構的表示方式 * **最普通的表示方法**: ![image-20200229230417613](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%B8%80/4.png) 如圖,樹結構的組成方式類似於連結串列,都是由一個個節點連線構成。不過,根據每個父節點子節點數量的不同,每一個父節點需要的引用數量也不同。比如節點A需要3個引用,分別指向子節點B,C,D;B節點需要2個引用,分別指向子節點E和F;K節點由於沒有子節點,所以不需要引用。 這種方法缺點在於我們無法確定某一結點的引用數。 * **兒子-兄弟表示法**: ![image-20200229232805477](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%B8%80/5.png) 這種表示方法可以完整地記錄每個節點的資料,比如: ``` //節點A Node{ //儲存資料 this.data = data //統一隻記錄左邊的子節點 this.leftChild = B //統一隻記錄右邊的第一個兄弟節點 this.rightSibling = null } //節點B Node{ this.data = data this.leftChild = E this.rightSibling = C } //節點F Node{ this.data = data this.leftChild = null this.rightSibling = null } ``` 這種表示法的優點在於每一個節點中引用的數量都是確定的。 * **兒子-兄弟表示法旋轉** 以下為兒子-兄弟表示法組成的樹結構: ![image-20200229234549049](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%B8%80/6.png) 將其順時針旋轉45°之後: ![image-20200229235549522](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%B8%80/7.png) 這樣就成為了一棵**二叉樹**,由此我們可以得出結論:**任何樹都可以通過二叉樹進行模擬**。但是這樣父節點不是變了嗎?其實,父節點的設定只是為了方便指向子節點,在程式碼實現中誰是父節點並沒有關係,只要能正確找到對應節點即可。 ### 二、二叉樹 #### 2.1.二叉樹簡介 **二叉樹的概念**:如果樹中的每一個節點最多隻能由**兩個子節點**,這樣的樹就稱為**二叉樹**; 二叉樹十分重要,不僅僅是因為簡單,更是因為幾乎所有的樹都可以表示成二叉樹形式。 **二叉樹的組成**: * 二叉樹可以為空,也就是沒有節點; * 若二叉樹不為空,則它由根節點和稱為其左子樹TL和右子樹TR的兩個不相交的二叉樹組成; **二叉樹的五種形態**: ![image-20200301001718079](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%B8%80/8.png) 上圖分別表示:空的二叉樹、只有一個節點的二叉樹、只有左子樹TL的二叉樹、只有右子樹TR的二叉樹和有左右兩個子樹的二叉樹。 **二叉樹的特性**: * 一個二叉樹的第 i 層的最大節點樹為:2^(i-1)^,i >= 1; * 深度為k的二叉樹的最大節點總數為:2^k^ - 1 ,k >= 1; * 對任何非空二叉樹,若 n~0~ 表示葉子節點的個數,n~2~表示度為2的非葉子節點個數,那麼兩者滿足關係:n~0~ = n~2~ + 1;如下圖所示:H,E,I,J,G為葉子節點,總數為5;A,B,C,F為度為2的非葉子節點,總數為4;滿足n~0~ = n~2~ + 1的規律。 ![image-20200301092140211](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%B8%80/9.png) #### 2.2.特殊的二叉樹 **完美二叉樹** 完美二叉樹(Perfect Binary Tree)也成為滿二叉樹(Full Binary Tree),在二叉樹中,除了最下一層的葉子節點外,每層節點都有2個子節點,這就構成了完美二叉樹。 ![image-20200301093237681](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%B8%80/10.png) **完全二叉樹** 完全二叉樹(Complete Binary Tree): * 除了二叉樹最後一層外,其他各層的節點數都達到了最大值; * 並且,最後一層的葉子節點從左向右是連續存在,只缺失右側若干葉子節點; * 完美二叉樹是特殊的完全二叉樹; ![image-20200301093659373](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%B8%80/11.png) 在上圖中,由於H缺失了右子節點,所以它不是完全二叉樹。 #### 2.3.二叉樹的資料儲存 常見的二叉樹儲存方式為**陣列**和**連結串列**: **使用陣列:** * **完全二叉樹**:按從上到下,從左到右的方式儲存資料。 ![image-20200301094919588](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%B8%80/12.png) | 節點 | A | B | C | D | E | F | G | H | | -------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | | **序號** | **1** | **2** | **3** | **4** | **5** | **6** | **7** | **8** | 使用陣列儲存時,取資料的時候也十分方便:左子節點的序號等於父節點序號 * 2,右子節點的序號等於父節點序號 * 2 + 1 。 * **非完全二叉樹**:非完全二叉樹需要轉換成完全二叉樹才能按照上面的方案儲存,這樣會浪費很大的儲存空間。 ![image-20200301100043636](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%B8%80/13.png) | 節點 | A | B | C | ^ | ^ | F | ^ | ^ | ^ | ^ | ^ | ^ | M | | -------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ------ | ------ | ------ | ------ | | **序號** | **1** | **2** | **3** | **4** | **5** | **6** | **7** | **8** | **9** | **10** | **11** | **12** | **13** | **使用連結串列** 二叉樹最常見的儲存方式為**連結串列**:每一個節點封裝成一個Node,Node中包含儲存的資料、左節點的引用和右節點的引用。 ![image-20200301100616105](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%B8%80/14.png) ### 三、二叉搜尋樹 #### 3.1.認識二叉搜尋樹 **二叉搜尋樹**(**BST**,Binary Search Tree),也稱為**二叉排序樹**和**二叉查詢樹**。 二叉搜尋樹是一棵二叉樹,可以為空; 如果不為空,則滿足以下**性質**: * 條件1:非空左子樹的**所有**鍵值**小於**其根節點的鍵值。比如三中節點6的所有非空左子樹的鍵值都小於6; * 條件2:非空右子樹的**所有**鍵值**大於**其根節點的鍵值;比如三中節點6的所有非空右子樹的鍵值都大於6; * 條件3:左、右子樹本身也都是二叉搜尋樹; ![image-20200301103139916](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%B8%80/15.png) 如上圖所示,樹二和樹三符合3個條件屬於二叉樹,樹一不滿足條件3所以不是二叉樹。 **總結:**二叉搜尋樹的特點主要是**較小的值**總是儲存在**左節點**上,相對**較大的值**總是儲存在**右節點**上。這種特點使得二叉搜尋樹的查詢效率非常高,這也就是二叉搜尋樹中"搜尋"的來源。 #### 3.2.二叉搜尋樹應用舉例 下面是一個二叉搜尋樹: ![image-20200301111718686](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%B8%80/16.png) 若想在其中查詢資料10,只需要查詢4次,查詢效率非常高。 * 第1次:將10與根節點9進行比較,由於10 > 9,所以10下一步與根節點9的右子節點13比較; * 第2次:由於10 < 13,所以10下一步與父節點13的左子節點11比較; * 第3次:由於10 < 11,所以10下一步與父節點11的左子節點10比較; * 第4次:由於10 = 10,最終查詢到資料10 。 ![image-20200301111751041](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%B8%80/17.png)同樣是15個數據,在排序好的陣列中查詢資料10,需要查詢10次: ![image-20200301115348138](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%B8%80/18.png) 其實:如果是排序好的陣列,可以通過二分查詢:第一次找9,第二次找13,第三次找15...。我們發現如果把每次二分的資料拿出來以樹的形式表示的話就是**二叉搜尋樹**。這就是陣列二分法查詢效率之所以高的原因。 >
參考資料:[JavaScript資料結構與演算法](https://www.bilibili.com/video/av86801505?from=search&seid=496776141191