C#數據結構與算法揭秘八


這節重點討論 樹的結構的源代碼實現。

先做一鋪墊,討論一下二叉樹的存儲結構。二叉樹的存儲結構分為線性存儲和鏈式存儲等等。

1、二叉樹的順序存儲結構
對於一棵完全二叉樹,由性質 5可計算得到任意結點 i 的雙親結點序號、左孩子結點序號和右孩子結點序號。所以,完全二叉樹的結點可按從上到下和從左到右的順序存儲在一維數組中,其結點間的關系可由性質 5計算得到,這就是二叉樹的順序存儲結構。下圖所示的二叉樹的順序存儲結構為:

但是,對於一棵非完全二叉樹,不能簡單地按照從上到下和從左到右的順序存放在一維數組中, 因為數組下標之間的關系不能反映二叉樹中結點之間的邏輯關系。 所以, 應該對一棵非完全二叉樹進行改造, 增加空結點 (並不存在的結點)使之成為一完全二叉樹,然后順序存儲在一維數組中。下圖(a)是

完全二叉樹形態,下圖(b)是順序存儲示意圖。

顯然, 順序存儲對於需增加很多空結點才能改造為一棵完全二叉樹的二叉樹適合,因為會造成空間的大量浪費。實際上,采用順序存儲結構,是對非線性
的數據結構線性化,用線性結構來表示二叉樹的結點之間的邏輯關系,所以,需要增加空間。一般來說,有大約一半的空間被浪費。最差的情況是右單支樹,如
下圖所示,一棵深度為k的右單支樹,只有k個結點,卻需要分配 2k-1個存儲單元。

二叉樹的鏈式存儲分為二叉鏈式存儲和三叉鏈式存儲。

二叉樹的二叉鏈式存儲:

二叉樹的二叉鏈表存儲結構是指二叉樹的結點有三個域: 一個數據域和兩個引用域,數據域存儲數據,兩個引用域分別存放其左、右孩子結點的地址。當左
孩子或右孩子不存在時,相應域為空,用符號 NULL 或∧表示。結點的存儲結構如下所示:

二叉樹的三叉鏈式存儲:

使用二叉鏈表,可以非常方便地訪問一個結點的子孫結點,但要訪問祖先結點非常困難。 可以考慮在每個結點中再增加一個引用域存放其雙親結點的地址信息,這樣就可以通過該引用域非常方便地訪問其祖先結點。這就是下面要介紹的三叉鏈表。
二叉樹的三叉鏈表存儲結構是指二叉樹的結點有四個域: 一個數據域和三個引用域,數據域存儲數據,三個引用域分別存放其左、右孩子結點和雙親結點的地址。當左、右孩子或雙親結點不存在時,相應域為空,用符號 NULL 或∧表示。結點的存儲結構如下所示:

簡單的介紹了二叉樹的存儲結構后,我們重點看一看他的源代碼的實現,這是這篇文章的重點。

二叉樹的二叉鏈表的結點類有 3個成員字段:數據域字段 data、左孩子引用域字段 lChild和右孩子引用域字段 rChild。二叉樹的二叉鏈表的結點類的實現如下所示。

 

public class Node<T>
{
private T data; //數據域
private Node<T> lChild; //左孩子
private Node<T> rChild; //右孩子  如下圖所示

//構造器 賦值給相應的數據域,左孩子,右孩子。如圖所示
public Node(T val, Node<T> lp, Node<T> rp)
{
data = val;
lChild = lp;
lChild = rp;
}


//構造器 賦值給相應相應的左孩子,右孩子,數據域賦值給相應的默認值 如圖所示
public Node(Node<T> lp, Node<T> rp)
{
data = default(T);
lChild = lp;
rChild = rp;
}

//構造器 賦值給相應的數據域,左孩子為空,右孩子為空。如圖所示
public Node(T val)
{
data = val;
lChild = null;
rChild = null;
}


//構造器 賦值給相應的 左孩子,右孩子為空,數據域為空。如圖所示
public Node()
{
data = default(T);
lChild = null;
rChild = null;
}


//數據屬性
public T Data
{
get
{
return data;
}
set
{
value = data;
}
}

//左孩子屬性
public Node<T> LChild
{
get
{
return lChild;
}
set
{
lChild = value;
}
}

public Node<T> RChild
{
get
{
return rChild;
}
set
{
rChild = value;
}
}

 

}

不帶頭結點的二叉樹的二叉鏈表比帶頭結點的二叉樹的二叉鏈表的區別與不帶頭結點的單鏈表與帶頭結點的單鏈表的區別一樣。 下面只介紹不帶頭結點的二叉樹的二叉鏈表的類 BiTree<T>。BiTree<T>類只有一個成員字段 head表示頭引用。以下是 BiTree<T>類的源代碼實現。

public class BiTree<T>
{
private Node<T> head; //頭引用 默認指向了根結點

//頭引用屬性
public Node<T> Head
{
get
{
return head;
}
set
{
head = value;
}
}

//構造器
public BiTree()
{
head = null;
}

//構造器
public BiTree(T val)
{
Node<T> p = new Node<T>(val);
head = p;

}

//構造器
public BiTree(T val, Node<T> lp, Node<T> rp)
{
Node<T> p = new Node<T>(val,lp,rp);
head = p;
}

//判斷是否是空二叉樹
public bool IsEmpty()
{
if (head == null)
{
return true;
}
else
{
return false;
}
}

//獲取根結點
public Node<T> Root()
{
return head;
}

//獲取結點的左孩子結點
public Node<T> GetLChild(Node<T> p)
{
return p.LChild;
}

//獲取結點的右孩子結點
public Node<T> GetRChild(Node<T> p)
{
return p.RChild;
}

//將結點p的左子樹插入值為val的新結點,
//原來的左子樹成為新結點的左子樹
public void InsertL(T val, Node<T> p)
{

Node<T> tmp = new Node<T>(val);
tmp.LChild = p.LChild;
p.LChild = tmp;
}

//將結點p的右子樹插入值為val的新結點,
//原來的右子樹成為新結點的右子樹
public void InsertR(T val, Node<T> p)
{
Node<T> tmp = new Node<T>(val);
tmp.RChild = p.RChild;
p.RChild = tmp;
}
算法的復雜度是O(1)
//若p非空,刪除p的左子樹
public Node<T> DeleteL(Node<T> p)
{
if ((p == null) || (p.LChild == null))
{
return null;
}

Node<T> tmp = p.LChild;
p.LChild = null;

return tmp;
}
算法的復雜度是O(1)
//若p非空,刪除p的右子樹
public Node<T> DeleteR(Node<T> p)
{
if ((p == null) || (p.RChild == null))
{
return null;
}

Node<T> tmp = p.RChild;
p.RChild = null;

return tmp;
}
算法的復雜度是O(1)
//判斷是否是葉子結點
public bool IsLeaf(Node<T> p)

{
if ((p != null) && (p.LChild == null) && (p.RChild == null))
{
return true;
}
else
{
return false;
}
}

這些操作的具體情況如圖所示:


}

由於類中基本操作都比較簡單,這里不一一詳細說明。

說完這些操作,我們再看看他的遍歷的實現

二叉樹的遍歷是指按照某種順序訪問二叉樹中的每個結點, 使每個結點被訪問一次且僅一次。遍歷是二叉樹中經常要進行的一種操作,因為在實際應用中,常常要求對二叉樹中某個或某些特定的結點進行處理, 這需要先查找到這個或這些結點。
實際上, 遍歷是將二叉樹中的結點信息由非線性排列變為某種意義上的線性排列。也就是說,遍歷操作使非線性結構線性化。
由二叉樹的定義可知,一棵二叉樹由根結點、左子樹和右子樹三部分組成,若規定 D、L、R 分別代表遍歷根結點、遍歷左子樹、遍歷右子樹,則二叉樹的遍歷方式有 6種:DLR、DRL、LDR、LRD、RDL、RLD。由於先遍歷左子樹和先遍歷右子樹在算法設計上沒有本質區別,所以,只討論三種方式:DLR(先序遍歷) 、LDR(中序遍歷)和 LRD(后序遍歷) 。 除了這三種遍歷方式外,還有一種方式:層序遍歷(Level Order)。層序遍歷
是從根結點開始, 按照從上到下、 從左到右的順序依次訪問每個結點一次僅一次。 由於樹的定義是遞歸的,所以遍歷算法也采用遞歸實現。下面分別介紹這四
種算法,並把它們作為 BiTree<T>類成員方法。

先序遍歷的基本思想是:首先訪問根結點,然后先序遍歷其左子樹,最后先序遍歷其右子樹。先序遍歷的遞歸算法實現如下,注意:這里的訪問根結點是把根結點的值輸出到控制台上。當然,也可以對根結點作其它處理。
public void PreOrder(Node<T> root)
{
//根結點為空
if (root == null)
{
return;
}

//處理根結點
Console.WriteLine("{0}", root.Data);

//先序遍歷左子樹

PreOrder(root.LChild);

//先序遍歷右子樹
PreOrder(root.RChild);  

這個算法的復雜度是O(n²) 如圖所示:


}

2、中序遍歷(LDR)
中序遍歷的基本思想是:首先中序遍歷根結點的左子樹,然后訪問根結點,
最后中序遍歷其右子樹。中序遍歷的遞歸算法實現如下:
public void InOrder(Node<T> root)
{
//根結點為空
if (root == null)
{
return;
}

//中序遍歷左子樹
InOrder(root.LChild);

//處理根結點
Console.WriteLine("{0}", root.Data);

//中序遍歷右子樹
InOrder(root.RChild);

算法的復雜度是O(n²) 如圖所示:


}

3、后序遍歷(LRD)
后序遍歷的基本思想是:首先后序遍歷根結點的左子樹,然后后序遍歷根結
點的右子樹,最后訪問根結點。后序遍歷的遞歸算法實現如下,
public void PostOrder(Node<T> root)
{
//根結點為空
if (root == null)
{
return;
}

//后序遍歷左子樹
PostOrder(root.LChild);

//后序遍歷右子樹

PostOrder(root.RChild);

//處理根結點
Console.WriteLine("{0}", root.Data);

算法的復雜度是O(n²)。如圖所示:

 

}

 

4、層序遍歷(Level Order)
層序遍歷的基本思想是:由於層序遍歷結點的順序是先遇到的結點先訪問,與隊列操作的順序相同。所以,在進行層序遍歷時,設置一個隊列,將根結點引用入隊,當隊列非空時,循環執行以下三步:
(1) 從隊列中取出一個結點引用,並訪問該結點;
(2) 若該結點的左子樹非空,將該結點的左子樹引用入隊;
(3) 若該結點的右子樹非空,將該結點的右子樹引用入隊;
層序遍歷的算法實現如下:
public void LevelOrder(Node<T> root)
{
//根結點為空
if (root == null)
{
return;
}

//設置一個隊列保存層序遍歷的結點
CSeqQueue<Node<T>> sq = new CSeqQueue<Node<T>>(50);

//根結點入隊
sq.In(root);

//隊列非空,結點沒有處理完
while (!sq.IsEmpty())
{
//結點出隊
Node<T> tmp = sq.Out();

//處理當前結點
Console.WriteLine("{o}", tmp);

//將當前結點的左孩子結點入隊
if (tmp.LChild != null)
{
sq.In(tmp.LChild);
}

if (tmp.RChild != null)
{
sq.In(tmp.RChild);
}
}

算法的復雜度是O(n²) 如圖所示:


}

這篇文章,我們介紹了二叉樹的源代碼的實現,一些基本的常識已經介紹完成了。我們下屆還補充一些樹案例,更好的利用樹的作用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM