树 (Tree)
树是 n( n≥0 ) 个节点的有限集合。
当 n=0 时,称为空树。
当 n>0 时,满足以下条件:
- 有且仅有一个特定的称为根(Root)的节点。
- 除根节点之外的其余节点被分为 m ( m≥0 ) 个互不相交的集合 T1,T2,...,Tm 。
- 每一个集合 Ti 本身又是一棵树,被称为根的子树(Subtree)。
相关术语
| 术语 | 定义 | 形象理解 |
|---|---|---|
| 根节点 | 树中唯一没有父节点的节点,位于最顶层。 | 家族的始祖 / 公司的CEO |
| 父/子节点 | 通过边连接的上下级节点。 | 父母与孩子 |
| 兄弟节点 | 拥有同一个父节点的节点。 | 亲兄弟姐妹 |
| 叶子节点 | 没有子节点的节点(度为0)。 | 树梢的叶子 / 基层员工 |
| 节点的度 | 一个节点拥有的子树(子节点)的个数。 | 一个人生了几个孩子 |
| 树的度 | 树内所有节点中,度最大的那个值。 | 家族里生孩子最多的人生了几个 |
| 深度/高度 | 深度:从根到该节点的层数。 高度:从该节点到最远叶子节点的层数。 | 楼层数 |
| 子树 | 以某个节点的子节点为根,包含其所有后代的树。 | 家族的一个分支 |
树的分类
树形数据结构
二叉树
Binary Tree
多路树
Multi-way Tree
堆
Heap
其他
Others
满二叉树
Full Binary Tree
完全二叉树
Complete Binary Tree
二叉查找树
Binary Search Tree
平衡二叉查找树
Self-balancing BST
AVL树
Adelson-Velsky and Landis Tree
红黑树
Red-Black Tree
B树
B-Tree
B+树
B+ Tree
2-3树
2-3 Tree
2-3-4树
2-3-4 Tree
小顶堆
Min Heap
大顶堆
Max Heap
优先级队列
Priority Queue
斐波那契堆
Fibonacci Heap
二项堆
Binomial Heap
树状数组
Binary Indexed Tree
线段树
Segment Tree
二叉树
二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节 点。注意,这里是最多有2个,也可能只有1个,或者没有孩子节点
满二叉树
一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就 是满二叉树。
完全二叉树
对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同 样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
二叉查找树
二叉查找树(binary search tree),二叉查找树在二叉树的基础上增加了以下几个条件:
- 如果左子树不为空,则左子树上所有节点的值均小于根节点的值
- 如果右子树不为空,则右子树上所有节点的值均大于根节点的值
- 左、右子树也都是二叉查找树
二叉查找树要求左子树小于父节点,右子树大于父节点,正是这样保证了二叉树的有序性。 因此二叉查找树还有另一个名字------二叉排序树(binary sort tree)。
平衡二叉查找树
平衡二叉查找树(Balanced Binary Search Tree,简称 BBST)是一种"自我修正"的二叉查找树。
1、红黑树
除了二叉查找树(BST)的特征外,还有以下特征:
- 每个节点要么是黑色,要么是红色
- 根节点是黑色
- 每个叶子节点都是黑色的空结点(NIL结点)(为了简单期间,一般会省略该节点)
- 如果一个节点是红色的,则它的子节点必须是黑色的(父子不能同为红)
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点(平衡的关键)
- 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行平衡
在对红黑树进行添加或者删除操作时可能会破坏这些特点,所以红黑树采取了很多方式来维护这些特点,从而维持平衡。主要包括:左旋转、右旋转和颜色反转
- 左旋(RotateLeft):逆时针旋转红黑树的两个结点,使得父结点被自己的右孩子取代,而自己成为自己的左孩子
- 右旋(RotateRight):顺时针旋转红黑树的两个结点,使得父结点被自己的左孩子取代,而自己成为自己的右孩子
- 颜色反转:就是当前节点与父节点、叔叔节点同为红色,这种情况违反了红黑树的规则,需要将红色向祖辈上传, 父节点和叔叔节点红色变为黑色,爷爷节点从黑色变为红色(爷爷节点必为黑色,因为此前是符合红黑 树规则的)。这样每条叶子结点到根节点的黑色节点数量并未发生变化,因此都其他树结构不产生影响
插入场景列举:
场景一:叔叔节点为红色
1、颜色变换:将父节点和叔叔节点都变为黑色。将祖父节点变为红色。
2、递归检查:将当前关注点移动到祖父节点,然后重复检查。如果祖父节点是根节点,则将其最终染黑;如果祖父节点的父节点也是红色,则继续递归处理。
场景二:叔叔节点为黑色(或不存在)
LL型 (左-左):新节点是父节点的 左孩子,父节点是祖父节点的左孩子。
- 变色:将父节点变为黑色,祖父节点变为红色。
- 右旋:以祖父节点为支点进行右旋。
RR型 (右-右):新节点是父节点的 右孩子,父节点是祖父节点的右孩子。
- 变色:将父节点变为黑色,祖父节点变为红色。
- 左旋:以祖父节点为支点进行左旋。
LR型 (左-右):新节点是父节点的 右孩子,父节点是祖父节点的左孩子。
这是一个双旋操作,目的是先将其转换为LL型。
- 左旋:以父节点为支点进行左旋。此时,新节点成为父节点,原父节点成为其左孩子,结构变为LL型。
- 按LL型处理:将新的父节点(即原新节点)变为黑色,祖父节点变为红色,然后以祖父节点为支点进行右旋。
RL型 (右-左):新节点是父节点的 左孩子,父节点是祖父节点的右孩子。
这也是一个双旋操作,目的是先将其转换为RR型。
- 右旋:以父节点为支点进行右旋。此时,新节点成为父节点,原父节点成为其右孩子,结构变为RR型。
- 按RR型处理:将新的父节点(即原新节点)变为黑色,祖父节点变为红色,然后以祖父节点为支点进行左旋。
删除场景列举:
场景一:兄弟节点为红色
被删除节点的兄弟节点是红色。
- 变色:将兄弟节点变为黑色,父节点变为红色。
- 左旋:以父节点为支点进行左旋。
- 转换:此操作后,会转换为下面兄弟节点为黑色的场景之一。
场景二:兄弟节点为黑色,且其两个子节点都为黑色
兄弟节点是黑色,且它的左右孩子也都是黑色(或为NIL)。
- 变色:将兄弟节点变为红色。
- 上移:将"双重黑色"的负担传递给父节点,即把父节点当作新的被删除节点,然后自底向上递归处理。
场景三:兄弟节点为黑色,其右子节点为红色
兄弟节点是黑色,且它的右孩子是红色(左孩子颜色任意)。
- 变色:将兄弟节点的颜色设为父节点的颜色,父节点变为黑色,兄弟节点的右子节点变为黑色。
- 左旋:以父节点为支点进行左旋。
- 完成:此操作后,"双重黑色"被消除,树恢复平衡。
场景四:兄弟节点为黑色,其左子节点为红色,右子节点为黑色
- 变色:将兄弟节点变为红色,其左子节点变为黑色。
- 右旋:以兄弟节点为支点进行右旋。
- 转换:此操作后,会转换为上面的"兄弟节点为黑色,其右子节点为红色"的场景,然后按该场景处理。
示例代码:
java
public class RedBlackTree<T extends Comparable<T>> {
// 定义节点颜色常量
private static final boolean RED = true;
private static final boolean BLACK = false;
// 定义节点类
private class Node {
T key; // 键
boolean color; // 颜色 (RED or BLACK)
Node left; // 左子节点
Node right; // 右子节点
public Node(T key, boolean color) {
this.key = key;
this.color = color;
}
}
private Node root; // 根节点
// ==========================================
// 1. 基础旋转操作
// ==========================================
/**
* 左旋 (Left Rotate)
* 场景:右子树过重,或者插入时出现"右左"情况的第一步
*
* h (旋转点) x (新的根)
* / \ / \
* A x ----> h C
* / \ / \
* B C A B
*/
private Node rotateLeft(Node h) {
Node x = h.right; // 1. 获取右子节点
h.right = x.left; // 2. 将 x 的左子树挂到 h 的右边
x.left = h; // 3. 将 h 挂到 x 的左边
x.color = h.color; // 4. 保持颜色一致(新根继承原根颜色)
h.color = RED; // 5. 原根变为红色(为了后续平衡)
return x; // 6. 返回新的根节点
}
/**
* 右旋 (Right Rotate)
* 场景:左子树过重,或者插入时出现"左右"情况的第一步
*
* h (旋转点) x (新的根)
* / \ / \
* x C ----> A h
* / \ / \
* A B B C
*/
private Node rotateRight(Node h) {
Node x = h.left; // 1. 获取左子节点
h.left = x.right; // 2. 将 x 的右子树挂到 h 的左边
x.right = h; // 3. 将 h 挂到 x 的右边
x.color = h.color; // 4. 保持颜色一致
h.color = RED; // 5. 原根变为红色
return x; // 6. 返回新的根节点
}
/**
* 颜色翻转 (Flip Colors)
* 作用:将节点的左右子节点变黑,自身变红。
* 通常用于处理插入节点时,左右子节点都为红色的情况(4-节点拆分)。
*/
private void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
// ==========================================
// 2. 插入操作与修复
// ==========================================
/**
* 对外暴露的插入方法
*/
public void insert(T key) {
root = insert(root, key);
// 根节点永远保持黑色
root.color = BLACK;
}
/**
* 内部递归插入方法
* 逻辑:先按二叉搜索树插入,再通过旋转和变色修复
*/
private Node insert(Node h, T key) {
// 1. 标准二叉搜索树插入,新节点默认为红色
if (h == null) return new Node(key, RED);
int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = insert(h.left, key);
else if (cmp > 0) h.right = insert(h.right, key);
else h.key = key; // 如果键已存在,更新值(此处简化为只更新键)
// 2. 红黑树修复逻辑 (自底向上)
// 情况1:右子节点是红色,左子节点是黑色 -> 左旋
// 目的:将红色的右节点转到左边,便于后续处理
if (isRed(h.right) && !isRed(h.left)) {
h = rotateLeft(h);
}
// 情况2:左子节点是红色,且左子节点的左孩子也是红色 -> 右旋
// 目的:处理连续两个红节点(左左情况)
if (isRed(h.left) && isRed(h.left.left)) {
h = rotateRight(h);
}
// 情况3:左右子节点都是红色 -> 颜色翻转
// 目的:模拟4-节点的拆分,将红色向上传递
if (isRed(h.left) && isRed(h.right)) {
flipColors(h);
}
return h;
}
// 辅助方法:判断节点是否为红色
private boolean isRed(Node h) {
if (h == null) return false;
return h.color == RED;
}
// ==========================================
// 3. 遍历测试
// ==========================================
/**
* 中序遍历 (In-order Traversal)
* 结果应该是有序的
*/
public void inorderTraversal() {
inorder(root);
System.out.println();
}
private void inorder(Node h) {
if (h == null) return;
inorder(h.left);
System.out.print(h.key + (h.color == RED ? "(R) " : "(B) "));
inorder(h.right);
}
// ==========================================
// 4. 主函数测试
// ==========================================
public static void main(String[] args) {
RedBlackTree<Integer> tree = new RedBlackTree<>();
// 测试数据:故意构造一些会导致不平衡的顺序
int[] keys = {40, 20, 60, 10, 30, 50, 70, 5, 15, 25, 35};
System.out.println("开始插入数据...");
for (int key : keys) {
tree.insert(key);
}
System.out.println("中序遍历结果 (格式: 值(颜色)):");
tree.inorderTraversal();
// 验证查找
System.out.println("查找 30: " + (tree.root != null)); // 简单验证
}
}
遍历方式
| 遍历方式 | 访问顺序 (根-左-右) | 核心特点 | 典型应用场景 |
|---|---|---|---|
| 前序遍历 | 根 → 左 → 右 | 根节点最先被处理 | 复制二叉树、生成前缀表达式、树的序列化 |
| 中序遍历 | 左 → 根 → 右 | 根节点在中间被处理 | 二叉搜索树(BST)的升序输出、验证BST合法性 |
| 后序遍历 | 左 → 右 → 根 | 根节点最后被处理 | 删除二叉树、计算目录大小、生成后缀表达式 |
| 层序遍历 | 逐层从左到右 | 按深度层级访问 | 求树的宽度、按层处理数据、寻找最短路径 |
深度优先搜索 (DFS)
1、前序遍历
- 逻辑:先访问根节点,再递归访问左子树,最后递归访问右子树。
- 示例序列 :
[1, 2, 4, 5, 3, 6, 7](假设根为1)
2、中序遍历
- 逻辑:先递归访问左子树,再访问根节点,最后递归访问右子树。
- 关键点 :对于二叉搜索树,中序遍历的结果一定是有序的(从小到大)。
3、后序遍历
- 逻辑:先递归访问左子树,再递归访问右子树,最后访问根节点。
- 关键点:适合处理"必须先处理完孩子,才能处理父亲"的场景(如计算文件夹大小)。
广度优先搜索 (BFS) / 层序遍历
BFS 的核心思想是"层层递进",即从上到下、从左到右逐层访问节点。
实现方式 :通常借助队列来实现。
- 将根节点入队。
- 当队列不为空时,取出队首节点并访问。
- 如果该节点有左/右孩子,依次将它们入队。
示例序列 :[1, 2, 3, 4, 5, 6, 7]
示例代码
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Stack;
// 1. 定义二叉树节点类
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
public class BinaryTreeTraversal {
/**
* ================= 递归实现 =================
*/
// 1. 前序遍历 (根 -> 左 -> 右)
public List<Integer> preorderRecursive(TreeNode root) {
List<Integer> result = new ArrayList<>();
dfsPre(root, result);
return result;
}
private void dfsPre(TreeNode node, List<Integer> result) {
if (node == null) return;
result.add(node.val); // 访问根
dfsPre(node.left, result); // 遍历左
dfsPre(node.right, result); // 遍历右
}
// 2. 中序遍历 (左 -> 根 -> 右)
public List<Integer> inorderRecursive(TreeNode root) {
List<Integer> result = new ArrayList<>();
dfsIn(root, result);
return result;
}
private void dfsIn(TreeNode node, List<Integer> result) {
if (node == null) return;
dfsIn(node.left, result); // 遍历左
result.add(node.val); // 访问根
dfsIn(node.right, result); // 遍历右
}
// 3. 后序遍历 (左 -> 右 -> 根)
public List<Integer> postorderRecursive(TreeNode root) {
List<Integer> result = new ArrayList<>();
dfsPost(root, result);
return result;
}
private void dfsPost(TreeNode node, List<Integer> result) {
if (node == null) return;
dfsPost(node.left, result); // 遍历左
dfsPost(node.right, result); // 遍历右
result.add(node.val); // 访问根
}
/**
* ================= 非递归实现 (迭代) =================
*/
// 4. 前序遍历 (使用栈)
// 逻辑:根入栈 -> 弹出根 -> 右入栈 -> 左入栈 (保证左先出)
public List<Integer> preorderIterative(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
result.add(node.val);
// 先压右,再压左,这样弹出的时候就是先左后右
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
return result;
}
// 5. 中序遍历 (使用栈)
// 逻辑:一直向左走压栈 -> 弹栈访问 -> 转向右
public List<Integer> inorderIterative(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
// 1. 一路向左,压入所有左节点
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
// 2. 弹出栈顶(最左节点)并访问
curr = stack.pop();
result.add(curr.val);
// 3. 转向右子树
curr = curr.right;
}
return result;
}
// 6. 后序遍历 (使用双栈法 - 较简单理解)
// 逻辑:利用前序遍历的变种 (根->右->左),然后反转结果
public List<Integer> postorderIterative(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Stack<TreeNode> stack1 = new Stack<>();
Stack<Integer> stack2 = new Stack<>(); // 用于反转顺序
stack1.push(root);
while (!stack1.isEmpty()) {
TreeNode node = stack1.pop();
stack2.push(node.val); // 存入结果栈
// 先左后右,这样 stack2 弹出时就是 左->右->根
if (node.left != null) stack1.push(node.left);
if (node.right != null) stack1.push(node.right);
}
while (!stack2.isEmpty()) {
result.add(stack2.pop());
}
return result;
}
/**
* ================= 层序遍历 (BFS) =================
*/
// 7. 层序遍历 (使用队列)
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size(); // 当前层的节点数
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(currentLevel);
}
return result;
}
// ================= 主函数测试 =================
public static void main(String[] args) {
// 构建测试树:
// 1
// / \
// 2 3
// / \ \
// 4 5 6
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.right = new TreeNode(6);
BinaryTreeTraversal solver = new BinaryTreeTraversal();
System.out.println("1. 前序遍历 (递归): " + solver.preorderRecursive(root));
System.out.println("2. 中序遍历 (递归): " + solver.inorderRecursive(root));
System.out.println("3. 后序遍历 (递归): " + solver.postorderRecursive(root));
System.out.println("4. 前序遍历 (迭代): " + solver.preorderIterative(root));
System.out.println("5. 中序遍历 (迭代): " + solver.inorderIterative(root));
System.out.println("6. 后序遍历 (迭代): " + solver.postorderIterative(root));
System.out.println("7. 层序遍历: " + solver.levelOrder(root));
}
}
运行结果:
html
1. 前序遍历 (递归): [1, 2, 4, 5, 3, 6]
2. 中序遍历 (递归): [4, 2, 5, 1, 3, 6]
3. 后序遍历 (递归): [4, 5, 2, 6, 3, 1]
4. 前序遍历 (迭代): [1, 2, 4, 5, 3, 6]
5. 中序遍历 (迭代): [4, 2, 5, 1, 3, 6]
6. 后序遍历 (迭代): [4, 5, 2, 6, 3, 1]
7. 层序遍历: [[1], [2, 3], [4, 5, 6]]
多路查找树
多路查找树(muitl-way search tree),其每一个节点的孩子数可以多于两个,且每一个节点处可以存 储多个元素。
B树
B树(BalanceTree)是对二叉查找树的改进。它的设计思想是,将相关数据尽量集中在一起,以便一 次读取多个数据,减少硬盘操作次数。
一棵m阶的B 树 (m叉树)的特性如下:
- B树中所有节点的孩子节点数中的最大值称为B树的阶,记为M
- 树中的每个节点至多有M棵子树 ---即:如果定了M,则这个B树中任何节点的子节点数量都不能超 过M
- 若根节点不是终端节点,则至少有两棵子树
- 除根节点和叶节点外,所有点至少有m/2棵子树
- 所有的叶子结点都位于同一层
B+树
B+树是B-树的变体,也是一种多路搜索树,其定义基本与B树相同,它的自身特征是:
非叶子结点的子树指针与关键字个数相同
非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树
为所有叶子结点增加一个链指针
所有关键字都在叶子结点出现
对比
| 对比维度 | B树 (B-Tree) | B+树 (B+Tree) |
|---|---|---|
| 数据存储位置 | 所有节点(内部节点和叶子节点)都存储数据。 | 只有叶子节点存储数据,内部节点仅作索引。 |
| 叶子节点结构 | 叶子节点之间相互独立,没有连接。 | 叶子节点通过双向链表连接,形成有序序列。 |
| 范围查询效率 | 较低。需要中序遍历整个树结构,涉及在不同层级间回溯,I/O路径复杂。 | 极高。找到范围的起始点后,只需沿着叶子节点的链表顺序扫描即可。 |
| 查询性能稳定性 | 不稳定。查询可能在任意层级命中,导致不同查询的I/O次数不同。 | 稳定。所有查询都必须到达叶子节点,I/O次数固定为树高。 |
| 磁盘I/O效率 | 相对较低。节点存储数据,占用空间大,导致单个节点能存储的索引键较少,树更高。 | 更高。内部节点只存索引,更"瘦",单个节点能容纳更多键,树更矮,减少了I/O次数。 |
二叉堆
二叉堆本质上是一种完全二叉树,它分为两个类型:
大顶堆(最大堆)
最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值
小顶堆(最小堆)
最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值
二叉堆的根节点叫作堆顶 最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素
存储原理
完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要 存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。
二叉堆的典型应用
优先队列
利用堆求 Top K问题
在一个包含 n 个数据的数组中,我们可以维护一个大小为 K 的小顶堆,顺序遍历数组,从数组中取出数 据与堆顶元素比较。如果比堆顶元素大,我们就把堆顶元素删除,并且将这个元素插入到堆中;如果比 堆顶元素小,则不做处理,继续遍历数组。这样等数组中的数据都遍历完之后,堆中的数据就是前 K 大 数据了