伸展树(Splay Tree)是一种高效的自适应二叉搜索树,它通过“伸展”操作将最近访问的节点移动到根位置,从而在摊还时间复杂度下保证操作的效率。
目录
一、伸展树的核心思想
1. 什么是伸展树?
伸展树是一种自平衡二叉搜索树,其核心特点是:
自适应性:每次访问节点(查找、插入、删除)后,通过“伸展(Splay)”操作将该节点移动到根位置。
无需额外存储平衡信息:不依赖AVL树的平衡因子或红黑树的颜色标记,仅通过旋转操作调整结构。
局部性原理:频繁访问的节点会被移动到靠近根的位置,减少后续访问时间。
2. 伸展操作(Splay)
伸展操作通过以下三种旋转组合将节点提升到根:
Zig(单旋转):当目标节点是根的左子节点或右子节点时使用。
Zig-Zig(同方向双旋转):当目标节点和父节点同为左子或右子时使用。
Zig-Zag(异方向双旋转):当目标节点和父节点方向相反时使用。
3. 时间复杂度分析
摊还时间复杂度:每个操作的摊还成本为
O(log n)
。最坏情况:单次操作可能达到
O(n)
,但长期操作效率趋于高效。
二、伸展树的Java实现
1. 节点结构
每个节点保存值、父节点、左右子节点信息。
class SplayTreeNode {
int key;
SplayTreeNode left;
SplayTreeNode right;
SplayTreeNode parent;
public SplayTreeNode(int key) {
this.key = key;
this.left = null;
this.right = null;
this.parent = null;
}
}
2. 旋转操作
(1) 左旋(Left Rotation)
private void leftRotate(SplayTreeNode x) {
SplayTreeNode y = x.right;
if (y != null) {
x.right = y.left;
if (y.left != null) {
y.left.parent = x;
}
y.parent = x.parent;
}
if (x.parent == null) {
root = y;
} else if (x == x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
if (y != null) {
y.left = x;
}
x.parent = y;
}
(2) 右旋(Right Rotation)
private void rightRotate(SplayTreeNode x) {
SplayTreeNode y = x.left;
if (y != null) {
x.left = y.right;
if (y.right != null) {
y.right.parent = x;
}
y.parent = x.parent;
}
if (x.parent == null) {
root = y;
} else if (x == x.parent.left) {
x.parent.left = y;
} else {
x.parent.right = y;
}
if (y != null) {
y.right = x;
}
x.parent = y;
}
3. 伸展操作(Splay)
将目标节点移动到根位置。
private void splay(SplayTreeNode node) {
while (node.parent != null) {
SplayTreeNode parent = node.parent;
SplayTreeNode grandParent = parent.parent;
// Case 1: Zig
if (grandParent == null) {
if (node == parent.left) {
rightRotate(parent);
} else {
leftRotate(parent);
}
} else {
// Case 2: Zig-Zig
if (parent.left == node && grandParent.left == parent) {
rightRotate(grandParent);
rightRotate(parent);
} else if (parent.right == node && grandParent.right == parent) {
leftRotate(grandParent);
leftRotate(parent);
}
// Case 3: Zig-Zag
else if (parent.right == node && grandParent.left == parent) {
leftRotate(parent);
rightRotate(grandParent);
} else {
rightRotate(parent);
leftRotate(grandParent);
}
}
}
root = node;
}
4. 插入操作
插入节点后执行伸展。
public void insert(int key) {
SplayTreeNode node = new SplayTreeNode(key);
SplayTreeNode current = root;
SplayTreeNode parent = null;
while (current != null) {
parent = current;
if (key < current.key) {
current = current.left;
} else {
current = current.right;
}
}
node.parent = parent;
if (parent == null) {
root = node;
} else if (key < parent.key) {
parent.left = node;
} else {
parent.right = node;
}
splay(node); // 插入后伸展
}
5. 查找操作
查找后伸展目标节点。
public SplayTreeNode search(int key) {
SplayTreeNode node = root;
while (node != null) {
if (key < node.key) {
node = node.left;
} else if (key > node.key) {
node = node.right;
} else {
splay(node); // 找到后伸展
return node;
}
}
return null;
}
6. 删除操作
删除节点并合并子树。
public void delete(int key) {
SplayTreeNode node = search(key);
if (node == null) return;
splay(node); // 确保删除的节点是根
if (node.left == null) {
root = node.right;
if (root != null) {
root.parent = null;
}
} else if (node.right == null) {
root = node.left;
root.parent = null;
} else {
SplayTreeNode leftSubtree = node.left;
leftSubtree.parent = null;
root = node.right;
root.parent = null;
// 找到右子树的最小节点并伸展
SplayTreeNode minNode = root;
while (minNode.left != null) {
minNode = minNode.left;
}
splay(minNode);
minNode.left = leftSubtree;
leftSubtree.parent = minNode;
}
}
三、应用场景
缓存系统:频繁访问的数据被移动到根附近,提高访问速度。
网络路由表:动态调整路由路径的访问优先级。
实时数据处理:快速响应最近更新的数据。
四、总结
优点:
实现简单,无需额外平衡信息。
适合局部性强的访问场景。
缺点:
单次操作可能较慢(但摊仍高效)。
严格实时系统需谨慎使用。
伸展树通过动态调整结构,在多数场景下提供高效操作。本文的Java实现展示了其核心逻辑,读者可根据需求扩展更多功能(如范围查询)。