資料結構之樹學習筆記
一.樹中的節點關係和一些概念
1.基本概念
樹中節點數可以使用n來表示
空樹:n為0的樹
節點的度:指節點的子節點數目,如上圖中B節點為一度,D節點為三度
父子兄弟關係:如上圖中D是G的父節點,H是D的子節點,G是H的兄弟節點,I是J的堂兄弟節點(這個概念不重要)
樹的層次:還是如上圖,A節點為第一層,B、C節點為第二層,D、E、F節點為第三層,G、H、I、J節點為第四層
樹的深度:還是上圖中,總共有四個層次的節點,稱樹的深度為4
樹中的一些注意事項:1)一個子節點只能有一個父節點,但是一個父節點的子節點數目沒有限制
2)子樹之間不能相交
3)樹只能有一個根節點
2.樹的儲存
樹可以採用鏈式儲存,可以採用以下的一些方式儲存:
1)在節點中記錄父節點的位置,即父節點儲存法。這個方式可以很方便找到父節點甚至父節點的父節點,但是尋找子節點時卻需要遍歷查詢;
2)在節點中記錄所有子節點的位置,即子節點儲存法。這個方式同樣方便找子節點,但是尋找父節點同樣需要遍歷;
3)在節點中記錄第一個子節點和其他兄弟節點的位置,即孩子兄弟儲存法。這個方法找子節點和兄弟節點很方便,但是找父節點同樣要遍歷。
二.二叉樹
二叉樹中每個節點的子節點最多隻能有兩個,稱為左右子節點,左右子節點下方的樹稱為左右子樹。
1.一些特殊的二叉樹
左斜樹:所有節點只有左子節點的樹。
右斜樹:所有節點只有右子節點的樹。
滿二叉樹:對於一個k層的二叉樹,k-1層的節點都有兩個子節點,第k層的節點都有0個子節點,這樣的二叉樹稱為滿二叉樹。
完全二叉樹:對一個有k層的滿二叉樹,第k層的節點有2k-1個,如果將這些節點從右向左連續刪除0-2k-1個後就得到一個完全二叉樹。滿二叉樹是特殊的完全二叉樹。下圖分別是4層的滿樹從右向左連續刪除6個節點和3個節點得到的兩個完全二叉樹。
2.二叉樹的儲存
對一個k層的滿二叉樹,每一層的節點數分別是20、21、......、2k-1,總節點數是2k-1。如下圖,將這個滿二叉樹的所有節點一層一層從左到右編號,可以發現一個有趣的現象:每個節點的父節點編號都是這個節點編號除以2的商,每個節點的子節點編號都是這個節點編號乘以2的積(左子節點)或者乘以2加1的和(右子節點)。
根據這個特性可以使用順序的方式儲存二叉樹,給每個節點編號,每個節點的子節點和父節點都可以通過編號運算得到。對於非完全二叉樹,編號時將空缺的位置一同編號即可。下面是三個非完全二叉樹的編號方式:
因此二叉樹的儲存可以使用陣列進行順序儲存,空缺的節點置為空即可。同時也由於空缺的節點都置為空,因此像右斜樹等不完全二叉樹會造成嚴重的空間浪費,因此連結串列儲存的方式也可以採用。
3.二叉樹的遍歷
1)前序遍歷:從根節點開始,先輸出當前節點的資料,再依次遍歷輸出左節點和右節點。
2)中序遍歷:從根節點開始,先輸出當前左節點的資料,再輸出當前節點的資料,最後輸出當前的右節點的資料。
3)後序遍歷:從根節點開始,先輸出當前節點左節點的資料,再輸出當前節點右節點的資料,最後輸出當前節點的資料。
4)層序遍歷:從樹的第一層開始,從上到下逐層遍歷,每一層從左到右依次輸出。
4.二叉樹的程式碼實現(C#),這裡使用順序儲存實現了二叉樹的數值新增和幾種遍歷,沒有實現刪除的方法。
class BiTree<T> { private T[] data; private int count = 0; //當前二叉樹儲存的資料量 public BiTree(int capacity) //當前二叉樹的資料容量 { data = new T[capacity]; } /// <summary> /// 向二叉樹中新增資料或者修改二叉樹中的資料 /// </summary> /// <param name="item"></param>需要儲存的資料 /// <param name="index"></param>需要儲存的資料的編號 /// <returns></returns> public bool Add(T item,int index) { //校驗二叉樹是否存滿 if (count >= data.Length) return false; data[index - 1] = item; count++; return true; } public void Traversal() { FirstTravalsal(1); Console.WriteLine(); MiddleTravalsal(1); Console.WriteLine(); LastTravalsal(1); Console.WriteLine(); LayerTravalsal(); } /// <summary> /// 前序遍歷 /// </summary> /// <param name="index"></param>遍歷的資料的編號 private void FirstTravalsal(int index) { //校驗編號是否存在 if (index > data.Length) return; //校驗資料是否存在,當前位置沒有資料則儲存為-1 if (data[index - 1].Equals(-1)) return; //輸出當前資料 Console.Write(data[index - 1] + " "); //計算左右子節點的下標 int leftNumber = index * 2; int rightNumber = index * 2 + 1; //遞迴遍歷左右子節點 FirstTravalsal(leftNumber); FirstTravalsal(rightNumber); } /// <summary> /// 中序遍歷 /// </summary> /// <param name="index"></param>遍歷的資料的編號 private void MiddleTravalsal(int index) { //校驗編號是否存在 if (index > data.Length) return; //校驗資料是否存在,當前位置沒有資料則儲存為-1 if (data[index - 1].Equals(-1)) return; //計算左右子節點的下標 int leftNumber = index * 2; int rightNumber = index * 2 + 1; //遞迴遍歷左子節點 FirstTravalsal(leftNumber); //輸出當前資料 Console.Write(data[index - 1] + " "); //遞迴遍歷右子節點 FirstTravalsal(rightNumber); } /// <summary> /// 後序遍歷 /// </summary> /// <param name="index"></param>遍歷的資料的編號 private void LastTravalsal(int index) { //校驗編號是否存在 if (index > data.Length) return; //校驗資料是否存在,當前位置沒有資料則儲存為-1 if (data[index - 1].Equals(-1)) return; //計算左右子節點的下標 int leftNumber = index * 2; int rightNumber = index * 2 + 1; //遞迴遍歷左子節點 FirstTravalsal(leftNumber); //遞迴遍歷右子節點 FirstTravalsal(rightNumber); //輸出當前資料 Console.Write(data[index - 1] + " "); } /// <summary> /// 層序遍歷 /// </summary> private void LayerTravalsal() { for(int i = 0;i < data.Length;i++) { //校驗當前資料是否為空 if (data[i].Equals(-1)) continue; //輸出遍歷的資料 Console.Write(data[i] + " "); } } }
三.二叉排序樹
二叉排序樹的節點位置和節點的大小有關,從根節點開始判斷,比當前節點小就往當前節點的左子樹上移動,反之往當前節點的右子樹上移動,一直判斷直到移動到的位置沒有節點,這個位置就是節點的放置位置。如下圖所示,連線線上的數字是節點的放置順序:
可以看到,同樣的資料,最後儲存出來的二叉樹形狀和資料的放置順序有關。
下面是二叉排序樹的實現(C#),這裡使用連結串列實現了二叉排序樹的節點添、查詢和刪除。
class BSNode { public BSNode LeftChild{get;set;} public BSNode RightChild { get; set; } public BSNode Parent { get; set; } public int Data { get; set; } public BSNode() { } public BSNode(int item) { this.Data = item; } }
class BSTree { //記錄根節點位置 public BSNode Root { get; set; } /// <summary> /// 新增資料 /// </summary> /// <param name="item"></param>要新增的資料,以int為例,也可以拓展為一個泛型 public void AddNode(int item) { //新建一個node BSNode newNode = new BSNode(item); //判斷樹中有沒有節點,沒有節點當前節點作為根節點,有節點將資料放入節點中 if (Root == null) Root = newNode; else { //定義一個臨時節點記錄當前正在訪問的節點位置 BSNode temp = Root; //死迴圈,需要不斷判斷節點應該往當前節點左邊還是右邊放置,且不知道要迴圈判斷多少次 while (true) { //如果要放置的資料大於當前節點,說明要放置的節點應該往右邊放 if(item >= temp.Data) { //判斷當前節點的右邊子節點位置是否已經有節點,如果沒有直接放置然後跳出迴圈 if (temp.RightChild == null) { temp.RightChild = newNode; newNode.Parent = temp; break; } //當前節點右節點位置有資料的情況下,將臨時節點置為當前節點的右節點,繼續迴圈判斷應該往這個節點的哪一邊放置 else { temp = temp.RightChild; } } //如果要放置的資料不是大於當前節點,說明要放置的節點應該往左邊放 else { if (temp.LeftChild == null) { temp.LeftChild = newNode; newNode.Parent = temp; break; } else { temp = temp.LeftChild; } } } } } /// <summary> /// 採用中序遍歷可以實現資料由小到大輸出 /// </summary> /// <param name="node"></param>遍歷輸出node節點及其子節點 public void MiddleTraversal(BSNode node) { if (node == null) return; MiddleTraversal(node.LeftChild); Console.Write(node.Data + " "); MiddleTraversal(node.RightChild); } /// <summary> /// 查詢資料是否在樹中(遞迴方式) /// </summary> /// <param name="item"></param>要查詢的資料 /// <param name="node"></param>在node節點及其子孫節點中查詢 /// <returns></returns> public bool Find1(int item,BSNode node) { //校驗當前節點是否為空 if (node == null) return false; //判斷節點的資料是否和要查詢的資料相同 else if (item == node.Data) return true; //判斷要查詢的資料和當前資料的大小,決定是繼續在左子樹中查詢還是在右子樹中查詢 else if (item > node.Data) return Find1(item, node.RightChild); else return Find1(item, node.LeftChild); } /// <summary> /// 查詢資料是否在樹中(迴圈方式) /// </summary> /// <param name="item"></param>要查詢的資料 /// <param name="node"></param>在node節點及其子孫節點中查詢 /// <returns></returns> public bool Find2(int item, BSNode node) { BSNode temp = node; while (true) { //校驗當前節點是否為空 if (temp == null) return false; //判斷節點的資料是否和要查詢的資料相同 else if (item == temp.Data) return true; //判斷要查詢的資料和當前資料的大小,決定是繼續在左子樹中查詢還是在右子樹中查詢 else if (item > temp.Data) temp = temp.RightChild; else temp = temp.LeftChild; } } /// <summary> /// 根據資料查詢並刪除儲存資料的節點 /// </summary> /// <param name="item"></param>要刪除的資料 /// <returns></returns> public bool DeleteNode(int item) { //首先需要查詢要刪除的節點是否存在,使用臨時節點temp記錄,如果存在才能刪除,否則不能刪除 BSNode temp = Root; while (true) { //校驗當前節點是否為空 if (temp == null) return false; //判斷節點的資料是否和要查詢的資料相同,相同就需要刪除這個節點 else if (item == temp.Data) { DeleteNode(temp); return true; } //判斷要查詢的資料和當前資料的大小,決定是繼續在左子樹中查詢還是在右子樹中查詢 else if (item > temp.Data) temp = temp.RightChild; else temp = temp.LeftChild; } } /// <summary> /// 刪除指定的節點 /// </summary> /// <param name="node"></param>要刪除的節點 private void DeleteNode(BSNode node) { //判斷當前節點是否為根節點,不是根節點刪除時需要修改當前節點的父節點的引用指向,是根節點需要修改Root的值 if (node.Parent != null) { //分為四種情況,分別是這個節點沒有子節點、只用左子節點、只有右子節點和有左右兩個子節點 //沒有子節點直接修改父節點的引用,並刪除節點 if (node.LeftChild == null && node.RightChild == null) { if (node.Parent.RightChild == node) node.Parent.RightChild = null; else node.Parent.LeftChild = null; node = null; } //只有左子節點或者右子節點需要更改父節點的引用,並更改相應子節點的父節點引用 else if (node.LeftChild == null && node.RightChild != null) { if (node.Parent.RightChild == node) node.Parent.RightChild = node.RightChild; else node.Parent.LeftChild = node.RightChild; node.RightChild.Parent = node.Parent; node = null; } else if (node.LeftChild != null && node.RightChild == null) { if (node.Parent.RightChild == node) node.Parent.RightChild = node.LeftChild; else node.Parent.LeftChild = node.LeftChild; node.LeftChild.Parent = node.Parent; node = null; } //左右子節點都有的情況下將右子樹的最小值的資料複製到當前節點,然後去刪除右子樹的最小值 else { BSNode temp = node.RightChild; while (true) { if (temp.LeftChild != null) temp = temp.LeftChild; else break; } node.Data = temp.Data; DeleteNode(temp); } } //當前節點是根節點的情況下和不是根節點的刪除類似,也是四種情況,不過需要修改的是Root的引用和子節點的父節點引用,不用管父節點 else { if (node.LeftChild == null && node.RightChild == null) { node = null; Root = null; } else if (node.LeftChild == null && node.RightChild != null) { node.RightChild.Parent = null; Root = node.RightChild; node = null; } else if (node.LeftChild != null && node.RightChild == null) { node.LeftChild.Parent = null; Root = node.LeftChild; node = null; } else { BSNode temp = node.RightChild; while (true) { if (temp.LeftChild != null) temp = temp.LeftChild; else break; } node.Data = temp.Data; DeleteNode(temp); } } } }
四.堆排序
1.大頂堆和小頂堆
大頂堆:每個節點的值都大於兩個子節點的完全二叉樹稱為大頂堆
小頂堆:每個節點的值都小於兩個子節點的完全二叉樹稱為小頂堆
如圖所示大頂堆和小頂堆:
2.堆排序演算法
以大頂堆為例,由於在大頂堆中根節點數值一定是最大的,因此只需要將根節點的資料和二叉樹中最後一個節點調換位置,然後將剩下的節點重新構造稱大頂堆,繼續重複取值即可。
3.堆排序演算法實現(大頂堆排序)
由於堆是一個完全二叉樹,因此使用順序儲存二叉樹,並將二叉樹改造為大頂堆。
static void Main(string[] args) { int[] data = { 62, 58, 38, 88, 47, 73, 99, 35, 51, 93, 37, 68 }; data = Sort(data); foreach(int i in data) { Console.Write(i + " "); } } /// <summary> /// 排序 /// </summary> /// <param name="data"></param>需要排序的陣列 public static int[] Sort(int[] data) { //初始化,將當前二叉樹構建成大頂堆 for(int i = data.Length / 2;i >= 1;i--) { data = AdjustHeap(data, i, data.Length); } //進行交換排序,交換後大頂堆的第一個節點和節點數發生了改變,因此只需要把發生了變化的第一個節點進行調整即可 for(int i = data.Length;i > 1;i--) { data[i - 1] += data[0]; data[0] = data[i - 1] - data[0]; data[i - 1] -= data[0]; data = AdjustHeap(data, 1,i - 1); } return data; } /// <summary> /// 將編號i的父節點及其所有子節點構建成大頂堆 /// </summary> /// <param name="data">指定的陣列</param> /// <param name="i">需要構建大頂堆的節點編號</param> /// <param name="length">當前構建大頂堆的節點數</param> public static int[] AdjustHeap(int[] data,int i,int length) { //迴圈構建大頂堆,如果當前節點沒有子節點或者當前節點結構已經是大頂堆就推出迴圈,否則哪邊進行了交換就繼續在那一邊構建大頂堆 while(true) { int leftChildNum = i * 2; int rightChildNum = i * 2 + 1; //如果當前節點沒有子節點,不需要構建大頂堆 if (leftChildNum > length && rightChildNum > length) break; //將最大節點編號定義為父節點編號 int maxNodeNumber = i; //比較並找到最大節點的編號 if (leftChildNum <= length && data[leftChildNum - 1] > data[maxNodeNumber - 1]) maxNodeNumber = leftChildNum; if (rightChildNum <= length && data[rightChildNum - 1] > data[maxNodeNumber - 1]) maxNodeNumber = rightChildNum; //如果最大節點的編號右改變,說明父節點小於子節點,進行交換即可 //交換後被交換的一邊可能又不是大頂堆了,所以需要繼續 if (maxNodeNumber != i) { data[maxNodeNumber - 1] += data[i - 1]; data[i - 1] = data[maxNodeNumber - 1] - data[i - 1]; data[maxNodeNumber - 1] -= data[i - 1]; i = maxNodeNumber; } //如果當前節點結構已經是大頂堆,沒有任何修改,也退出迴圈 else break; } return data; }
&n