事先声明,本文不适合对数据结构完全不懂的小白 请至少学会链表再阅读
c# 数据结构 链表篇 有关单链表的一切_c# 链表-CSDN博客
数据结构理论先导:《数据结构(C 语言描述)》也许是全站最良心最通俗易懂最好看的数据结构课(最迟每周五更新~~)_哔哩哔哩_bilibili
代码随想录:
关于二叉树,你该了解这些!| 二叉树理论基础一网打尽,二叉树的种类、二叉树的存储方式、二叉树节点定义、二叉树的遍历顺序_哔哩哔哩_bilibili
目录
0.树基础概念
1.二叉树基础概念 BT B:Binary
编辑
2.普通二叉树存储/遍历/缺点
存储方式
顺序存储:
链式存储:
遍历方式
顺序存储遍历(递归)
链式存储遍历(递归)
插入方式及其缺点
链式存储插入
缺点
3.二叉搜索树 BST S:Search
链式构建及其增删查改
测试用例以及全部代码
4.平衡二叉树(AVL) BBT: B:Balanced
构建和插入
删除
5.二叉树的线索化
5.1设计线索化结构
5.2 线索化构建树
5.3 线索化后中序遍历方法举例
关于红黑树和b+树 下一篇数据结构 树篇 将会做出彻底详解
0.树基础概念
0和1是基础概念和一些性质 可以看个大概 知道有这么回事就行了 后面用到再回来想想是不是这么回事即可
叶子节点也算是子树
树的性质
解释:
结点的度 = 该节点下最大子树个数,也就是看有几个孩子
比如:A结点 有三个子树 即为三个孩子 ,B有两个孩子,K无孩子
解释:
还是这棵树 i =1开始
树的度 = 最大结点的度,也就是有最多孩子的结点的度
还是上面这棵树 最多孩子的度为A||D 所以 m = 3
第一层就是3的0次方 =1
第二层就是3的1次方 =3
第三层就是3的二次方 =9 (最大值)
1.二叉树基础概念 BT B:Binary
解释:
对于这个性质的解释:
2.普通二叉树存储/遍历/缺点
存储方式
顺序存储:
顺序存储需要注意的是让索引 i 从0
特点是:依据其索引的特点 通过线性结构存储起来
我用的是List(i=0开始)
public class Tree<T> : MonoBehaviour
{
private List<T> bitTree = new List<T>();
public void AddTree(T[] values)
{
for(int i = 0; i < values.Length; i++)
{
bitTree.Add(values[i]);
}
}
}
Tree<char> tree = new Tree<char>();
char[] values = { 'A', 'B', 'C', 'D' };
tree.AddTree(values);
链式存储:
特点为 =左右孩子指针 + 数据存储位
//节点暂时定义为char
public class TreeNode {
public char Date;
public TreeNode LeftNode;
public TreeNode RightNode;
public TreeNode(char Date, TreeNode LeftNode=null, TreeNode RightNode=null) {
this.Date = Date;
this.LeftNode =LeftNode;
this.RightNode = RightNode;
}
遍历方式
有三种分为前中后序
前序特点:
就是先当前节点(如果从根节点开始),再左节点再右节点
中序特点:
先左,再当前,最后右
后序特点:
先左,再右,最后当前
顺序存储遍历(递归)
以前序为例 先构造树
再做递归
//顺序遍历二叉树
//前序
public void Preorder(int index)
{
if (index >= bitTree.Count)
return;
Debug.Log(bitTree[index]);
int LeftNode = 2*index+1;
int RightNode = 2*index+2;
Preorder(LeftNode);
Preorder(RightNode);
}
//中序
public void MiddleOrder(int index)
{
if (index >= bitTree.Count)
return;
int LeftNode = 2 * index + 1;
int RightNode = 2 * index + 2;
MiddleOrder(LeftNode);
Debug.Log(bitTree[index]);
MiddleOrder(RightNode);
}
//后序
public void AfterOrder(int index) {
if (index >= bitTree.Count)
return;
int LeftNode = 2 * index + 1;
int RightNode = 2 * index + 2;
AfterOrder(LeftNode);
AfterOrder(RightNode);
Debug.Log(bitTree[index]);
}
链式存储遍历(递归)
还是以前序遍历为例:
TreeNode rootNode = new TreeNode('A',
new TreeNode('B',new TreeNode('D'), new TreeNode('E')),
new TreeNode('C', new TreeNode('F')));
rootNode.FirstRoot(rootNode);
Console.WriteLine();
rootNode.MiddleRoot(rootNode);
Console.WriteLine();
rootNode.AfterRoot(rootNode);
//节点暂时定义为char
public class TreeNode {
public char Date;
public TreeNode LeftNode;
public TreeNode RightNode;
public TreeNode(char Date, TreeNode LeftNode=null, TreeNode RightNode=null) {
this.Date = Date;
this.LeftNode =LeftNode;
this.RightNode = RightNode;
}
//遍历方法 前
public void FirstRoot(TreeNode treeNode)
{
if (treeNode == null) return;
Console.Write(treeNode.Date);
FirstRoot(treeNode.LeftNode);
FirstRoot(treeNode.RightNode);
}
//遍历方法 中
public void MiddleRoot(TreeNode treeNode)
{
if (treeNode == null) return;
Console.Write(treeNode.Date);
MiddleRoot(treeNode.LeftNode);
MiddleRoot(treeNode.RightNode);
} //遍历方法 后
public void AfterRoot(TreeNode treeNode)
{
if (treeNode == null) return;
AfterRoot(treeNode.LeftNode);
AfterRoot(treeNode.RightNode);
Console.Write(treeNode.Date);
}
}
插入方式及其缺点
链式存储插入
我想给原来的二叉树的 2 6之间插入一个4
TreeNode root = new TreeNode(1);
root.LeftNode = new TreeNode(2);
root.RightNode = new TreeNode(3);
root.LeftNode.LeftNode = new TreeNode(6);
root.LeftNode.RightNode = new TreeNode(7);
//节点暂时定义为char
public class TreeNode
{
public int Date;
public TreeNode LeftNode;
public TreeNode RightNode;
public TreeNode(int Date, TreeNode LeftNode = null, TreeNode RightNode = null)
{
this.Date = Date;
this.LeftNode = LeftNode;
this.RightNode = RightNode;
}
我需要做的
// 创建新节点
TreeNode newNode = new TreeNode(4);
// 在节点 2 的左子节点位置插入新节点 4
BinaryTree.InsertNode(root.LeftNode, true, newNode);
public class BinaryTree
{
/// <summary>
/// 值插入
/// </summary>
/// <param name="parent"></param>
/// <param name="isLeft">是否是左子节点</param>
/// <param name="newNode"></param>
public static void InsertNode(TreeNode parent, bool isLeft, TreeNode newNode)
{
if (isLeft)
{
newNode.LeftNode = parent.LeftNode;
parent.LeftNode = newNode;
}
else
{
newNode.RightNode = parent.RightNode;
parent.RightNode = newNode;
}
}
}
缺点
发现了没有和他娘的链表似的:
无法利用 “值的顺序” 进行高效操作
比如搜索某个值(如查找 4
)时,只能从根节点开始遍历整个树(如广度优先或深度优先搜索),时间复杂度为 O(n),和链表无异
插入逻辑不通用:插入节点 4
到节点 2
的左子节点,依赖于手动指定父子关系,没有统一的规则,如果树结构复杂,插入位置的选择会非常随意,导致树的高度不平衡(如退化成链表)
于是有了下面这个玩意 二叉搜索树
3.二叉搜索树 BST S:Search
数据结构合集 - 二叉搜索树(二叉排序树)(二叉查找树)_哔哩哔哩_bilibili
特点 左子树Data小于根Data 右子树Data 大于根Data
操作 | 最好时间复杂度 | 最坏时间复杂度 |
---|---|---|
查找(Search) | O(log n) | O(n) |
插入(Insert) | O(log n) | O(n) |
删除(Delete) | O(log n) | O(n) |
空间复杂度 | O(n) | {O (n)(存储所有节点,与树的形态无关)} |
存储方式还是有链式和顺序 我倾向于链式 因为比较简单和清晰 所以拿此举例
链式构建及其增删查改
构建
public class TreeNode {
public int value;
public TreeNode Left;
public TreeNode Right;
public TreeNode(int value, TreeNode left = null, TreeNode right = null) {
this.value = value;
this.Left = left;
this.Right = right;
}
}
public class BinarySearchTree
{
public TreeNode root;
public BinarySearchTree()
{
root = null;
}
增加
//增加节点
public void Insert(int value) {
InsertTrueMethod(root, value);
}
/// <summary>
/// 递归查找插入位置
/// </summary>
/// <param name="node">插入的节点</param>
/// <param name="val">插入的值</param>
private TreeNode InsertTrueMethod(TreeNode node, int value)
{
if (node == null)
{
return new TreeNode(value);
}
//如果插入值 小于 节点值,则在左子树中插入
if (value < node.value)
{
node.Left = InsertTrueMethod(node.Left, value);
}
//如果插入值 大于 节点值,则在右子树中插入
else if (value > node.value)
{
node.Right = InsertTrueMethod(node.Right, value);
}
//如果找到了相同的值,则不插入
return node;
}
删除
private TreeNode DeleteTrueMethod(TreeNode node, int value)
{
//如果树为空,则直接返回空
if (node == null)
{
return null;
}
//如果删除值 小于 节点值,则在左子树找
if (value < node.value)
{
node.Left = DeleteTrueMethod(node.Left, value);
}
//如果删除值 大于 节点值,则在右子树
else if (value > node.value)
{
node.Right = DeleteTrueMethod(node.Right, value);
}
//如果找到了相同的值,则删除该节点
else
{
//如果该节点没有子节点,则直接删除该节点
if (node.Left == null && node.Right == null)
{
node = null;
}
//如果该节点只有一个子节点,则直接用该节点的子节点替换该节点
else if (node.Left == null)
{
node = node.Right;
}
else if (node.Right == null)
{
node = node.Left;
}
// 如果该节点有两个子节点,此时有两种常见的替换策略:
// 可以选择该节点右子树中的最小节点,也可以选择左子树中的最大节点。
// 这里我们采用选择右子树中最小节点的策略。选择右子树最小节点(或者左子树最大节点)的目的是
// 为了保证替换后仍然满足二叉搜索树的性质:左子树所有节点值小于根节点值,右子树所有节点值大于根节点值。
// 步骤为:先找到右子树的最小节点,用其值替换当前要删除节点的值,
// 然后再递归地从右子树中删除这个最小节点。
else
{
TreeNode minNode = GetMinNode(node.Right);
node.value = minNode.value;
node.Right = DeleteTrueMethod(node.Right, minNode.value);
}
}
return node;
}
//获取节点方法
private TreeNode GetMinNode(TreeNode node)
{
while (node.Left != null)
{
node = node.Left;
}
return node;
}
测试用例以及全部代码
注释进行了简单的修改
using System;
BinarySearchTree bst = new BinarySearchTree();
// 测试用例 1:删除叶子节点
int[] values1 = { 50, 30, 20, 40, 70, 60, 80 };
foreach (var val in values1) bst.Insert(val);
//Console.WriteLine("删除前(用例1):");
//bst.InOrderTraversal(bst.root); // 输出:20 30 40 50 60 70 80
//bst.Delete(20);
//Console.WriteLine("删除后(用例1-叶子节点):");
//bst.InOrderTraversal(bst.root); // 预期:30 40 50 60 70 80
测试用例 2:删除单子节点
//BinarySearchTree bst2 = new BinarySearchTree();
//int[] values2 = { 50, 30, 40 };
//foreach (var val in values2) bst2.Insert(val);
//bst2.Delete(30);
//Console.WriteLine("删除后(用例2-单子节点):");
//bst2.InOrderTraversal(bst2.root); // 预期:40 50
测试用例 3:删除双子节点
BinarySearchTree bst3 = new BinarySearchTree();
int[] values3 = { 50, 30, 20, 40, 70, 60, 80 };
foreach (var val in values3) bst3.Insert(val);
bst3.Delete(50);
Console.WriteLine("删除后(用例3-双子节点):");
bst3.InOrderTraversal(bst3.root); // 预期:20 30 40 60 70 80
public class TreeNode
{
public int value;
public TreeNode Left;
public TreeNode Right;
public TreeNode(int value, TreeNode left = null, TreeNode right = null)
{
this.value = value;
this.Left = left;
this.Right = right;
}
}
public class BinarySearchTree
{
public TreeNode root;
public BinarySearchTree()
{
root = null;
}
// 增加节点
public void Insert(int value)
{
root = InsertTrueMethod(root, value);
}
private TreeNode InsertTrueMethod(TreeNode node, int value)
{
if (node == null)
return new TreeNode(value);
if (value < node.value)
node.Left = InsertTrueMethod(node.Left, value);
else if (value > node.value)
node.Right = InsertTrueMethod(node.Right, value);
return node;
}
public void Delete(int value)
{
root = DeleteTrueMethod(root, value);
}
private TreeNode DeleteTrueMethod(TreeNode node, int value)
{
//先找到待删除节点
if (node == null) return null;
if (value < node.value)
node.Left = DeleteTrueMethod(node.Left, value);
else if (value > node.value)
node.Right = DeleteTrueMethod(node.Right, value);
else
{
//找到以后分为三种情况
//1. 叶子节点
if (node.Left == null && node.Right == null)
node = null;
//2. 单子节点
else if (node.Left == null)
node = node.Right;
else if (node.Right == null)
node = node.Left;
else
{
//3. 双子节点 找到右子树最小节点(或者左子树最大节点 替换待删除节点 然后递归删除右子树最小节点, 或者左子树最大节点)
TreeNode minNode = GetRightMinNode(node.Right);
node.value = minNode.value;
node.Right = DeleteTrueMethod(node.Right, minNode.value);
}
}
return node;
}
private TreeNode GetRightMinNode(TreeNode node)
{
while (node.Left != null) node = node.Left;
return node;
}
public void InOrderTraversal(TreeNode node)
{
if (node == null) return;
InOrderTraversal(node.Left);
Console.Write(node.value + " ");
InOrderTraversal(node.Right);
if (node == root) Console.WriteLine();
}
}
但是还是有一个问题 如果数据本来就有序 那构建二叉搜索树会成为一条线
所以为了解决这个问题 有了下面这个平衡二叉树
4.平衡二叉树(AVL) BBT: B:Balanced
建议直接看视频 平衡二叉树(AVL树)_哔哩哔哩_bilibili
这个东西是比较抽象的 而且情况也比较多 下图来自 b站 @帕拉迪克
平衡因子:Balanced Factor
上代码
构建和插入
using System;
//测试结果
AVLTree tree = new AVLTree();
int[] values = { 5,3,4};
foreach (int value in values) {
tree.Insert(value);
}
tree.InOrderTraversal(tree.root);
// 定义 AVL 树节点类
public class TreeNode
{
public int Value;
public TreeNode Left;
public TreeNode Right;
public int Height;
public TreeNode(int value)
{
Value = value;
Height = 1; // 新节点初始高度为1
}
}
public class AVLTree
{
public TreeNode root;
//获取传入节点的高度
private int GetHight(TreeNode node) {
return node == null? 0 : node.Height;
}
//计算平衡因子 让传入节点的左子树 - 右子树高度
private int ComputeBalanceFactor(TreeNode node) {
if(node == null ) return 0;
int BalanceFactor = GetHight(node.Left) - GetHight(node.Right);
return BalanceFactor;
}
//右旋 右旋父节点,然后将冲突的右放在旋转后的父节点的左子树
private TreeNode RightRotate(TreeNode father) {
//找到左子树节点和新插入节点
TreeNode leftChild = father.Left;
TreeNode conflict = leftChild.Right;
//旋转
leftChild.Right = father; //原父节点弄到左子树的右边
father.Left = conflict; //冲突的弄到原父节点的左边
//更新高度 先更新一下原来的父节点的高度 再更新一下现在的父节点的高度
father.Height = Math.Max(GetHight(father.Left), GetHight(father.Right)) + 1;
leftChild.Height = Math.Max(GetHight(leftChild.Left), GetHight(leftChild.Right)) + 1;
//返回旋转后的父节点
return leftChild;
}
//左旋 左旋父节点,然后将冲突的左放在旋转后的父节点的右子树
private TreeNode LeftRotate(TreeNode father)
{
//找到右子树节点和新插入节点
TreeNode rightChild = father.Right;
TreeNode conflict = rightChild.Left;
//旋转
rightChild.Left = father; //原父节点弄到右子树的左边
father.Right = conflict; //冲突的弄到原父节点的右边
//更新高度 先更新一下原来的父节点的高度 再更新一下现在的父节点的高度
father.Height = Math.Max(GetHight(father.Left), GetHight(father.Right)) + 1;
rightChild.Height = Math.Max(GetHight(rightChild.Left), GetHight(rightChild.Right)) + 1;
//返回旋转后的父节点
return rightChild;
}
public void Insert(int value)
{
root = InsertTrueMethod(root, value);
}
private TreeNode InsertTrueMethod(TreeNode root, int value)
{
//BST插入 空创建 左右递归插入 重复值不插入
if (root == null)
return new TreeNode(value);
if (value < root.Value)
root.Left = InsertTrueMethod(root.Left, value); //递归+更新+平衡
else if (value > root.Value)
root.Right = InsertTrueMethod(root.Right, value);//递归+更新+平衡
else
return root;
//更新高度
root.Height = Math.Max(GetHight(root.Left), GetHight(root.Right)) + 1;
//计算平衡因子
int balanceFactor = ComputeBalanceFactor(root);
//根据平衡因子判断怎么转
//LL型 右旋 value < root.Left.Value:验证新节点确实插入在左子树的左侧
if (balanceFactor > 1 && value < root.Left.Value)
return RightRotate(root);
//RR型 左旋
if (balanceFactor < -1 && value > root.Right.Value)
return LeftRotate(root);
//LR型 先左旋再右旋
if (balanceFactor > 1 && value > root.Left.Value)
{
root.Left = LeftRotate(root.Left);
return RightRotate(root);
}
//RL型 先右旋再左旋
if (balanceFactor < -1 && value < root.Right.Value)
{
root.Right = RightRotate(root.Right);
return LeftRotate(root);
}
//不用旋返回根节点
return root;
}
//中序遍历
public void InOrderTraversal(TreeNode root)
{
if (root == null)
return;
InOrderTraversal(root.Left);
Console.WriteLine(root.Value);
InOrderTraversal(root.Right);
}
}
删除
// 删除指定值的节点
public void Delete(int value)
{
root = DeleteNode(root, value);
}
private TreeNode DeleteNode(TreeNode root, int value)
{
// 如果根节点为空,直接返回 null
if (root == null)
return root;
// 如果要删除的值小于当前节点的值,递归删除左子树中的节点
if (value < root.Value)
root.Left = DeleteNode(root.Left, value);
// 如果要删除的值大于当前节点的值,递归删除右子树中的节点
else if (value > root.Value)
root.Right = DeleteNode(root.Right, value);
// 找到要删除的节点
else
{
// 情况 1: 节点没有子节点或只有一个子节点
if (root.Left == null || root.Right == null)
{
TreeNode temp = root.Left ?? root.Right;
// 如果没有子节点,直接删除该节点
if (temp == null)
{
root = null;
}
else
{
// 用子节点替换当前节点
root = temp;
}
}
// 情况 2: 节点有两个子节点
else
{
// 找到右子树中的最小节点
TreeNode temp = MinValueNode(root.Right);
// 用最小节点的值替换当前节点的值
root.Value = temp.Value;
// 递归删除右子树中的最小节点
root.Right = DeleteNode(root.Right, temp.Value);
}
}
// 如果删除后树为空,直接返回 null
if (root == null)
return root;
// 更新节点高度
root.Height = Math.Max(GetHight(root.Left), GetHight(root.Right)) + 1;
// 计算平衡因子
int balanceFactor = ComputeBalanceFactor(root);
// LL 型失衡,进行右旋操作
if (balanceFactor > 1 && ComputeBalanceFactor(root.Left) >= 0)
return RightRotate(root);
// LR 型失衡,先对左子树进行左旋,再对根节点进行右旋
if (balanceFactor > 1 && ComputeBalanceFactor(root.Left) < 0)
{
root.Left = LeftRotate(root.Left);
return RightRotate(root);
}
// RR 型失衡,进行左旋操作
if (balanceFactor < -1 && ComputeBalanceFactor(root.Right) <= 0)
return LeftRotate(root);
// RL 型失衡,先对右子树进行右旋,再对根节点进行左旋
if (balanceFactor < -1 && ComputeBalanceFactor(root.Right) > 0)
{
root.Right = RightRotate(root.Right);
return LeftRotate(root);
}
return root;
}
// 找到以给定节点为根的子树中的最小节点
private TreeNode MinValueNode(TreeNode node)
{
TreeNode current = node;
// 不断向左遍历,直到找到最左边的节点
while (current.Left != null)
current = current.Left;
return current;
}
看起来有点难度实际上熟悉以后你会发现其编写都是套路 多读几遍就好了
递归递归递归,你会发现二叉树一直在做这个动作 那么有没有一种方式脱离它呢?
有的兄弟 有的 :线索化二叉树
5.二叉树的线索化
线索化二叉树的设计思想是:
将空的指针(Left
或 Right
)利用起来,不再指向子树(因为子树为空),而是指向该节点在中序遍历中的前驱或后继节点
这样做可以在后续遍历中,不依赖递归或栈,直接通过线索找到前后节点
画图,逻辑思想
下为代码解释
5.1设计线索化结构
其中LeftIsThread 和 RightIsThread 是指当前Left和Right是否为空
public class ThreadedNode<T>
{
public T Data { get; set; }
public ThreadedNode<T> Left { get; set; }
public ThreadedNode<T> Right { get; set; }
// 标志位:false表示指向子节点,true表示线索
public bool LeftIsThread { get; set; }
public bool RightIsThread { get; set; }
public ThreadedNode(T data)
{
this.Data = data;
this.Left = null;
this.Right = null;
this.LeftIsThread = false;
this.RightIsThread = false;
}
}
5.2 线索化构建树
public class ThreadedBinaryTree<T>
{
private ThreadedNode<T> _root;
private ThreadedNode<T> _pre; // 记录前驱节点
// 线索化核心逻辑(中序)
private void ThreadNodes(ThreadedNode<T> node)
{
if (node == null) return;
// 递归线索化左子树
ThreadNodes(node.Left);
// 处理当前节点的前驱
if (node.Left == null)
{
node.Left = _pre;
node.LeftIsThread = true; // 标记为线索
}
// 处理前驱节点的后继
if (_pre != null && _pre.Right == null)
{
_pre.Right = node;
_pre.RightIsThread = true;
}
_pre = node; // 更新前驱
// 递归线索化右子树
ThreadNodes(node.Right);
}
public void BuildThreadedTree(ThreadedNode<T> root)
{
_root = root;
_pre = null;
ThreadNodes(root);
}
}
5.3 线索化后中序遍历方法举例
// 中序遍历(利用线索)
public void InOrderTraversal()
{
ThreadedNode<T> current = _root;
while (current != null)
{
// 找到最左节点
while (!current.LeftIsThread)
{
current = current.Left;
}
Console.Write(current.Data + " ");
// 根据后继线索遍历
while (current.RightIsThread)
{
current = current.Right;
Console.Write(current.Data + " ");
}
current = current.Right;
}