書接上回,今天和大家一起動手來自己實現樹。
相信透過前面的章節學習,大家已經明白樹是什麼了,今天我們主要針對二叉樹,分別使用順序儲存和鏈式儲存來實現樹。
01、陣列實現
我們在上一節中說過,如果從樹的頂層開始從上到下,從左到右,對每個節點按順序進行編號,根節點為1作為起始,則有節點編號i,其左子節點編號為2i,其右子節點編號為2i+1。但是我們知道陣列的索引是從0開始,因此如果我們用陣列實現二叉樹,那麼我們可以把上面的編號改為從0開始,因此可以得到如下結論:
節點編號:i;
其左子節點編號:2i+1;
其右子節點編號:2i+2;
1、初始化 Init
初始化主要是指定樹節點容量,建立指定容量的陣列。
//初始化樹為指定容量
public MyselfTreeArray<T> Init(int capacity)
{
//初始化指定長度陣列用於存放樹節點元素
_array = new T[capacity];
//返回樹
return this;
}
2、獲取樹節點數 Count
樹節點數可以透過內部陣列長度直接獲取。
//樹節點數量
public int Count
{
get
{
return _array.Length;
}
}
3、獲取節點索引 GetIndex
獲取節點索引主要是為了把節點值轉為索引值,因為我們是使用陣列實現,元素的操作更多依賴索引。其實我們還有其他方案來處理獲取索引的方式,比如可以透過設計元素物件使其包含索引欄位,這樣其實操作起來更為方便。下面我們還是用最直接獲取的方法作為演示。
//返回指定資料的索引
public int GetIndex(T data)
{
if (data == null)
{
return -1;
}
//根據值查詢索引
return Array.IndexOf(_array, data);
}
4、計算父節點索引 CalcParentIndex
該方法用於實現透過子節點索引計算父節點索引,無論左子節點還是右子節點,使用下面一個公式就可以了,程式碼如下:
//根據子索引計算父節點索引
public int CalcParentIndex(int childIndex)
{
//應用公式計算父節點索引
var parentIndex = (childIndex + 1) / 2 - 1;
//檢查索引是否越界
if (childIndex <= 0 || childIndex >= _array.Length
|| parentIndex < 0 || parentIndex >= _array.Length)
{
return -1;
}
//返回父節點索引
return parentIndex;
}
5、計算左子節點索引 CalcLeftChildIndex
該方法用於根據父節點索引計算左子節點索引。
//根據父節點索引計算左子節點索引
public int CalcLeftChildIndex(int parentIndex)
{
//應用公式計算左子節點索引
var leftChildIndex = 2 * parentIndex + 1;
//檢查索引是否越界
if (leftChildIndex <= 0 || leftChildIndex >= _array.Length
|| parentIndex < 0 || parentIndex >= _array.Length)
{
return -1;
}
//返回左子節點索引
return leftChildIndex;
}
6、計算右子節點 CalcRightChildIndex
該方法用於根據父節點索引計算右子節點索引。
//根據父節點索引計算右子節點索引
public int CalcRightChildIndex(int parentIndex)
{
//應用公式計算右子節點索引
var rightChildIndex = 2 * parentIndex + 2;
//檢查索引是否越界
if (rightChildIndex <= 0 || rightChildIndex >= _array.Length
|| parentIndex < 0 || parentIndex >= _array.Length)
{
return -1;
}
//返回右子節點索引
return rightChildIndex;
}
7、獲取節點值 Get
該方法透過節點索引獲取節點值,如果索引越界則報錯。
//透過節點索引獲取節點值
public T Get(int index)
{
//檢查索引是否越界
if (index < 0 || index >= _array.Length)
{
throw new IndexOutOfRangeException("無效索引");
}
//返回節點值
return _array[index];
}
8、獲取左子節點值 GetLeftChild
該方法是透過節點索引獲取其左節點值,首先獲取左子節點索引,再取左子節點值。
//透過節點索引獲取其左子節點值
public T GetLeftChild(int parentIndex)
{
//獲取左子節點索引
var leftChildIndex = CalcLeftChildIndex(parentIndex);
//檢查索引是否越界
if (leftChildIndex < 0)
{
throw new IndexOutOfRangeException("無效索引");
}
//返回左子節點值
return _array[leftChildIndex];
}
9、獲取右子節點值 GetRightChild
該方法是透過節點索引獲取其右節點值,首先獲取右子節點索引,再取右子節點值。
//透過節點索引獲取其右子節點值
public T GetRightChild(int parentIndex)
{
//獲取右子節點索引
var rightChildIndex = CalcRightChildIndex(parentIndex);
//檢查索引是否越界
if (rightChildIndex < 0)
{
throw new IndexOutOfRangeException("無效索引");
}
//返回右子節點值
return _array[rightChildIndex];
}
10、新增或更新節點值 AddOrUpdate
該方法是透過節點索引新增或更新節點值,因為陣列初始化的時候已經有了預設值,因此新增或者更新節點值就是直接給陣列元素賦值,如果索引越界則報錯。
//透過節點索引新增或更新節點值
public void AddOrUpdate(int index, T data)
{
//檢查索引是否越界
if (index < 0 || index >= _array.Length)
{
throw new IndexOutOfRangeException("無效索引");
}
//更新值
_array[index] = data;
}
11、新增或更新左子節點值 AddOrUpdateLeftChild
該方法根據節點值新增或更新其左子節點值,首先透過節點值找到其索引,然後透過其索引計算出其左子節點索引,索引校驗成功後,直接更新左子節點值。
//透過節點值新增或更新左子節點值
public void AddOrUpdateLeftChild(T parent, T left)
{
//獲取節點索引
var parentIndex = GetIndex(parent);
//獲取左子節點索引
var leftChildIndex = CalcLeftChildIndex(parentIndex);
//檢查索引是否越界
if (leftChildIndex < 0)
{
throw new IndexOutOfRangeException("無效索引");
}
//更新值
_array[leftChildIndex] = left;
}
12、新增或更新右子節點值 AddOrUpdateRightChild
該方法根據節點值新增或更新其右子節點值,首先透過節點值找到其索引,然後透過其索引計算出其右子節點索引,索引校驗成功後,直接更新右子節點值。
//透過節點值新增或更新右子節點值
public void AddOrUpdateRightChild(T parent, T right)
{
//獲取節點索引
var parentIndex = GetIndex(parent);
//獲取左子節點索引
var rightChildIndex = CalcRightChildIndex(parentIndex);
//檢查索引是否越界
if (rightChildIndex < 0)
{
throw new IndexOutOfRangeException("無效索引");
}
//更新值
_array[rightChildIndex] = right;
}
13、刪除節點及其所有後代節點 Remove
該方法透過節點索引刪除其自身以及其所有後代節點,刪除後代節點需要左右子節點分別遞迴呼叫方法自身。
//透過節點索引刪除節點及其所有後代節點
public void Remove(int index)
{
//非法索引直接跳過
if (index < 0 || index >= _array.Length)
{
return;
}
//清除節點值
_array[index] = default;
//獲取左子節點索引
var leftChildIndex = CalcLeftChildIndex(index);
//遞迴刪除左子節點及其所有後代
Remove(leftChildIndex);
//獲取右子節點索引
var rightChildIndex = CalcRightChildIndex(index);
//遞迴刪除右子節點及其所有後代
Remove(rightChildIndex);
}
14、刪除左節點及其所有後代節點 RemoveLeftChild
該方法透過節點值刪除其左節點以及其左節點所有後代節點,首先透過節點值獲取節點索引,然後計算出左子節點索引,最後透過呼叫刪除節點Remove方法完成刪除。
//透過節點值刪除其左節點及其所有後代節點
public void RemoveLeftChild(T parent)
{
//獲取節點索引
var parentIndex = GetIndex(parent);
//獲取左子節點索引
var leftChildIndex = CalcLeftChildIndex(parentIndex);
//檢查索引是否越界
if (leftChildIndex < 0)
{
throw new IndexOutOfRangeException("無效索引");
}
//刪除左子節點及其所有後代
Remove(leftChildIndex);
}
15、刪除右節點及其所有後代節點 RemoveRightChild
該方法透過節點值刪除其右節點以及其右節點所有後代節點,首先透過節點值獲取節點索引,然後計算出右子節點索引,最後透過呼叫刪除節點Remove方法完成刪除。
//透過節點值刪除其右節點及其所有後代節點
public void RemoveRightChild(T parent)
{
//獲取節點索引
var parentIndex = GetIndex(parent);
//獲取右子節點索引
var rightChildIndex = CalcRightChildIndex(parentIndex);
//檢查索引是否越界
if (rightChildIndex < 0)
{
throw new IndexOutOfRangeException("無效索引");
}
//刪除右子節點及其所有後代
Remove(rightChildIndex);
}
16、前序遍歷 PreOrderTraversal
前序遍歷的核心思想就是先列印樹的根節點,然後再列印樹的左子樹,最後列印樹的右子樹。
//前序遍歷
public void PreOrderTraversal(int index = 0)
{
//非法索引直接跳過
if (index < 0 || index >= _array.Length)
{
return;
}
//列印
Console.Write(_array[index]);
//獲取左子節點索引
var leftChildIndex = CalcLeftChildIndex(index);
//列印左子樹
PreOrderTraversal(leftChildIndex);
//獲取右子節點索引
var rightChildIndex = CalcRightChildIndex(index);
//列印右子樹
PreOrderTraversal(rightChildIndex);
}
17、中序遍歷 InOrderTraversal
中序遍歷的核心思想就是先列印樹的左子樹,然後再列印樹的根節點,最後列印樹的右子樹。
//中序遍歷
public void InOrderTraversal(int index = 0)
{
//非法索引直接跳過
if (index < 0 || index >= _array.Length)
{
return;
}
//獲取左子節點索引
var leftChildIndex = CalcLeftChildIndex(index);
//列印左子樹
InOrderTraversal(leftChildIndex);
//列印
Console.Write(_array[index]);
//獲取右子節點索引
var rightChildIndex = CalcRightChildIndex(index);
//列印右子樹
InOrderTraversal(rightChildIndex);
}
18、後序遍歷 PostOrderTraversal
後序遍歷的核心思想就是先列印樹的左子樹,然後再列印樹的右子樹,最後列印樹的根節點。
//後序遍歷
public void PostOrderTraversal(int index = 0)
{
//非法索引直接跳過
if (index < 0 || index >= _array.Length)
{
return;
}
//獲取左子節點索引
var leftChildIndex = CalcLeftChildIndex(index);
//列印左子樹
PostOrderTraversal(leftChildIndex);
//獲取右子節點索引
var rightChildIndex = CalcRightChildIndex(index);
//列印右子樹
PostOrderTraversal(rightChildIndex);
//列印
Console.Write(_array[index]);
}
19、層次遍歷 LevelOrderTraversal
層次遍歷的核心思想是藉助佇列,從根節點開始,從上到下,從左到右,先入列後等待後續列印,然後出列後立即列印,同時把此節點的左右子節點按順序加入佇列,迴圈往復直至所有元素列印完成。
//層次遍歷
public void LevelOrderTraversal()
{
//建立一個佇列用於層次遍歷
var queue = new Queue<int>();
//先把根節點索引0入隊
queue.Enqueue(0);
//只有佇列中有值就一直處理
while (queue.Count > 0)
{
//出列,取出第一個節點索引
var currentIndex = queue.Dequeue();
//列印第一個節點值
Console.Write(_array[currentIndex]);
//獲取左子節點索引
int leftChildIndex = CalcLeftChildIndex(currentIndex);
// 如果左子節點存在,將其索引加入佇列
if (leftChildIndex >= 0)
{
queue.Enqueue(leftChildIndex);
}
//獲取右子節點索引
int rightChildIndex = CalcRightChildIndex(currentIndex);
// 如果右子節點存在,將其索引加入佇列
if (rightChildIndex >= 0)
{
queue.Enqueue(rightChildIndex);
}
}
}
02、連結串列實現
連結串列實現通用性會更強,基本可以實現所有的樹結構,同時操作也更方便了,下面我們還是以二叉樹的實現為例做演示。
1、初始化樹根節點 InitRoot
在初始化樹根節點前需要定義好連結串列的節點,其包含資料域和左右子節點,同時在樹種還需要維護根節點以及節點數量兩個私有欄位。
public class MyselfTreeNode<T>
{
//資料域
public T Data { get; set; }
////左子節點
public MyselfTreeNode<T> Left { get; set; }
//右子節點
public MyselfTreeNode<T> Right { get; set; }
public MyselfTreeNode(T data)
{
Data = data;
Left = null;
Right = null;
}
}
public class MyselfTreeLinkedList<T>
{
//根節點
private MyselfTreeNode<T> _root;
//樹節點數量
private int _count;
//初始化樹根節點
public MyselfTreeLinkedList<T> InitRoot(T root)
{
_root = new MyselfTreeNode<T>(root);
//樹節點數量加1
_count++;
//返回樹
return this;
}
}
2、獲取樹節點數量 Count
樹節點數量可以透過私有欄位_count直接返回。
//獲取樹節點數量
public int Count
{
get
{
return _count;
}
}
3、獲取根節點 Root
根節點可以透過私有欄位_root直接返回。
//獲取根節點
public MyselfTreeNode<T> Root
{
get
{
return _root;
}
}
4、新增或更新左子節點值 AddOrUpdateLeftChild
該方法是透過節點新增或更新其左子節點值。
//透過指定節點新增或更新左子節點值
public void AddOrUpdateLeftChild(MyselfTreeNode<T> parent, T left)
{
if (parent.Left != null)
{
//更節點值
parent.Left.Data = left;
return;
}
//新增節點
parent.Left = new MyselfTreeNode<T>(left);
//節點數量加1
_count++;
}
5、新增或更新右子節點值 AddOrUpdateRightChild
該方法是透過節點新增或更新其右子節點值。
//透過指定節點新增或更新右子節點元素
public void AddOrUpdateRightChild(MyselfTreeNode<T> parent, T right)
{
if (parent.Right != null)
{
//更節點值
parent.Right.Data = right;
return;
}
//新增節點
parent.Right = new MyselfTreeNode<T>(right);
//節點數量加1
_count++;
}
6、刪除節點及其後代節點 Remove
該方法透過節點刪除其自身以及其所有後代節點,需要遞迴刪除左右子節點及其所有後代節點。
//透過指定節點刪除節點及其後代節點
public void Remove(MyselfTreeNode<T> node)
{
if (node.Left != null)
{
//遞迴刪除左子節點的所有後代
Remove(node.Left);
}
if (node.Right != null)
{
//遞迴刪除右子節點的所有後代
Remove(node.Right);
}
//刪除節點
node.Data = default;
//節點數量減1
_count--;
}
7、前序遍歷 PreOrderTraversal
核心思想和陣列實現一樣。
//前序遍歷
public void PreOrderTraversal(MyselfTreeNode<T> node)
{
//列印
Console.Write(node.Data);
if (node.Left != null)
{
//列印左子樹
PreOrderTraversal(node.Left);
}
if (node.Right != null)
{
//列印右子樹
PreOrderTraversal(node.Right);
}
}
8、中序遍歷 InOrderTraversal
核心思想和陣列實現一樣。
//中序遍歷
public void InOrderTraversal(MyselfTreeNode<T> node)
{
if (node.Left != null)
{
//列印左子樹
InOrderTraversal(node.Left);
}
//列印
Console.Write(node.Data);
if (node.Right != null)
{
//列印右子樹
InOrderTraversal(node.Right);
}
}
9、後序遍歷 PostOrderTraversal
核心思想和陣列實現一樣。
//後序遍歷
public void PostOrderTraversal(MyselfTreeNode<T> node)
{
if (node.Left != null)
{
//列印左子樹
PostOrderTraversal(node.Left);
}
if (node.Right != null)
{
//列印右子樹
PostOrderTraversal(node.Right);
}
//列印
Console.Write(node.Data);
}
10、層次遍歷 LevelOrderTraversal
核心思想和陣列實現一樣。
//層次遍歷
public void LevelOrderTraversal()
{
//建立一個佇列用於層次遍歷
Queue<MyselfTreeNode<T>> queue = new Queue<MyselfTreeNode<T>>();
//先把根節點入隊
queue.Enqueue(_root);
//只有佇列中有值就一直處理
while (queue.Count > 0)
{
//出列,取出第一個節點
var node = queue.Dequeue();
//列印第一個節點值
Console.Write(node.Data);
// 如果左子節點存在將其加入佇列
if (node.Left != null)
{
queue.Enqueue(node.Left);
}
// 如果右子節點存在將其加入佇列
if (node.Right != null)
{
queue.Enqueue(node.Right);
}
}
}
注:測試方法程式碼以及示例原始碼都已經上傳至程式碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner