1、树 Tree
1.1 概念
1.1.1 树
树是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n ≥ 0)个有限结点组成一个具有层次关系的集合。它具有以下的特点:
有且仅有一个没有父结点的节点,称为根结点;
每一个非根结点有且仅有一个父结点;
每个结点都只有有限个子结点或无子结点;
除了根结点外,每个子结点可以分为多个不相交的子树;
树里面没有环路(cycle)
1.1.2 结点
包含一个或多个数据项,以及指向其他结点的引用(通常是子结点)。结点可以包含额外的信息,如结点的深度、父结点的引用等,具体取决于树的实现和需求。
根结点(Root Node):树结构的顶层结点,它没有父结点,是树的起始点。
内部结点(Internal Node):除了根结点外的其他节点,它们至少有一个子结点。
叶结点(Leaf Node):没有子结点节点的节点,位于树结构的末端。
子结点(Child Node):某个结点直接连接的下一层级的结点。
父结点(Parent Node):某个节点直接连接的上一层级的结点。
结点的深度(Degree):拥有的子树数
degree = 0:叶结点或终端结点
degree != 0:非终端结点或分支结点
层次:从树根开始定义,根结点为第1层,它的子结点为第2层,以此类推。
深度:从根结点开始自顶向下逐层累加的。
高度:从叶结点开始自底向上逐层累加的。
树的度是树内各结点度的最大值
树的高度(或深度)是树中结点的最大层数。图中树的高度为4
1.2 存储结构
1.2.1 父结点表示法
用一组连续空间存储树的结点;
在每个结点,附设指示器指示器父结点在数组中的位置
1.2.2 孩子链表表示法
用多重链表表示,即每个结点有多个指针域,每个指针指向一颗子树的根结点
方案一:指针域的个数为树的的度
方案二:指针域的个数为该结点的度
方案三:
由一个顺序存储的数组和n个单链表组成,有两种结点结构:表头结点 & 孩子结点;
数组存储n个表头结点,其中包含树中每个结点的数据和其孩子链表的头指针;
单链表将每个结点的子结点排列存储起来。
查找某结点的兄弟:查找孩子链表
遍历整棵树:遍历头结点的数组
2、二叉树
2.1 定义
二叉树是一种特殊的树结构,其中每个节点最多有两个子结点,分别称为左子结点和右子结点。且左右结点有次序之分不可颠倒。
2.2 性质
1)二叉树的第i层上最多有个结点(i≥1);
2)深度为k的二叉树至多有 - 1 个结点;
3)对任何一棵二叉树T,如果其终端结点数为,度为1的结点数为,度为2的结点数为,树的结点总数为n,则:
n = + + ;
= + 1;
分支数 = n - 1 = + 2;
2.3 特殊类型
2.3.1 斜树
2.3.2 完美二叉树(Perfect Binary Tree)
A Perfect Binary Tree(PBT) is a tree with all leaf nodes at the same depth.All internal nodes have degree 2.
一棵深度为k,且有−1个节点的二叉树,称为完美二叉树。这种树的特点是每一层上的节点数都是最大节点数。
2.3.3 完全二叉树(Complete Binary Tree)
A Complete Binary Tree (CBT) is a binary tree in which every level, except possibly the last, is completely filled, and all nodes are as far left as possible.
在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干节点,则此二叉树为完全二叉树。具有n个节点的完全二叉树的深度为 + 1。深度为k的完全二叉树,至少有 个结点,至多有- 1个结点。
2.3.4 完满二叉树(Full Binary Tree)
A Full Binary Tree (FBT) is a tree in which every node other than the leaves has two children.
所有非叶结点的结点都有两个子结点
2.4 存储结构
2.4.1 顺序存储
用一维数组存储二叉树中的结点。
完全二叉树;一般二叉树;极端情况(深度为k的二叉树只有k个结点但分配了- 1个结点)
容易造成空间的浪费,顺序存储结构一般只用于完全二叉树
2.4.2 二叉链表存储
每个结点最多有两个孩子——一个数据域和两个指针域的二叉链表
结构示意图
结点结构定义代码(c#)
using System;
public class TreeNode
{
public int Data { get; set; }
public TreeNode Left { get; set; }
public TreeNode Right { get; set; }
public TreeNode(int data)
{
Data = data;
Left = null;
Right = null;
}
}
2.5 遍历
2.5.1 深度优先遍历(Depth-First Traversal)
1)前序遍历(Preorder Traversal)
递归
当前结点为空直接返回 —— 访问当前结点 —— 前序遍历左子树 —— 前序遍历右子树
public void PreorderTraversal(TreeNode root)
{
if (root != null)
{
Console.Write(root.Val + " "); // 访问根节点
PreorderTraversal(root.Left); // 递归遍历左子树
PreorderTraversal(root.Right); // 递归遍历右子树
}
}
迭代
迭代方法使用栈来模拟递归过程,利用栈保存节点的顺序,实现前序遍历。逻辑思路如下:
将根结点压入栈中。
循环执行以下操作,直到栈为空:
弹出栈顶结点,访问该节点。
如果该节点的右子结点不为空,则将右子节点压入栈中。
如果该节点的左子结点不为空,则将左子节点压入栈中。
public void PreorderTraversalIterative(TreeNode root)
{
if (root == null)
return;
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.Push(root);
while (stack.Count > 0)
{
TreeNode node = stack.Pop();
Console.Write(node.Data + " ");
if (node.Right != null)
stack.Push(node.Right);
if (node.Left != null)
stack.Push(node.Left);
}
}
2)中序遍历(Inorder Traversal)
递归
当前结点为空直接返回 —— 中序遍历左子树 —— 访问当前结点 —— 中序遍历右子树
public void InorderTraversalRecursive(TreeNode root)
{
if (root != null)
{
InorderTraversalRecursive(root.Left); // 递归遍历左子树
Console.Write(root.Data + " "); // 访问当前节点
InorderTraversalRecursive(root.Right); // 递归遍历右子树
}
}
迭代
迭代方法使用栈来模拟递归过程,利用栈保存节点的顺序,实现中序遍历。逻辑思路如下:
从根结点开始,将所有左子结点依次入栈,直到左子结点为空。
弹出栈顶结点,访问该结点。
将当前结点指向其右子结点,并重复步骤 1 和步骤 2。
public void InorderTraversalIterative(TreeNode root)
{
if (root == null)
return;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode current = root;
while (current != null || stack.Count > 0)
{
// 将当前节点及其左子节点依次入栈
while (current != null)
{
stack.Push(current);
current = current.Left;
}
// 弹出栈顶节点并访问
current = stack.Pop();
Console.Write(current.Data + " ");
// 将当前节点指向其右子节点
current = current.Right;
}
}
3)后序遍历(Postorder Traversal)
递归
当前结点为空直接返回 —— 后序遍历左子树 —— 后序遍历右子树 —— 访问当前结点
public void PostorderTraversalRecursive(TreeNode root)
{
if (root != null)
{
PostorderTraversalRecursive(root.Left); // 递归遍历左子树
PostorderTraversalRecursive(root.Right); // 递归遍历右子树
Console.Write(root.Data + " "); // 访问当前节点
}
}
迭代
使用栈来模拟递归过程,利用栈保存节点的顺序,实现后序遍历。逻辑思路如下:
将根节点压入栈中。
循环执行以下操作,直到栈为空:
弹出栈顶节点,访问该节点。
如果该节点的左子节点不为空,则将左子节点压入栈中。
如果该节点的右子节点不为空,则将右子节点压入栈中。
最后,逆序输出访问结果即可。
public void PostorderTraversalIterative(TreeNode root)
{
if (root == null)
return;
Stack<TreeNode> stack = new Stack<TreeNode>();
List<int> result = new List<int>();
stack.Push(root);
while (stack.Count > 0)
{
TreeNode node = stack.Pop();
result.Insert(0, node.Data); // 逆序插入结果列表
if (node.Left != null)
stack.Push(node.Left);
if (node.Right != null)
stack.Push(node.Right);
}
// 输出结果列表
Console.WriteLine("迭代后序遍历结果:");
foreach (int data in result)
{
Console.Write(data + " ");
}
}
2.5.2 广度优先遍历(Breadth-First Traversal)
层序遍历(Level Order Traversal)
从树的根结点开始逐层访问,先访问第一层的结点,再访问第二层的结点,依次类推,直到遍历完整棵树。层序遍历通常使用队列来实现,其基本思路如下:
创建一个空队列,并将根结点入队。
循环执行以下操作,直到队列为空:
弹出队列中的一个结点,访问该结点。
将弹出结点的左子结点(如果存在)入队。
将弹出结点的右子结点(如果存在)入队。
public void LevelOrderTraversal(TreeNode root)
{
// 如果根结点为空,则直接返回
if (root == null)
return;
// 创建一个队列,用于保存结点的顺序
Queue<TreeNode> queue = new Queue<TreeNode>();
// 将根结点入队
queue.Enqueue(root);
// 循环执行以下操作,直到队列为空
while (queue.Count > 0)
{
// 弹出队列中的一个结点
TreeNode node = queue.Dequeue();
// 访问当前结点
Console.Write(node.Data + " ");
// 如果当前节点存在左子结点,则将左子结点入队
if (node.Left != null)
queue.Enqueue(node.Left);
// 如果当前节点存在右子结点,则将右子结点入队
if (node.Right != null)
queue.Enqueue(node.Right);
}
}
2.6 构造
2.6.1 前序 + 中序
2.6.2 中序 + 后序
2.7 二叉搜索树 Binary Search Tree
2.7.1 二叉搜索树
1)概念
二叉搜索树(BST),也称为有序二叉树(Ordered binary tree)或排序二叉树(sorted binary tree)。其指一颗空树或具有下列性质的二叉树:
1、若任意结点的左子树不为空,则左子树上所有结点的值均小于它的根结点的值;
2、若任意结点的右子树不为空,则右子树上所有结点的值均大于它的根结点的值;
3、任意结点的左右子树也分别为二叉搜索树;
2)操作
搜索
从根结点开始,根据目标值与当前结点值的比较结果,递归查找左子树和右子树
public bool Search(TreeNode root, int target) {
if (root == null) {
return false; // 如果树为空,则返回 false
}
if (root.val == target) {
return true; // 如果当前节点值等于目标值,则返回 true
}
if (target < root.val) {
return Search(root.left, target); // 递归搜索左子树
} else {
return Search(root.right, target); // 递归搜索右子树
}
}
插入
从根节点开始,根据目标值与当前节点值的比较结果,递归地沿着左子树或右子树插入新节点,直到找到插入位置为止。
新插入的结点总为叶子结点
public TreeNode Insert(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val); // 如果树为空,则创建新节点作为根节点
}
if (val < root.val) {
root.left = Insert(root.left, val); // 递归插入左子树
} else {
root.right = Insert(root.right, val); // 递归插入右子树
}
return root;
}
删除
分类讨论:
1、删除结点为叶子结点:无左右子树,直接删除,父结点指针设为空
2、删除结点只有左/右子树:删除结点的父结点指向左/右子树
3、删除结点同时有左右子树:
找到待删除节点的右子树中的最小节点(或左子树中的最大节点),记为替代节点。
将替代节点的值赋给待删除节点。
删除此时的最小结点(满足1或2的情况)
public class Solution {
public TreeNode DeleteNode(TreeNode root, int key) {
if (root == null) return null;
if (key < root.val) {
root.left = DeleteNode(root.left, key);
} else if (key > root.val) {
root.right = DeleteNode(root.right, key);
} else { // Found the key
// Case 1: No child or one child
if (root.left == null) return root.right;
else if (root.right == null) return root.left;
// Case 2: Two children
TreeNode minNode = FindMin(root.right);
root.val = minNode.val;
root.right = DeleteNode(root.right, minNode.val);
}
return root;
}
private TreeNode FindMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node;
}
}
3)构造
4)性能
时间复杂度
平均情况:
对于随机构造的二叉搜索树,查找、插入和删除操作的平均时间复杂度为 O(log n),其中 n 是树中节点的数量。
最坏情况:
在最坏情况下,即当二叉搜索树退化为链表时,查找、插入和删除操作的时间复杂度会退化为 O(n)。因此,在实际应用中,需要注意避免二叉搜索树的不平衡情况。
空间复杂度
二叉搜索树的空间复杂度为 O(n),其中 n 是树中节点的数量。这是因为每个节点都需要额外的空间来存储节点本身的值以及左右子节点的引用。
一般的二叉查找树的查询复杂度取决于目标结点到树根的距离(即深度),因此当结点的深度普遍较大时,查询的均摊复杂度会上升。为了实现更高效的查询,产生了平衡树。
2.7.2 平衡树
AVL树(AVL tree)
树堆(Treap)
伸展树(Splay tree)
红黑树(Red-black tree)
加权平衡树(Weight balanced tree)
2-3树
AA树
替罪羊树
2.7.3 AVL树
1)概念
计算机科学中最早被发明的平衡二叉搜索树,也被称为高度平衡树
在AVL树中,任意结点的两课子树的最大高度差为1。
平衡因子 Balance Factor:
结点的左子树的高度 - 结点的右子树的高度
平衡因子的绝对不大于1的结点被认为是平衡的。其余则是不平衡的,需要重新平衡。
平衡因子可以直接存储在每个结点中。或根据可能存储在结点中的子树高度计算
public class AVLTree
{
public class TreeNode
{
public int value;
public int height;
public TreeNode left;
public TreeNode right;
public TreeNode(int val)
{
value = val;
height = 1;
left = null;
right = null;
}
}
}
2)操作
AVL树的基本操作一般涉及运作同在不平衡的二叉查找树所运作的同样的算法。但是要进行预先或随后做一次或多次所谓的"AVL旋转"。
3)旋转
右旋
LL型(左左型)不平衡:
在某结点的左子树的左子树中插入结点导致不平衡,用右旋操作来恢复平衡
右旋操作:
将不平衡结点的左子结点提升为新的结点,原根结点降为新根结点的右子结点;
- 找到不平衡的结点(记为A)以及其左子结点(记为B)。
- 将A的左子结点(B)提升为新的根结点
- 将A作为B的右子结点。
public TreeNode LLRotation(TreeNode root)
{
TreeNode newRoot = root.left;
root.left = newRoot.right;
newRoot.right = root;
// 更新节点高度
root.height = Math.Max(GetHeight(root.left), GetHeight(root.right)) + 1;
newRoot.height = Math.Max(GetHeight(newRoot.left), GetHeight(newRoot.right)) + 1;
return newRoot;
}
public int GetHeight(TreeNode node)
{
if (node == null)
return 0;
return node.height;
}
左旋
RR型(右右型)不平衡:
在某结点的右子树的右子树中插入结点导致不平衡,用左旋操作来恢复平衡。
左旋操作:
将不平衡结点的右子结点提升为新的根结点,原根结点降为新根结点的左子结点。
- 找到不平衡的结点(记为A)以及其右子结点(记为B)。
- 将A的右子结点(B)提升为新的根结点。
- 将A作为B的左子结点。
public TreeNode RRRotation(TreeNode root)
{
TreeNode newRoot = root.right;
root.right = newRoot.left;
newRoot.left = root;
// 更新节点高度
root.height = Math.Max(GetHeight(root.left), GetHeight(root.right)) + 1;
newRoot.height = Math.Max(GetHeight(newRoot.left), GetHeight(newRoot.right)) + 1;
return newRoot;
}
public int GetHeight(TreeNode node)
{
if (node == null)
return 0;
return node.height;
}
左右双旋
LR型(左右型)不平衡:
在某结点的左子树的右子树中插入结点导致不平衡,需要2次操作来恢复平衡,首先进行左旋转,然后进行右旋转。
左右双旋:
先对不平衡结点的左子树进行左旋转,再对不平衡结点进行右旋转。
- 找到不平衡的结点(记为A)以及其左子结点的右子结点(记为B)。
- 对A的左子结点(B)进行左旋转,得到临时根结点。
- 对A进行右旋转,将临时根结点提升为新的根结点。
public TreeNode LRRotation(TreeNode root)
{
root.left = RRRotation(root.left); // 左子树先进行右旋转
return LLRotation(root); // 再进行左旋转
}
public int GetHeight(TreeNode node)
{
if (node == null)
return 0;
return node.height;
}
右左双旋
RL型(右左型)不平衡:
在某结点的右子树的左子树中插入结点导致不平衡,需要2次操作来恢复平衡,首先进行右旋转,然后进行左旋转。
右左双旋:
先对不平衡结点的右子树进行右旋转,再对不平衡结点进行左旋转。
- 找到不平衡的结点(记为A)以及其右子结点的左子结点(记为B)。
- 对A的右子结点(B)进行右旋转,得到临时根结点。
- 对A进行左旋转,将临时根结点提升为新的根结点。
public TreeNode RLRotation(TreeNode root)
{
root.right = LLRotation(root.right); // 右子树先进行左旋转
return RRRotation(root); // 再进行右旋转
}
private int GetHeight(TreeNode node)
{
if (node == null)
return 0;
return node.height;
}