一文看懂二叉樹的概念和原理

二十二畫程式設計師發表於2021-04-18

系列文章推薦閱讀

0. 前言

到目前為止,我們已經講述了順序表連結串列佇列四種資料結構,它們有一個共同的特點,就是它們都是線性表,換句話來說,它們都是線性結構,像一根繩子一樣。

四種線性資料結構

在文章【線性表】已經介紹過線性表的定義了,即由若干元素按照線性結構(一對一的關係)組成的有限序列。

關鍵詞是一對一的關係

顯然,在複雜的現實社會中,這種一對一的關係是不能較好的滿足我們的需求的。

比如說,父母和多個孩子之間的關係,一個父親/母親對應多個孩子,這顯然不是一對一,而是一對多的關係。那麼此時我們如何來描述這種一對多的關係呢?

當然是使用具有一對多關係的資料結構啦!有這種資料結構嗎?有!本文就來介紹這種資料結構 —— 樹及其特殊形式的二叉樹。

1. 識樹

1.0. 什麼是樹?

提到樹(Tree),大家腦海中首先浮出的畫面應該是類似這樣的:

圖片來自網路

之所以我們會用“樹”這個名詞來命名具有“一對多關係”特性的資料結構,是因為樹剛好能夠很形象地詮釋這種特性。我們來分析一下。

看一下上圖中的樹(土地以上的部分),它有一個樹根,從樹根開始往上分叉,主樹幹分叉成許多次樹幹,次樹幹又繼續分叉為許多小樹枝,小樹枝上有許多葉子……

主樹幹對次樹幹、次樹幹對小樹枝、小樹枝對葉子都是一對多的關係,我們用圓圈代表樹幹和葉子,把自然界的樹倒過來進行一次抽象,得下圖(為了方便起見,我們的資料全為字元型別):

一棵樹

可以看到,現實中的樹完美契合我們需要的資料結構,所以我們稱這種資料結構為樹(Tree)。

1.1. 名詞與概念

我們按圖索驥,來認識樹的相關名詞。

  • 子樹:樹是一個有限集合,子樹則是該集合的子集。就像套娃一樣,一棵樹下面還包含著其子樹。

    比如,樹T1 的子樹為 樹T2、T3、T4,樹T2的子樹為 T5、T6. 上圖中還有許多子樹沒有標記出來。

  • 結點(Node):一個結點包括一個資料元素和若干指向其子樹分支。

    比如,在樹T1 中,結點A 包括一個資料元素A 和 三個指向其子樹的分支。上圖中共有 17 個結點。

  • 根結點(Root):一顆樹只有一個樹根,這是常識。在資料結構中,“樹根”即根節點。

    比如,結點A 是樹 T1 的根結點;結點C 是樹T1 的子結點,是樹 T3 的根結點。

  • (Degree):一個結點擁有的子樹數。

    比如,結點A 的度為 3,結點G 的度為 3,結點H 的度為 1.

  • 葉子(Leaf)/ 終端結點:度為 0 的結點被稱為葉子結點,很形象吧。

    比如,對於樹 T1來說,結點F、I、K、L、M、N、O、P、Q 均為葉子。

  • 分支結點 / 非終端結點:和葉子結點相對,即度不為 0 的結點。

  • 內部結點:顧名思義,在樹內部的結點,即不是根結點和葉子結點的結點。

  • 孩子(Child)、雙親(Parent)、兄弟(Sibling)、堂兄弟祖先子孫這些概念和族譜上的相同。

    比如,對於結點B 來說:結點A 是其雙親結點,結點E、F 是其孩子結點,結點C、D 是其兄弟結點,結點K 是其子孫結點。

  • 層次(Level):從根結點開始,根為第一次,根的孩子為第二層,依次往下。

    比如,結點K 在樹 T1 中的層次為 4.

  • 深度(Depth)/ 高度:指樹的最大層次。

    比如,樹 T1 的深度為 4.

  • 有序樹:如果結點的各子樹從左到右是有次序的、不能顛倒,則為有序樹,否則為無序樹。對於有序樹的孩子來說,最左邊的孩子稱為第一個孩子,最右邊的孩子稱為最後一個孩子。

    比如,如果樹T1是一個有序樹,則其根結點的第一個孩子為結點B,最後一個孩子為結點D.

1.2. 樹的遞迴概念

前面已經介紹了樹的輪廓和相關名詞概念,為了回答什麼是樹這個問題,我們這裡還需要介紹三種常見的樹結構。

【空樹】:一顆空樹,即沒有結點的樹。

空樹

【只有根結點的樹】:只有一個根節點,沒有其他結點。

只有根結點的樹

【普通的樹】

普通樹

現在我們能來回答什麼是樹了:

(Tree)是由 N (N >= 0) 個結點構成的有限集合。

  • 當 N = 0 時,樹為空樹
  • 當 N = 1 時,樹只有一個根結點
  • 當 N > 1 時,樹除了一個根結點外,其餘結點又可分為若干個不相交的有限集合,我們稱之為子樹。

非空樹有且僅有一個根結點。

樹的一對多的關係存在於雙親結點和孩子結點之間。

在樹中,因為存在樹、子樹的概念,所以樹的子樹仍是一顆樹,子樹的子樹仍是一棵樹。

舉個例子:人類的孩子仍是人類,人類的孩子的孩子仍是人類。

因為存在雙親、孩子、子孫的概念,所以根結點的孩子結點可以其子樹的根結點。

舉個例子:一個人,在其孩子看來是父親,在其父母看來是兒子。

這種概念,就是遞迴的概念。

即,對於某個“事物”而言,它的“孩子”和它本身並無實質區別,它做的事,它的“孩子”也會做、也要做。一直向下,“孫子”“曾孫”“玄孫”皆是如此。

為了說明遞迴這個概念,我們將上圖的樹遞迴地分解為子樹,下圖中每個區域都是一顆樹(或子樹):

遞迴解樹

分解到最後,我們最終得到的,可以說是葉子結點,也可以說是隻有根結點的樹。如結點F、K、L.

在分解的過程中,我們還可以發現,對於每個結點來說,我們都可以將其看作某棵樹(子樹)的根結點。比如結點E、I都是某棵子樹的根結點。這與樹有且只有一個根結點並不矛盾。

這就好比我們說,小明只能有一個親生父親,但不影響他成為別人的父親。

整個過程就像在族譜上從祖宗找到子孫一樣。所以如果對樹的概念有啥不瞭解的,可以找個族譜翻翻看。

到此,我們可以說,樹的定義是一個遞迴的定義,樹是由根結點和它的若干子樹組成的,子樹也是由根結點和它的若干子樹組成的……即在樹的定義中又用到樹的定義。

1.3. 樹和線性表的比較

比較

看圖直觀體驗何為(前驅結點和後繼結點間)一對一的關係,何為(雙親結點和孩子結點之間)一對多的關係。

2. 識二叉樹

2.0. 什麼是二叉樹?

何為二叉樹?首先它得是顆樹,其次它得是二叉的。

前面已經初步認識了樹,它的結點的孩子數量是沒有限制的,即,你想要幾個孩子就要幾個孩子,想分幾個叉就分幾個叉。

而二叉樹,則是限制了孩子數量,即每個結點最多隻能有兩個孩子(左孩子和右孩子),打個比方就是“二胎樹”。

二叉樹

結點A 的左孩子是結點B,右孩子是結點C.

二叉樹是一種每個結點至多有兩棵子樹(即每個結點的度最大為 2 )的有序樹。

2.1. 二叉樹的幾種形態

一、空二叉樹

二、僅有根結點的二叉樹

三、左子樹為空的二叉樹

四、右子樹為空的二叉樹

五、左右子樹都不為空的二叉樹

2.2. 滿二叉樹和完全二叉樹

滿二叉樹的特點在於“滿”,即每層的結點數都是最大結點數。

滿二叉樹

T2 的第 3 層次沒有達到最大結點數,缺了 1 個;T3 的第 4 層次沒有達到最大結點數,缺了 7 個。

完全二叉樹是相對於滿二叉樹來說的,見下圖:

紅色部分為編號

二叉樹是有序樹,對一顆滿二叉樹和一顆完全二叉樹按「自上向下,自左向右」的順序進行編號,如上圖。

完全二叉樹中的所有結點的編號必須和滿二叉樹的相同編號的結點在位置上完全相同

換句話說,完全二叉樹的結點按「自上向下,自左向右」的順序不能中斷。T3 的結點C 沒有左孩子,顯然按那個順序是中斷的。

3. 二叉樹的遍歷

3.0. 如何遍歷?

線上性表中,我們的遍歷非常簡單粗暴,找到線性表頭,使用迴圈直接一股腦的到線性表尾,即完成遍歷了。在樹中,我們不能在做這麼簡單粗暴的事了,因為樹是一對多的關係,所以從頭到尾的遍歷是不可能的。

遍歷的實質是,將線性排列的元素順序列印出來。(遍歷不止幹列印的事,為了方便起見,我們的遍歷是列印元素)

而遍歷樹的矛盾在於,我們的樹不是線性的,為了解決這個矛盾,我們可不可以約定好某種順序,將樹的元素按這種順序線性排列起來,然後遍歷就是從頭到尾的簡單粗暴之事了?答案是可以的。

我們知道樹是遞迴的定義,二叉樹是由根結點、左子樹、右子樹這三部分遞迴地組合而成的。所以我們要約定的就是這三部分誰先誰後。

按照人們寫字先左後右的約定,我們也約定先左子樹後右子樹的順序(當然你可以先右後左),那麼根結點就只有三個位置可以放了。

  • 根結點 >> 左子樹 >> 右子樹,稱為先序(根)遍歷
  • 左子樹 >> 根結點 >> 右子樹,稱為中序(根)遍歷
  • 左子樹 >> 右子樹 >> 根結點,稱為後序(根)遍歷

約定好之後,只需要按照順序遞迴地來就好了,就像找族譜一樣。

下面以遍歷下圖二叉樹為例:

為了方便起見,我們將 null 畫出來,且將所有子樹用顏色標誌出來。

3.1. 先序遍歷

先序遍歷的遞迴描述如下:

若二叉樹為空,則空操作;否則:

  1. 訪問根結點
  2. 先序遍歷左子樹
  3. 先序遍歷右子樹

你可能會問,怎麼只有訪問根結點這一步?左孩子和右孩子結點呢?前面說過一句話:對於每個結點來說,我們都可以將其看作某棵樹(子樹)的根結點。就像你的兒子會成為別人的父親一樣。所以只要遞迴地訪問根結點,將每個結點遞迴地變為“根結點”,我們就能完成遍歷。

所以與其說是在遍歷結點,不如說是在遍歷「根結點」,我們只是在遞迴地把「所有根結點」找出來並輸出而已。(因為每個結點都可以看做是根結點)

所以遍歷的重點,在於將所有結點轉化為根結點看待,又因為每棵樹有且僅有一個根結點,所以我們要不斷地遞迴分解子樹(先左子樹後右子樹),直到分解到 NULL 為止。

過程如下:

  1. T1的根結點為結點A,輸出A
  2. T1的左子樹不為空,為T2,進入T2
    1. T2的根結點為結點B,輸出B
    2. T2的左子樹不為空,為T3,進入T3
      1. T3的根結點為結點D,輸出D
      2. T3的左子樹為空,做空操作
      3. T3的右子樹為空,做空操作
      4. 返回到T2
    3. T2的右子樹不為空,為T4,進入T4
      1. T4的根結點為結點E,輸出E
      2. T4的左子樹不為空,為T5,進入T5
        1. T5的根結點為結點G,輸出G
        2. T5的左子樹為空,做空操作
        3. T5的右子樹為空,做空操作
        4. 返回到T4
      3. T4的右子樹為空,做空操作
      4. 返回到T2
    4. 返回到T1
  3. T1的右子樹不為空,為T6,進入T6
    1. T6的根結點為結點C,輸出C
    2. T6的左子樹為空,做空操作
    3. T6的右子樹不為空,為T7,進入T7
      1. T7的根結點為結點F,輸出F
      2. T7的左子樹為空,做空操作
      3. T7的右子樹為空,做空操作
      4. 返回到T6
    4. 返回到T1
  4. 遍歷完成

先序遍歷的順序為:A B D E G C F

如果你感覺文字描述不直觀,可以在我以前寫過的文章中找到二叉樹遍歷過程的動態圖

3.2. 中序遍歷

中序遍歷的遞迴描述如下:

若二叉樹為空,則空操作;否則:

  1. 中序遍歷左子樹
  2. 訪問根結點
  3. 中序遍歷右子樹

過程如下:

  1. T1的左子樹不為空,為T2,進入T2
    1. T2的左子樹不為空,為T3,進入T3
      1. T3的左子樹為空,做空操作
      2. T3的根結點為結點D,輸出D
      3. T3的右子樹為空,做空操作
      4. 返回到T2
    2. T2的根結點為結點B,輸出B
    3. T2的右子樹不為空,為T4,進入T4
      1. T4的左子樹不為空,為T5,進入T5
        1. T5的左子樹為空,做空操作
        2. T5的根結點為結點G,輸出G
        3. T5的右子樹為空,做空操作
        4. 返回到T4
      2. T4的根結點為結點E,輸出E
      3. T4的右子樹為空,做空操作
      4. 返回到T2
    4. 返回到T1
  2. T1的根結點為結點A,輸出A
  3. T1的右子樹不為空,為T6,進入T6
    1. T6的左子樹為空,做空操作
    2. T6的根結點為結點C,輸出C
    3. T6的右子樹不為空,為T7,進入T7
      1. T7的左子樹為空,做空操作
      2. T7的根結點為結點F,輸出F
      3. T7的右子樹為空,做空操作
      4. 返回到T6
    4. 返回到T1
  4. 遍歷完成

中序遍歷的順序為:D B G E A C F

3.3. 後序遍歷

後序遍歷的遞迴描述如下:

若二叉樹為空,則空操作;否則:

  1. 後序遍歷左子樹
  2. 後序遍歷右子樹
  3. 訪問根結點

過程不再描述,後序遍歷的順序為:D G E B F C A

4. 總結

概念和原理是進行實踐的基礎,如果這些不瞭解,那麼程式碼實現就無從下手。

二叉樹的概念和原理先介紹到這裡。

但是樹的相關內容絕不止這一篇文章,後續還會有相關內容。

完整程式碼請移步至 GitHub | Gitee 獲取。

如有錯誤,還請指正。

如果覺得寫的不錯,可以點個贊和關注。後續會有更多資料結構和演算法相關文章。

相關文章