数据结构之树学习笔记
一.树中的节点关系和一些概念
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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!