首页 > 其他分享 >数据结构 - 树,三探之代码实现

数据结构 - 树,三探之代码实现

时间:2024-10-25 11:18:10浏览次数:9  
标签:左子 index 数据结构 代码 右子 public 索引 三探 节点

数据结构 - 树,三探之代码实现

 本文介绍了使用数组和链表两种方式实现二叉树,包括初始化、节点操作(如获取、添加、删除)、以及遍历方法(前序、中序、后序、层次遍历)。测试代码已上传至代码库。 

书接上回,今天和大家一起动手来自己实现树。

相信通过前面的章节学习,大家已经明白树是什么了,今天我们主要针对二叉树,分别使用顺序存储和链式存储来实现树。

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);

    }

    //打印

    Co

nsole.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);

        }

标签:左子,index,数据结构,代码,右子,public,索引,三探,节点
From: https://blog.csdn.net/weixin_55010563/article/details/143173592

相关文章

  • 低代码开发平台有哪些功能
    低代码开发平台具备多种功能,主要包括:一、可视化开发界面;二、预置组件库;三、自动化代码生成;四、集成开发和部署工具;五、自定义业务逻辑;六、移动应用支持。其中,可视化开发界面使开发过程更直观,无需深入编码,通过拖放操作即可构建用户界面,提高开发效率。一、可视化开发界面低代码......
  • 代码随想录算法训练营第24天(补第13天)|226.翻转二叉树, 101. 对称二叉树,104.二叉树的最
    226.翻转二叉树文章链接:https://programmercarl.com/0226.翻转二叉树.html#算法公开课题目链接:https://leetcode.cn/problems/invert-binary-tree/description/迭代法:这里使用了前序遍历来交换左右孩子节点classSolution{public:TreeNode*invertTree(TreeNode*r......
  • 代码大全读后感2
    在阅读《代码大全2》的前四分之一部分时,我深刻体会到了代码质量对软件开发的重要性。书中首先阐述了软件构建的核心思想,强调了编写代码不仅仅是让它运行,而是要让它易于理解、维护和扩展。书中提到的编程原理,如“深思熟虑的设计”和“代码简洁”,让我更加认识到代码不仅仅是供机器......
  • 基于对称点模式(symmetric dot pattern)的多元数据融合-matlab代码
        引言受最近深度学习在计算机视觉和语音识别方面的成功启发,许多研究者提出将一维时间序列数据编码为不同类型的图像,这样可以放大数据中的动态特性,更好地表征原数据。基于对称点模式(symmetricdotpattern)的多元数据融合对称点模式(SymmetrizedDotPattern,SDP)算法可......
  • 【MATLAB代码】EKF和CDKF的对比
    目录主要特点应用场景运行结果展示本MATLAB程序实现了扩展卡尔曼滤波(EKF)与协方差差分卡尔曼滤波(CDKF)在三维状态估计中的效果对比,为需要高精度定位与动态系统分析的用户提供了一种实用工具。通过直观的结果展示,您可以轻松比较两种滤波算法的性能。主要特点多算法对比:......
  • 【Unity】OnGUI 代码生成UI
    ​GUI.Box盒子GUI.Button按钮GUI.RepeatButton 按住会触发的按钮GUI.Label标签文本GUI.TextField 单行文本框GUI.TextArea 多行文本框GUI.Toggle 单选radioGUI.Toolbar 单选tabGUI.SelectionGrid 单选可以表格布局GUI.HorizontalSlider 滑动条水平方向GU......
  • ECharts饼图-环形图,附视频讲解与代码下载
    引言: 在数据可视化的世界里,ECharts凭借其丰富的图表类型和强大的配置能力,成为了众多开发者的首选。今天,我将带大家一起实现一个饼图图表,通过该图表我们可以直观地展示和分析数据。此外,我还将提供详细的视频讲解和代码下载链接,帮助大家快速上手。一、图表效果预览 二、视......
  • 数据结构 - 堆
    今天我们将学习新的数据结构-堆。01、定义堆是一种特殊的二叉树,并且满足以下两个特性:(1)堆是一棵完全二叉树;(2)堆中任意一个节点元素值都小于等于(或大于等于)左右子树中所有节点元素值;小根堆,根节点元素永远是最小值,即堆中每个节点元素值都小于等于左右子树中所有节点元素值;大根......
  • (神经网络和卷积入门)Pytorch小土堆跟练代码(第7天)
    本系列为跟练小土堆每集代码,然后进入李宏毅机器学习教程。在系列中会敲完所有视频中代码,并且在注释详细写出感悟和易错点。欢迎大家一起交流!最前面的软件安装和环境配置部分,可以移步我的另一个帖子一、神经网络'主要在torch.nn里''首先学的是骨架container''Module,所......
  • 代码随想录算法训练营day25| 491.递增子序列 46.全排列 47.全排列2
    学习资料:https://programmercarl.com/0491.递增子序列.html#算法公开课排列与组合的区别,不用startIndex,而每个树层都从0开始,但是要跳过已经用过的数(用used判断)学习记录:491.递增子序列(添加一个数组used(hash表),来保持数组每个位置上的数的使用情况,没用过为0,用过变成1)点击查看代......