資料結構 - 樹,三探之程式碼實現

IT规划师發表於2024-10-23

書接上回,今天和大家一起動手來自己實現樹。

相信透過前面的章節學習,大家已經明白樹是什麼了,今天我們主要針對二叉樹,分別使用順序儲存和鏈式儲存來實現樹。

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

相關文章