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

news/2024/10/23 1:47:11/文章来源:https://www.cnblogs.com/hugogoos/p/18494233

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/819474.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不限次数无广告的短网址生成工具推荐

无论是在社交媒体、营销推广还是日常分享,短网址都能让我们更加便捷地分享内容。然而,在众多的短网址生成工具中,用户常常会遇到一些令人头疼的问题:跳转次数限制、插入广告等等。本文将为大家推荐一款不限次数且无广告的短网址生成工具——小码短链接,并详细介绍其优势。…

使用文件重定向

在Linux终端使用了下文件重定向在试的时候命令比较混乱,再重新捋一下:首先使用vim编辑器创建一个名为test.cpp的文件。 具体内容如下:然后使用g++ -o test test.cpp命令编译生成可执行文件test。接着使用vim编辑器创建了输入文件data.txt 具体内容如下:再运行命令./test &l…

The 2022 ICPC Asia Xian Regional Contest 前六题

VP一场,成都赛前找手感,这次还不戳,前三题略讲The 2022 ICPC Asia Xian Regional Contest 签到题题解 CFJ J. Strange Sum 易证最多只能选两个,从大到小排序后 \(\max(0, a_1) + \max(0, a_2)\) 即为所求。 void solve(){cin>>n;vector<ll>a(n+1);for(int i=1;…

利用数组处理批量数据

数组是一组有序数据的集合。数组中各数据的排列有一定规律,下标代表数据在数组中的序号 用一个数组名和下标来唯一的确定数组中的元素 数组中的每一个元素都属于同一个数据类型。不能把不同类型的数据放在同一个数组中 将数组和循环结合起来,可以有效的处理大批量的数据 怎样…

执行yum install 的时候提示【没有可用的软件包】的解决方案

这种情况,可能是yum 源不正确的问题,解决方案如下: 1.执行cd /etc/yum.repos.d,进入这个目录下,查看文件是否存在并检查文件内容的正确性 2、CentOS-Base.repo文件可以在网上下载一个,以下是范文# CentOS-Base.repo # # The mirror system uses the connecting IP addres…

MySQL安装-Linux系统

MySQL安装-Linux系统本文在此只介绍一种安装方式,其他安装方法可以查阅其他相关资料。 一、准备工作 1、下载MySQL社区版 官方网站:https://www.mysql.com/ ,找到下载DOWNLOADS,下载操作系统对应的社区版本。本文使用的数据库版本是5.7.41选择对应的MySQL版本和系统以及系统…

newc++file.cpp在哪

本人的newc++file.cpp文件在C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\VC\VCProjectItems可以在这个cpp文件里面自己选择是否写#define _CRT_SECURE_NO_WARNINGS 如果写了,则在visual studio中新建的cpp文件都有这个这个预处理命令主要是为…

Android13冻结进程分析:如何提高设备性能和用户体验

本文介绍了Android13中的冻结进程功能,它是一种重要的资源管理策略,可以提高系统性能和稳定性,同时最大限度地节省设备的资源和电池消耗。 文章讨论了如何合理分配资源,包括CPU、内存等,以提高设备性能和用户体验。此外,文章还提到了冻结进程对应用程序线程的影响,并介绍…

一图总结sql语言的最常用知识

一, 五大类sql语言DDL Data Definition Language, 数据定义语言,用于定义不同的数据字段、数据库、表、列、索引。如:create、drop、alter等DML Data Manipulation Language,数据操作语言,用于添加、删除、修改、查询数据的完整性。如:insert、 update 、 delete 等DQL Data…

10/22二叉树 求度为1的结点个数

include using namespace std; typedef struct BiNode { char data; struct BiNode* lchild, * rchild; }BiTNode, * BiTree; void CreateBiTree(BiTree& T)//创建一个二叉树 { char ch; cin >> ch; if (ch == #) T = NULL; else { T = new BiTNode; T->data = c…

初识封装

1.理解:“高内聚,低耦合” 高内聚即是说在内部繁琐的代码细节都由我们自己一人完成,包装起来,不让他人看见。而低耦合则是给用户一些较低的权限去使用软件。 2.铭记:属性私有,get/set 3.private:用于私有属性,与public形成反差,私有后的属性无法被随意调用。 如图: 4…

软件工程团队作业

需求规格说明书 0. 目录需求规格说明书0. 目录 1. 引言1.1 目的 1.2 背景 1.3 定义 1.4 参考文献2. 项目概述2.1 产品背景 2.2 产品描述 2.3 产品功能 2.4 未来市场2.5 应用目标与作用范围2.6 用户场景 2.7 假设与约束2.7.1 假设 2.7.2 约束3. 具体需求3.1 外部接口需求3.1.1 用…