二叉樹 陳廣老師

6.2.2  二叉樹的存儲結構

二叉樹的存儲可分爲兩種:順序存儲結構和鏈式存儲結構。

1.      順序存儲結構

把一個滿二叉樹自上而下、從左到右順序編號,依次存放在數組內,可得到圖6.8(a)所示的結果。設滿二叉樹結點在數組中的索引號爲i,那麼有如下性質。

(1) 如果i = 0,此結點爲根結點,無雙親。

(2) 如果i > 0,則其雙親結點爲(i -1) / 2 。(注意,這裏的除法是整除,結果中的小數部分會被捨棄。)

(3) 結點i的左孩子爲2i + 1,右孩子爲2i + 2

(4) 如果i > 0,當i爲奇數時,它是雙親結點的左孩子,它的兄弟爲i + 1;當i爲偶數時,它是雙新結點的右孩子,它的兄弟結點爲i – 1

(5) 深度爲k的滿二叉樹需要長度爲2 k-1的數組進行存儲。

通過以上性質可知,使用數組存放滿二叉樹的各結點非常方便,可以根據一個結點的索引號很容易地推算出它的雙親、孩子、兄弟等結點的編號,從而對這些結點進行訪問,這是一種存儲二叉滿二叉樹或完全二叉樹的最簡單、最省空間的做法。

爲了用結點在數組中的位置反映出結點之間的邏輯關係,存儲一般二叉樹時,只需要將數組中空結點所對應的位置設爲空即可,其效果如圖6.8(b)所示。這會造成一定的空間浪費,但如果空結點的數量不是很多,這些浪費可以忽略。

一個深度爲k的二叉樹需要2 k-1個存儲空間,當k值很大並且二叉樹的空結點很多時,最壞的情況是每層只有一個結點,再使用順序存儲結構來存儲顯然會造成極大地浪費,這時就應該使用鏈式存儲結構來存儲二叉樹中的數據。

5143392249434326573.jpg

2.      鏈式存儲結構

二叉樹的鏈式存儲結構可分爲二叉鏈表和三叉鏈表。二叉鏈表中,每個結點除了存儲本身的數據外,還應該設置兩個指針域leftright,它們分別指向左孩子和右孩子(如圖6.9(a)所示)。

當需要在二叉樹中經常尋找某結點的雙親,每個結點還可以加一個指向雙親的指針域parent,如圖6.9(b)所示,這就是三叉鏈表。

5143392249434326574.jpg

6.10所示的是二叉鏈表和三叉鏈表的存儲結構,其中虛線箭頭表示parent指針所指方向。

3998070569198544374.jpg

二叉樹還有一種叫雙親鏈表的存儲結構,它只存儲結點的雙親信息而不存儲孩子信息,由於二叉樹是一種有序樹,一個結點的兩個孩子有左右之分,因此結點中除了存放雙新信息外,還必須指明這個結點是左孩子還是右孩子。由於結點不存放孩子信息,無法通過頭指針出發遍歷所有結點,因此需要藉助數組來存放結點信息。圖6.10(a)所示的二叉樹使用雙親鏈表進行存儲將得到圖6.11所示的結果。由於根節點沒有雙新,所以它的parent指針的值設爲-1

5143392249434326576.jpg

雙親鏈表中元素存放的順序是根據結點的添加順序來決定的,也就是說把各個元素的存放位置進行調換不會影響結點的邏輯結構。由圖6.11可知,雙親鏈表在物理上是一種順序存儲結構。

二叉樹存在多種存儲結構,選用何種方法進行存儲主要依賴於對二叉樹進行什麼操作來確定。而二叉鏈表是二叉樹最常用的存儲結構,下面幾節給出的有關二叉樹的算法大多基於二叉鏈表存儲結構。

6.3 二叉樹的遍歷

二叉樹遍歷(Traversal)就是按某種順序對樹中每個結點訪問且只能訪問一次的過程。訪問的含義很廣,如查詢、計算、修改、輸出結點的值。樹遍歷本質上是將非線性結構線性化,它是二叉樹各種運算和操作的實現基礎,需要高度重視。

6.3.1  二叉樹的深度優先遍歷

我們是用遞歸的方法來定義二叉樹的。每棵二叉樹由結點、左子樹、右子樹這三個基本部分組成,如果遍歷了這三部分,也就遍歷了整個二叉樹。如圖6.12所示,D爲二叉樹中某一結點,LR分別爲結點D的左、右子樹,則其遍歷方式有6種:

5143392249434326577.jpg

         先左後右   先右後左

先序       DLR       DRL

中序       LDR       RDL

後序       LRD       RLD

這裏只討論先左後右的三種遍歷算法。

如圖6.13所示,在沿着箭頭方向所指的路徑對二叉樹進行遍歷時,每個節點會在這條搜索路徑上會出現三次,而訪問操作只能進行一次,這時就需要決定在搜索路徑上第幾次出現的結點進行訪問操作,由此就引出了三種不同的遍歷算法。

5143392249434326578.jpg

1.      先序遍歷

若二叉樹爲非空,則過程爲:

(1) 訪問根節點。

(2) 先序遍歷左子樹。

(3) 先序遍歷右子樹。

6.13中,先序遍歷就是把標號爲(1)的結點按搜索路徑訪問的先後次序連接起來,得出結果爲:ABDECF

2.      中序遍歷

若二叉樹爲非空,則過程爲:

(1) 按中序遍歷左子樹。

(2) 訪問根結點。

(3) 按中序遍歷右子樹。

6.13中,先序遍歷就是把標號爲(2)的結點按搜索路徑訪問的先後次序連接起來,得出結果爲:DBEACF

3.      後序遍歷

若二叉樹爲非空,則過程爲:

(1) 按後序遍歷左子樹。

(2) 按後序遍歷右子樹

(3) 訪問根結點。

6.13中,先序遍歷就是把標號爲(3)的結點按搜索路徑訪問的先後次序連接起來,得出結果爲:DEBFCA

【例6-1  BinaryTreeNode.cs】二叉樹結點類

1  &65279;using System;

2  public class Node

3  {

4      //成員變量

5      private object _data; //數據

6      private Node _left; //左孩子

7      private Node _right; //右孩子

8      public object Data

9      {

10         get { return _data; }

11     }

12     public Node Left //左孩子

13     {

14         get { return _left; }

15         set { _left = value; }

16     }

17     public Node Right //右孩子

18     {

19         get { return _right; }

20         set { _right = value; }

21     }

22     //構造方法

23     public Node(object data)

24     {

25         _data = data;

26     }

27     public override string ToString()

28     {

29         return _data.ToString();

30     }

31 }

Node類專門用於表示二叉樹中的一個結點,它很簡單,只有三個屬性:Data表示結點中的數據;Left表示這個結點的左孩子,它是Node類型;Right表示這個結點的右孩子,它也是Node類型。

【例6-1  BinaryTree.cs】二叉樹集合類

1  &65279;using System;

2  public class BinaryTree

3  { //成員變量

4      private Node _head; //頭指針

5      private string cStr; //用於構造二叉樹的字符串

6      public Node Head //頭指針

7      {

8          get { return _head; }

9      }

10     //構造方法

11     public BinaryTree(string constructStr)

12     {

13         cStr = constructStr; //保存構造字符串

14         _head = new Node(cStr[0]); //添加頭結點

15         Add(_head, 0); //給頭結點添加孩子結點

16     }

17     private void Add(Node parent, int index)

18     {

19         int leftIndex = 2 * index + 1; //計算左孩子索引

20         if (leftIndex < cStr.Length) //如果索引沒超過字符串長度

21         {

22             if (cStr[leftIndex] != '#') //'#'表示空結點

23             {   //添加左孩子

24                 parent.Left = new Node(cStr[leftIndex]);

25                 //遞歸調用Add方法給左孩子添加孩子節點

26                 Add(parent.Left, leftIndex);

27             }

28         }

29         int rightIndex = 2 * index + 2;

30         if (rightIndex < cStr.Length)

31         {

32             if (cStr[rightIndex] != '#')

33             {   //添加右孩子

34                 parent.Right = new Node(cStr[rightIndex]);

35                 //遞歸調用Add方法給右孩子添加孩子節點

36                 Add(parent.Right, rightIndex);

37             }

38         }

39     }

40     public void PreOrder(Node node) //先序遍歷

41     {

42         if (node != null)

43         {

44             Console.Write(node.ToString()); //打印字符

45             PreOrder(node.Left); //遞歸

46             PreOrder(node.Right); //遞歸

47         }

48     }

49     public void MidOrder(Node node) //中序遍歷

50     {

51         if (node != null)

52         {

53             MidOrder(node.Left); //遞歸

54             Console.Write(node.ToString()); //打印字符

55             MidOrder(node.Right); //遞歸

56         }

57     }

58     public void AfterOrder(Node node) //後繼遍歷

59     {

60         if (node != null)

61         {

62             AfterOrder(node.Left); //遞歸

63             AfterOrder(node.Right); //遞歸

64             Console.Write(node.ToString()); //打印字符

65         }

66     }

67 }

BinaryTree是一個二叉樹的集合類,它屬於二叉鏈表,實際存儲的信息只有一個頭結點指針(Head),由於是鏈式存儲結構,可以由Head指針出發遍歷整個二叉樹。爲了便於測試及添加結點,假設BinaryTree類中存放的數據是字符類型,第5行聲明瞭一個字符串類型成員cStr,它用於存放結點中所有的字符。字符串由滿二叉樹的方式進行構造,空結點用‘#’號表示(參考本章“二叉樹存儲結構”這一小節中的“順序存儲結構”)。圖6.13所示的二叉樹可表示爲:“ABCDE#F”。

1116行的構造方法傳入一個構造字符串,並在Add()方法中根據這個字符串來構造二叉樹中相應的結點。需要注意,這個構造方法只用於測試。

1739行的Add()方法用於添加結點,它的第一個參數parent表示需要添加孩子結點的雙親結點,第二個參數index表示這個雙親結點的編號(編號表示使用順序存儲結構時它在數組中的索引,請參考本章“二叉樹存儲結構”這一小節中的“順序存儲結構”)。添加孩子結點的方法是先計算孩子結點的編號,然後通過這個編號在cStr中取出相應的字符,並構造新的孩子結點用於存放這個字符,接下來遞歸調用Add()方法給孩子結點添加它們的孩子結點。注意,這個方法只用於測試。

4048行代碼的PreOrder()方法用於先序遍歷,它的代碼跟之前所講解的先序遍歷過程完全一樣。

4957行代碼的MidOrder()方法用於中序遍歷。

5866行代碼的AfterOrder()方法用於後序遍歷。

以上三個方法都使用了遞歸來完成遍歷,這符合二叉樹的定義。

【例6-1  Demo6-1.cs】二叉樹深度優先遍歷測試

1  using System;

2  class Demo6_1

3  {

4      static void Main(string[] args)

5      {   //使用字符串構造二叉樹

6          BinaryTree bTree = new BinaryTree("ABCDE#F");

7          bTree.PreOrder(bTree.Head); //先序遍

8          Console.WriteLine();

9          bTree.MidOrder(bTree.Head); //中序遍

10         Console.WriteLine();

11         bTree.AfterOrder(bTree.Head); //後序遍

12         Console.WriteLine();

13     }

14 }

運行結果:

ABDECF

DBEACF

DEBFCA

6.3.2  二叉樹的寬度優先遍歷

之前所講述的二叉樹的深度優先遍歷的搜索路徑是首先搜索一個結點的所有子孫結點,再搜索這個結點的兄弟結點。是否可以先搜索所有兄弟和堂兄弟結點再搜索子孫結點呢?

由於二叉樹結點分屬不同的層次,因此可以從上到下、從左到右依次按層訪問每個結點。它的訪問順序正好和之前所述二叉樹順序存儲結構中的結點在數組中的存放順序相吻合。如圖6.13中的二叉樹使用寬度優先遍歷訪問的順序爲:ABCDEF

這個搜索過程不再需要使用遞歸,但需要藉助隊列來完成。

(1) 將根結點壓入隊列之中,開始執行步驟(2)

(2) 若隊列爲空,則結束遍歷操作,否則取隊頭結點D

(3) 若結點D的左孩子結點存在,則將其左孩子結點壓入隊列。

(4) 若結點D的右孩子結點存在,則將其右孩子結點壓入隊列,並重復步驟(2)

【例6-2  BinaryTreeNode.cs.cs】二叉樹結點類,使用例6-1同名文件。

【例6-2  LevelOrderBinaryTree.cs】包含寬度優先遍歷方法的二叉樹集合類

打開例6-1的【BinaryTree.cs】文件,在BinaryTree類中添加如入方法後另存爲LevelOrderBinaryTree.cs文件。

68     public void LevelOrder() //寬度優先遍歷

69     {

70         Queue queue = new Queue(); //聲明一個隊例

71         queue.Enqueue(_head); //把根結點壓入隊列

72         while (queue.Count > 0) //只要隊列不爲空

73         {

74             Node node = (Node)queue.Dequeue(); //出隊

75             Console.Write(node.ToString()); //訪問結點

76             if (node.Left != null) //如果結點左孩子不爲空

77             {   //把左孩子壓入隊列

78                 queue.Enqueue(node.Left);

79             }

80             if (node.Right != null) //如果結點右孩子不爲熔

81             {   //把右孩子壓入隊列

82                 queue.Enqueue(node.Right);

83             }

84         }

85     }

【例6-2  Demo6-2.cs】二叉樹寬度優先遍歷測試

1 using System;

2 class Demo6_2

3 {

4     static void Main(string[] args)

5     {   //使用字符串構造二叉樹

6         BinaryTree bTree = new BinaryTree("ABCDE#F");

7         bTree.LevelOrder(); //寬度優先遍歷

8     }

9 }

運行結果:

ABCDEF

發佈了30 篇原創文章 · 獲贊 1 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章