/* 二叉树节点类 */
class TreeNode {
int val; // 节点值
TreeNode left; // 左子节点引用
TreeNode right; // 右子节点引用
TreeNode(int x) {
val = x;
}
}
1.二叉树常见术语
2.二叉树的基本操作
1.初始化
// 初始化节点
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
2.插入和删除
3.常见二叉树类型
1).完美二叉树
完美二叉树(满二叉树) perfect binary tree」所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 0 ,其余所 有节点的度都为 2 ;若树高度为 ℎ ,则节点总数为 2 的h+1次方-1 ,呈现标准的指数级关系,反映了自然界中常 见的细胞分裂现象。
2).完全二叉树
3).完满二叉树
4). 平衡二叉树
4.二叉树退化
二叉树最佳与最差情况对比
完美二叉树 | 链表 | |
---|---|---|
第i层节点数量 | 2的i-1次方 | 1 |
高度
ℎ
树的叶节点数量
| 2的h次方 | 1 |
高度
ℎ
树的节点总数
| 2的h+1次方-1 | h+1 |
节点总数
𝑛
树的高度
|
log
2
(𝑛 + 1) − 1
| n-1 |
5.二叉树遍历
1).层序遍历
代码实现:
广度优先遍历通常借助“队列”来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进” 的规则,两者背后的思想是一致的。
public class Test1 {
public static void main(String[] args) {
// 初始化节点
TreeNode n1 = new TreeNode(1);
TreeNode n2 = new TreeNode(2);
TreeNode n3 = new TreeNode(3);
TreeNode n4 = new TreeNode(4);
TreeNode n5 = new TreeNode(5);
// 构建引用指向(即指针)
n1.left = n2;
n1.right = n3;
n2.left = n4;
n2.right = n5;
List<Integer> integers = levelOrder(n1);
System.out.println(integers);
}
/* 层序遍历 */
static List<Integer> levelOrder(TreeNode root) {
// 初始化队列,加入根节点
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
// 初始化一个列表,用于保存遍历序列
List<Integer> list = new ArrayList<>();
while (!queue.isEmpty()) {
TreeNode node = queue.poll(); // 队列出队
list.add(node.val); // 保存节点值
if (node.left != null)
queue.offer(node.left); // 左子节点入队
if (node.right != null)
queue.offer(node.right); // 右子节点入队
}
return list;
}
}
- 创建一个空队列,并将根节点入队。
- 进入循环,直到队列为空: a. 出队当前节点。 b. 处理当前节点。 c. 如果当前节点有左孩子,将左孩子入队。 d. 如果当前节点有右孩子,将右孩子入队。
2).前序、中序、后序遍历
相应地,前序、中序和后序遍历都属于「深度优先遍历 depth‑first traversal」,它体现了一种“先走到尽头,
再回溯继续”的遍历方式。下图展示了对二叉树进行深度优先遍历的工作原理。深度优先遍历就像是绕着整个二叉树的外围“走”一 圈,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。
代码:
/* 前序遍历 */
void preOrder(TreeNode root) {
if (root == null)return;
// 访问优先级:根节点 -> 左子树 -> 右子树
list.add(root.val);
preOrder(root.left);
preOrder(root.right);
}
/* 中序遍历 */
void inOrder(TreeNode root) {
if (root == null)return;
// 访问优先级:左子树 -> 根节点 -> 右子树
inOrder(root.left);
list.add(root.val);
inOrder(root.right);
}
/* 后序遍历 */
void postOrder(TreeNode root) {
if (root == null)return;
// 访问优先级:左子树 -> 右子树 -> 根节点
postOrder(root.left);
postOrder(root.right);
list.add(root.val);
}
6.二叉树的数组表示
1.完美二叉树
2.任意二叉树
/* 二叉树的数组表示 */// 使用 int 的包装类 Integer ,就可以使用 null 来标记空位Integer [] tree = { 1 , 2 , 3 , 4 , null, 6 , 7 , 8 , 9 , null, null, 12 , null, null, 15 };
package com.syctest.test1.test.model;
import java.util.ArrayList;
import java.util.List;
/* 数组表示下的二叉树类 */
public class ArrayBinaryTree {
private List<Integer> tree;
/* 构造方法 */
public ArrayBinaryTree(List<Integer> arr) {
tree = new ArrayList<>(arr);
}
/* 节点数量 */
public int size() {
return tree.size();
}
/* 获取索引为 i 节点的值 */
public Integer val(int i) {
// 若索引越界,则返回 null ,代表空位
if (i < 0 || i >= size())
return null;
return tree.get(i);
}
/* 获取索引为 i 节点的左子节点的索引 */
public Integer left(int i) {
return 2 * i + 1;
}
/* 获取索引为 i 节点的右子节点的索引 */
public Integer right(int i) {
return 2 * i + 2;
}
/* 获取索引为 i 节点的父节点的索引 */
public Integer parent(int i) {
return (i - 1) / 2;
}
/* 层序遍历 */
public List<Integer> levelOrder() {
List<Integer> res = new ArrayList<>();
// 直接遍历数组
for (int i = 0; i < size(); i++) {
if (val(i) != null)
res.add(val(i));
}
return res;
}
/* 深度优先遍历 */
private void dfs(Integer i, String order, List<Integer> res) {
// 若为空位,则返回
if (val(i) == null)
return;
// 前序遍历
if (order == "pre")
res.add(val(i));
dfs(left(i), order, res);
// 中序遍历
if (order == "in")
res.add(val(i));
dfs(right(i), order, res);
// 后序遍历
if (order == "post")
res.add(val(i));
}
/* 前序遍历 */
public List<Integer> preOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "pre", res);
return res;
}
/* 中序遍历 */
public List<Integer> inOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "in", res);
return res;
}
/* 后序遍历 */
public List<Integer> postOrder() {
List<Integer> res = new ArrayList<>();
dfs(0, "post", res);
return res;
}
}
优点:
- 空间利用率高: 数组不需要额外的指针来存储左右子节点的地址,因此相比链式结构,它在内存上的空间利用率更高。
- 访问效率高: 通过索引直接访问节点,不需要通过指针进行遍历。
-
‧ 允许随机访问节点。
二叉搜索树
操作:
1. 查找节点
代码:
/* 查找节点 */
TreeNode search(int num) {
TreeNode cur = root;
// 循环查找,越过叶节点后跳出
while (cur != null) {
// 目标节点在 cur 的右子树中
if (cur.val < num)
cur = cur.right;
// 目标节点在 cur 的左子树中
else if (cur.val > num)
cur = cur.left;
// 找到目标节点,跳出循环
else
break;
}
// 返回目标节点
return cur;
}
2.插入
在代码实现中,需要注意以下两点。
/* 插入节点 */
void insert(int num) {
// 若树为空,则初始化根节点
if (root == null) {
root = new TreeNode(num);
return;
}
TreeNode cur = root, pre = null;
// 循环查找,越过叶节点后跳出
while (cur != null) {
// 找到重复节点,直接返回
if (cur.val == num)
return;
pre = cur;
// 插入位置在 cur 的右子树中
if (cur.val < num)
cur = cur.right;
// 插入位置在 cur 的左子树中
else
cur = cur.left;
}
// 插入节点
TreeNode node = new TreeNode(num);
if (pre.val < num)
pre.right = node;
else
pre.left = node;
}
3.删除
void remove(int num) {
// 若树为空,直接提前返回
if (root == null) return;
//cur存储要删除的节点 pre存储要删除节点的父节点
TreeNode cur = root, pre = null;
// 通过循环查找,找到待删除的节点。在查找的过程中,通过比较当前节点的值与目标值的大小,决定是往左子树还是右子树移动。
while (cur != null) {
// 找到待删除节点,跳出循环
if (cur.val == num)
break;
pre = cur;//保存为要删除节点的父节点
// 待删除节点在 cur 的右子树中
if (cur.val < num)
cur = cur.right;
// 待删除节点在 cur 的左子树中
else
cur = cur.left;
}
// 若无待删除节点,则直接返回
if (cur == null) return;
// 如果找到了待删除的节点,分为两种情况处理:
// 如果待删除节点的子节点数量为0或1,直接删除该节点。如果是根节点,重新指定根节点;如果是非根节点,将其父节点指向待删除节点的子节点。
// 如果待删除节点的子节点数量为2,找到中序遍历中的下一个节点,递归删除该下一个节点,然后用该下一个节点的值覆盖待删除节点的值。
if (cur.left == null || cur.right == null) {//左右只要有一个为空,就是子节点数量为0或1
// 当子节点数量 = 0 / 1 时, child = null / 该子节点
TreeNode child = cur.left != null ? cur.left : cur.right;//取出要删除节点的不为空的那个子节点
// 删除节点 cur
if (cur != root) {//要删除节点不是根节点
if (pre.left == cur)//此时pre为要删除节点cur的父节点
pre.left = child;//要删除的是父节点的左边的,将要删除节点的子节点覆盖要删除的节点
else
pre.right = child;
} else {
// 若删除节点为根节点,则重新指定根节点
root = child;
}
} else {// 子节点数量 = 2
// 获取中序遍历中 cur 的下一个节点 中序遍历遵循“左 → 根 → 右”的遍历顺序
TreeNode tmp = cur.right;
while (tmp.left != null) {
tmp = tmp.left;
}
// 递归删除节点 tmp
remove(tmp.val);
// 用 tmp 覆盖 cur
cur.val = tmp.val;
}
}
4.中序遍历
4.二叉搜索树的效率
5.二叉搜索树的应用
-
数据库索引: 数据库管理系统中,索引常常使用二叉搜索树来实现。通过在数据库表的某一列上建立索引,可以加快对该列的搜索和排序操作。
-
符号表: 编译器和解释器中常用二叉搜索树来实现符号表,用于存储变量、函数等符号信息。
-
字典: 二叉搜索树可以用作字典,其中键-值对按照键的顺序进行排序,使得查找、插入和删除等操作都能在较快的时间内完成。
-
优先队列: 通过二叉搜索树实现的优先队列,可以在 O(log n) 的时间内实现插入和删除最大或最小元素的操作。
-
文件系统: 文件系统中的目录结构通常可以使用二叉搜索树来组织,以便更有效地进行文件搜索和访问。
-
网络路由表: 在计算机网络中,路由表的实现中也可以使用二叉搜索树,以加速对目标地址的查找操作。
-
自平衡二叉搜索树: AVL树、红黑树等自平衡二叉搜索树的实现在算法和数据结构中有重要应用,用于确保树的平衡性,以提高性能。
-
图形图像处理: 在图形学和图像处理中,二叉搜索树可以用于处理和搜索像素或图形数据。
-
数值范围查询: 二叉搜索树的有序性使得在某个数值范围内进行查询变得高效,因此在一些数值处理场景中也有应用。
总体而言,二叉搜索树的有序性和高效的搜索、插入、删除操作使其在各种应用场景中都能发挥重要作用。然而,需要注意的是,为了避免退化成链表的情况,有时候会使用平衡二叉搜索树,如AVL树、红黑树等。
AVL树
@Data
@AllArgsConstructor
public class TreeNode {
public int height; // 节点高度
public int val; // 节点值
public TreeNode left; // 左子节点引用
public TreeNode right; // 右子节点引用
public TreeNode(int x) {
val = x;
}
}
节点平衡因子
AVL树旋转
1.右旋
/* 右旋操作 */
TreeNode rightRotate(TreeNode node) {
TreeNode child = node.left;
TreeNode grandChild = child.right;
// 以 child 为原点,将 node 向右旋转
child.right = node;
node.left = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
2.左旋
/* 左旋操作 */
TreeNode leftRotate(TreeNode node) {
TreeNode child = node.right;
TreeNode grandChild = child.left;
// 以 child 为原点,将 node 向左旋转
child.left = node;
node.right = grandChild;
// 更新节点高度
updateHeight(node);
updateHeight(child);
// 返回旋转后子树的根节点
return child;
}
3. 先左旋后右旋
4. 先右旋后左旋
5.旋转的选择
/* 执行旋转操作,使该子树重新恢复平衡 */
TreeNode rotate(TreeNode node) {
// 获取节点 node 的平衡因子
int balanceFactor = balanceFactor(node);
// 左偏树
if (balanceFactor > 1) {
if (balanceFactor(node.left) >= 0) {
// 右旋
return rightRotate(node);
} else {
// 先左旋后右旋
node.left = leftRotate(node.left);
return rightRotate(node);
}
}
// 右偏树
if (balanceFactor < -1) {
if (balanceFactor(node.right) <= 0) {
// 左旋
return leftRotate(node);
} else {
// 先右旋后左旋
node.right = rightRotate(node.right);
return leftRotate(node);
}
}
// 平衡树,无须旋转,直接返回
return node;
}
AVL 树常用操作
1. 插入节点
/* 插入节点 */
// void insert(int val) {
// root = insertHelper(root, val);
// }
/* 递归插入节点(辅助方法) */
TreeNode insertHelper(TreeNode node, int val) {
if (node == null)
return new TreeNode(val);
/* 1. 查找插入位置,并插入节点 */
if (val < node.val)
node.left = insertHelper(node.left, val);
else if (val > node.val)
node.right = insertHelper(node.right, val);
else
return node; // 重复节点不插入,直接返回
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
2. 删除节点
/* 递归删除节点(辅助方法) */
TreeNode removeHelper(TreeNode node, int val) {
if (node == null)
return null;
/* 1. 查找节点,并删除之 */
if (val < node.val)
node.left = removeHelper(node.left, val);
else if (val > node.val)
node.right = removeHelper(node.right, val);
else {
if (node.left == null || node.right == null) {
TreeNode child = node.left != null ? node.left : node.right;
// 子节点数量 = 0 ,直接删除 node 并返回
if (child == null)
return null;
// 子节点数量 = 1 ,直接删除 node
else
node = child;
} else {
// 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点
TreeNode temp = node.right;
while (temp.left != null) {
temp = temp.left;
}
node.right = removeHelper(node.right, temp.val);
node.val = temp.val;
}
}
updateHeight(node); // 更新节点高度
/* 2. 执行旋转操作,使该子树重新恢复平衡 */
node = rotate(node);
// 返回子树的根节点
return node;
}
3. 查找节点
AVL 树典型应用
小结
‧ 二叉树是一种非线性数据结构,体现“一分为二”的分治逻辑。每个二叉树节点包含一个值以及两个指针,分别指向其左子节点和右子节点。‧ 对于二叉树中的某个节点,其左(右)子节点及其以下形成的树被称为该节点的左(右)子树。‧ 二叉树的相关术语包括根节点、叶节点、层、度、边、高度和深度等。‧ 二叉树的初始化、节点插入和节点删除操作与链表操作方法类似。‧ 常见的二叉树类型有完美二叉树、完全二叉树、完满二叉树和平衡二叉树。完美二叉树是最理想的状态,而链表是退化后的最差状态。‧ 二叉树可以用数组表示,方法是将节点值和空位按层序遍历顺序排列,并根据父节点与子节点之间的索引映射关系来实现指针。‧ 二叉树的层序遍历是一种广度优先搜索方法,它体现了“一圈一圈向外”的分层遍历方式,通常通过队列来实现。‧ 前序、中序、后序遍历皆属于深度优先搜索,它们体现了“走到尽头,再回头继续”的回溯遍历方式, 通常使用递归来实现。‧ 二叉搜索树是一种高效的元素查找数据结构,其查找、插入和删除操作的时间复杂度均为 𝑂( log 𝑛) 。当二叉搜索树退化为链表时,各项时间复杂度会劣化至 𝑂(𝑛) 。‧ AVL 树,也称为平衡二叉搜索树,它通过旋转操作,确保在不断插入和删除节点后,树仍然保持平衡。‧ AVL 树的旋转操作包括右旋、左旋、先右旋再左旋、先左旋再右旋。在插入或删除节点后,AVL 树会 从底向顶执行旋转操作,使树重新恢复平衡。