好的,我们来详细探讨您图片中提到的主题:二叉搜索树 (Binary Search Tree, BST),并对其进行性能分析。这部分内容是理解更高级的二叉平衡树 (Balanced Binary Tree) 的基石。
我将按照以下结构为您深入讲解,并提供带有逐行详细注释的 Java 代码实现。
-
引言:从二叉树到二叉搜索树
-
为什么需要二叉搜索树?
-
-
1.1 二叉搜索树的概念
-
严格定义与核心性质
-
图解示例
-
-
1.2 二叉搜索树的核心操作(查找、插入、删除)
-
查找 (Search) 操作详解与代码实现
-
插入 (Insert) 操作详解与代码实现
-
删除 (Delete) 操作详解与代码实现
-
-
1.3 二叉搜索树性能分析
-
最好情况:完全平衡
-
最坏情况:退化成链表
-
平均情况
-
引出“二叉平衡树”的必要性
-
-
完整的 Java 实现与演示
1. 引言:从二叉树到二叉搜索树
普通的二叉树对其节点值的存放没有任何规则,这意味着查找一个特定值的节点时,我们可能需要遍历整棵树,其效率与遍历一个链表或无序数组无异,时间复杂度为 O(n)。
为了提高查找效率,二叉搜索树 (Binary Search Tree, BST) 应运而生。它通过对节点值的存放施加特定的规则,使得查找、插入和删除等操作的效率可以大大提升。
2. (1.1) 二叉搜索树的概念
定义与性质
二叉搜索树(也称为二叉排序树)是一种特殊的二叉树,它或者是一棵空树,或者具有以下性质:
-
左子树性质:若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值。
-
右子树性质:若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值。
-
递归性质:任意节点的左、右子树也分别为二叉搜索树。
-
唯一性:通常情况下,二叉搜索树不允许存在键值相同的节点。
图解示例
下面是一个合法的二叉搜索树:
8
/ \
3 10
/ \ \
1 6 14
/ \
4 7
-
根节点是
8
。 -
其左子树的所有节点 (
1
,3
,4
,6
,7
) 都小于8
。 -
其右子树的所有节点 (
10
,14
) 都大于8
。 -
这个规则对树中的每一个节点(如
3
,10
等)都成立。
3. (1.2) 二叉搜索树的核心操作
我们将通过代码详细解释 BST 的三大核心操作:查找、插入和删除。
查找 (Search) 操作
查找操作充分利用了 BST 的性质,过程非常高效。
算法思想
-
从根节点开始比较。
-
如果当前节点的值等于目标值,则查找成功。
-
如果目标值小于当前节点的值,则在当前节点的左子树中继续查找。
-
如果目标值大于当前节点的值,则在当前节点的右子树中继续查找。
-
如果遇到空节点(
null
),则说明树中不存在该目标值,查找失败。
Java 代码实现 (递归)
Java
// 节点类
class Node {
int key; // 节点存储的键值
Node left; // 左子节点
Node right; // 右子节点
public Node(int item) {
key = item;
left = right = null;
}
}
// 二叉搜索树类
class BinarySearchTree {
Node root; // 树的根节点
// ... (其他方法)
/**
* 公开的查找方法,供外部调用
* @param key 要查找的键值
* @return 如果找到,返回包含该键值的节点;否则返回 null
*/
public Node search(int key) {
// 从根节点开始递归查找
return searchRec(root, key);
}
/**
* 递归实现的查找辅助方法
* @param current 当前正在访问的节点
* @param key 要查找的键值
* @return 找到的节点或 null
*/
private Node searchRec(Node current, int key) {
// 基本情况 1: 如果当前节点为空,或者找到了键值,则返回当前节点
if (current == null || current.key == key) {
return current;
}
// 如果要查找的 key 小于当前节点的 key,则往左子树递归
if (key < current.key) {
return searchRec(current.left, key);
}
// 否则 (key > current.key),往右子树递归
return searchRec(current.right, key);
}
}
插入 (Insert) 操作
插入操作与查找类似,首先找到新节点应该被插入的位置,然后将新节点连接上去。
算法思想
-
从根节点开始,像查找操作一样向下遍历。
-
如果待插入的值小于当前节点的值,则走向左子树;如果大于,则走向右子树。
-
持续这个过程,直到遇到一个空链接(
null
),这个位置就是新节点的插入点。 -
创建一个新节点并将其链接到该位置。
Java 代码实现 (递归)
Java
class BinarySearchTree {
// ... (root 和 search 方法)
/**
* 公开的插入方法,供外部调用
* @param key 要插入的键值
*/
public void insert(int key) {
// 调用递归辅助方法,并更新根节点 (因为树可能原本是空的)
root = insertRec(root, key);
}
/**
* 递归实现的插入辅助方法
* @param current 当前正在访问的节点
* @param key 要插入的键值
* @return 返回插入新节点后的子树的根
*/
private Node insertRec(Node current, int key) {
// 如果当前子树为空,说明找到了插入位置,创建新节点并返回
if (current == null) {
return new Node(key);
}
// 根据 BST 的性质,决定向左还是向右递归
if (key < current.key) {
// 递归地在左子树中插入,并将返回的新左子树链接到当前节点
current.left = insertRec(current.left, key);
} else if (key > current.key) {
// 递归地在右子树中插入,并将返回的新右子树链接到当前节点
current.right = insertRec(current.right, key);
}
// 如果 key == current.key,则什么也不做 (不允许重复键)
// 返回未修改的(或已修改子链接的)当前节点指针
return current;
}
}
删除 (Delete) 操作
删除是 BST 中最复杂的操作,需要分三种情况讨论:
-
要删除的节点是叶子节点 (没有子节点):直接将其父节点的对应链接(左或右)设为
null
。 -
要删除的节点只有一个子节点:将其父节点直接链接到它的那个唯一子节点上,跳过被删除的节点。
-
要删除的节点有两个子节点:
-
这是最复杂的情况。我们不能直接删除它,因为会破坏树的结构。
-
策略:在它的右子树中找到最小的节点(这个节点被称为中序后继节点)。
-
将这个中序后继节点的值,复制到要删除的节点上。
-
然后,递归地删除那个中序后继节点。由于中序后继节点是其子树中最小的,它一定没有左子节点,因此删除它会转化为前面更简单的 Case 1 或 Case 2。
-
Java 代码实现 (递归)
Java
class BinarySearchTree {
// ... (其他方法)
/**
* 公开的删除方法
* @param key 要删除的键值
*/
public void delete(int key) {
root = deleteRec(root, key);
}
/**
* 递归实现的删除辅助方法
* @param current 当前节点
* @param key 要删除的键值
* @return 返回删除节点后的子树的根
*/
private Node deleteRec(Node current, int key) {
// 如果树为空,直接返回
if (current == null) {
return null;
}
// 1. 寻找要删除的节点
if (key < current.key) {
// 要删除的节点在左子树
current.left = deleteRec(current.left, key);
} else if (key > current.key) {
// 要删除的节点在右子树
current.right = deleteRec(current.right, key);
} else {
// 找到了要删除的节点 (current.key == key)
// 情况 1: 节点没有子节点或只有一个子节点
if (current.left == null) {
return current.right; // 如果右子节点也为 null,则返回 null (叶子节点)
} else if (current.right == null) {
return current.left;
}
// 情况 2: 节点有两个子节点
// 找到右子树中的最小节点 (中序后继)
int successorValue = findMinValue(current.right);
// 将中序后继的值复制到当前节点
current.key = successorValue;
// 从右子树中删除那个中序后继节点
current.right = deleteRec(current.right, successorValue);
}
return current;
}
/**
* 辅助方法:在给定的子树中查找最小值
* @param node 子树的根节点
* @return 该子树的最小值
*/
private int findMinValue(Node node) {
// 根据 BST 性质,最小值一定在最左边的叶子节点
while (node.left != null) {
node = node.left;
}
return node.key;
}
}
4. (1.3) 二叉搜索树性能分析
BST 的性能完全取决于它的高度 (Height)。树的高度是从根到最深叶子节点的路径长度。所有核心操作(查找、插入、删除)的时间复杂度都与树的高度成正比,即 O(h)。
最好情况
当树是完全平衡的,或接近完全平衡时,性能最好。在这种情况下, n 个节点的树,其高度 h 约为 log_2n。
-
形态:树的左右子树节点数大致相等,结构非常茂盛、匀称。
-
时间复杂度:
-
查找:O(logn)
-
插入:O(logn)
-
删除:O(logn)
-
最坏情况
当插入的元素序列是有序的(例如,依次插入 1, 2, 3, 4, 5)或逆序的,BST 会退化 (degenerate) 成一个链表。
-
形态:树变成一条单边的长链,所有节点都只有左子节点或右子节点。
-
高度:树的高度 h 约等于节点数 n。
-
时间复杂度:
-
查找:O(n)
-
插入:O(n)
-
删除:O(n)
-
这种情况下,BST 的性能优势荡然无存,和普通链表没有区别。
平均情况
当插入的元素是随机的,不刻意构造有序序列时,树的高度期望值是 O(logn)。因此,在大多数实际应用中,BST 的平均性能是很好的。
-
时间复杂度 (平均):
-
查找:O(logn)
-
插入:O(logn)
-
删除:O(logn)
-
总结与引出“二叉平衡树”
情况 | 树的高度 h | 操作时间复杂度 |
最好 | O(logn) | O(logn) |
平均 | O(logn) | O(logn) |
最坏 | O(n) | O(n) |
正是因为 BST 在最坏情况下的性能无法接受,我们才需要二叉平衡树 (Balanced Binary Tree)。
二叉平衡树本质上也是一棵二叉搜索树,但它增加了一些额外的约束和调整机制(如旋转操作),来确保树在任何插入和删除操作之后,其高度始终保持在 O(logn) 的量级。这样就从根本上避免了退化成链表的可能,保证了所有操作在最坏情况下也具有对数级的优异性能。
常见的二叉平衡树有 AVL 树和红黑树,它们是解决 BST 性能不稳定问题的关键。
5. 完整的 Java 实现与演示
下面是包含所有操作的完整代码,您可以直接运行查看效果。
Java
// 节点类
class Node {
int key;
Node left, right;
public Node(int item) {
key = item;
left = right = null;
}
}
// 二叉搜索树类
public class BinarySearchTree {
Node root;
// 构造函数
public BinarySearchTree() {
root = null;
}
// --- 插入操作 ---
public void insert(int key) {
root = insertRec(root, key);
}
private Node insertRec(Node current, int key) {
if (current == null) {
return new Node(key);
}
if (key < current.key) {
current.left = insertRec(current.left, key);
} else if (key > current.key) {
current.right = insertRec(current.right, key);
}
return current;
}
// --- 查找操作 ---
public boolean search(int key) {
return searchRec(root, key) != null;
}
private Node searchRec(Node current, int key) {
if (current == null || current.key == key) {
return current;
}
if (key < current.key) {
return searchRec(current.left, key);
}
return searchRec(current.right, key);
}
// --- 删除操作 ---
public void delete(int key) {
root = deleteRec(root, key);
}
private Node deleteRec(Node current, int key) {
if (current == null) return null;
if (key < current.key) {
current.left = deleteRec(current.left, key);
} else if (key > current.key) {
current.right = deleteRec(current.right, key);
} else {
if (current.left == null) return current.right;
if (current.right == null) return current.left;
current.key = findMinValue(current.right);
current.right = deleteRec(current.right, current.key);
}
return current;
}
private int findMinValue(Node node) {
int minv = node.key;
while (node.left != null) {
minv = node.left.key;
node = node.left;
}
return minv;
}
// --- 中序遍历 (用于打印树的内容) ---
public void inorder() {
inorderRec(root);
System.out.println();
}
private void inorderRec(Node node) {
if (node != null) {
inorderRec(node.left);
System.out.print(node.key + " ");
inorderRec(node.right);
}
}
public static void main(String[] args) {
BinarySearchTree bst = new BinarySearchTree();
// 插入节点
bst.insert(50);
bst.insert(30);
bst.insert(20);
bst.insert(40);
bst.insert(70);
bst.insert(60);
bst.insert(80);
// 打印中序遍历结果 (应该是有序的)
System.out.print("中序遍历: ");
bst.inorder(); // 输出: 20 30 40 50 60 70 80
// 查找节点
System.out.println("查找 40: " + bst.search(40)); // true
System.out.println("查找 90: " + bst.search(90)); // false
// 删除叶子节点 20
System.out.println("\n删除 20 (叶子节点)...");
bst.delete(20);
System.out.print("中序遍历: ");
bst.inorder(); // 输出: 30 40 50 60 70 80
// 删除只有一个子节点的节点 30
System.out.println("\n删除 30 (只有一个子节点)...");
bst.delete(30);
System.out.print("中序遍历: ");
bst.inorder(); // 输出: 40 50 60 70 80
// 删除有两个子节点的节点 50
System.out.println("\n删除 50 (有两个子节点)...");
bst.delete(50);
System.out.print("中序遍历: ");
bst.inorder(); // 输出: 40 60 70 80
}
}
希望这份详细的讲解能帮助您扎实地掌握二叉搜索树的知识!