引言
伸展树是基于AVL树的,但它并没有AVL的平衡要求,任意节点的左右子树可以相差任意深度。与二叉搜索树类似,伸展树的单次搜索也可能需要n次操作。但伸展树可以保证,m次的连续搜索操作的复杂度为mlog(n)的量级,而不是mn量级。
伸展树的出发点是这样的:考虑到局部性原理(刚被访问过的数据,极有可能很快地再次被访问),因此将刚被访问过的节点移(旋转)到根节点。
如果没有了解过AVL树,建议先看下 图解AVL树
伸展策略
那么该如何旋转呢?
逐层伸展
联系刚刚学过的AVL树,我们可以从待访问的节点开始,逐层往上旋转。
我们使用的手段无非是,
当待访问节点v
是左孩子,进行一次右旋操作。
若是右孩子,则进行一次左旋操作。
这种策略简单明了,但是存在一些不足之处,是什么呢?我们来看一种特例:
假设有这样一颗树,按照访问最深节点的方式进行访问。初始访问节点1:
经过若干次伸展(旋转)之后,1被送到了根节点,接下来访问最深节点2:
…
最后访问最深节点7,该伸展树会重新变成了类似链表的结构:
整个过程如下:
这样会有什么问题呢,显然树的高度会很高,旋转次数呈周期性的算术级数演变:每一周期累计
Ω
(
n
2
)
\Omega(n^2)
Ω(n2),分摊
Ω
(
n
)
\Omega(n)
Ω(n)。
好在Robert Endre Tarjan 提出了一种新的伸展策略:双层伸展
双层伸展
该策略的精髓是:向上伸展两层,而非一层。
我们考察节点v
,父亲p
以及祖父g
。根据它们的相对位置,经过两次旋转,使得v
为该子树的根。它们的相对位置,如上图所示。
首先考虑zag-zig的情况(对称的zig-zag是同理的):
节点v
是左孩子,父亲p
是右孩子。
首先围绕节点p
做一次右旋,使v
上升一层,接着围绕g
做一次左旋,使v
再上升一层。
如果你观察仔细的话,会发现其实与AVL树双旋完全等效,并且与逐层伸展也没有区别,唯一不同是,整个操作使v
上升了两层。
没错,真正精髓的地方在于zig-zig和zag-zag情形:
对于zig-zig的情况,要越级旋转,不是围绕节点p
旋转,而是它的祖父节点g
进行旋转。然后才围绕它的父亲节点p
旋转。过程如上图所示。
我们看下与常规方法的区别:
上图中上半部分是常规的两次旋转,下半部分是双层旋转。可以看到旋转后的结构的区别。
我们同样考虑上面那种情况:
先访问最深节点1,此时不是围绕2进行旋转,而是1的祖父3。
然后是它的父亲节点2:
接下来是针对以5为根的子树,这里就不展开描述了。
最终形成的结构如下:
可以看到,访问了节点1之后,整颗树的高度基本缩减了一半。
继续尝试访问最深节点3:
可以看到,树的高度又缩减了一半。
因此我们主要研究双层伸展树的实现,接下来到了给代码的时间。
实现
我们采用带有parent
引用的实现,当然还有不带该引用的实现,有兴趣的同学可以去搜一下。
树节点结构
private static class Node<E> {
E data;
Node<E> left;
Node<E> right;
//指向父节点 维护起来要复杂一点, 但是很多方法不需要返回Node了
Node<E> parent;
Node(E data) {
this.data = data;
}
/**
* @return 当前节点是否为父节点的左孩子
*/
boolean isLeft() {
return parent != null && parent.left == this;
}
/**
* @return 当前节点是否为父节点的右孩子
*/
boolean isRight() {
return parent != null && parent.right == this;
}
/**
* 返回祖父节点,如果有的话
*
* @return
*/
Node<E> grandparent() {
if (parent != null) {
return parent.parent;
}
return null;
}
/**
* 为当前节点附加一个右孩子
*
* @param child
*/
void attachRightChild(Node<E> child) {
this.right = child;
if (child != null) {
child.parent = this;
}
}
/**
* 为当前节点附加一个左孩子
*
* @param child
*/
void attachLeftChild(Node<E> child) {
this.left = child;
if (child != null) {
child.parent = this;
}
}
}
增加了一些便利方法。
因为代码中也有图解(啥?不敢相信是吧,往下看),其他方法就不一一分析了。有问题欢迎留言。
完整代码
package com.algorithms.tree;
import java.util.NoSuchElementException;
import java.util.stream.IntStream;
/**
* 双层伸展树
*
* @author yjw
* @date 2019/6/26/026
*/
public class SplayTree<E extends Comparable<? super E>> implements BinaryTree<E> {
private Node<E> root;
BinaryTreePrinter<Node<E>> printer;
public SplayTree() {
printer = new BinaryTreePrinter<>(node -> node.data.toString(), node -> node.left, eNode -> eNode.right);
printer.setHspace(4);
printer.setSquareBranches(true);
}
@Override
public void insert(E x) {
Node<E> node = new Node<>(x);
Node<E> cur = root;
Node<E> p = null;//保存cur的parent引用
while (cur != null) {
p = cur;
if (node.data.compareTo(cur.data) < 0) {
cur = cur.left;
} else {
cur = cur.right;
}
}
node.parent = p;
if (p == null) {
root = node;
} else if (node.data.compareTo(p.data) < 0) {
//node < p
p.left = node;
} else {
//node >= p
p.right = node;
}
splay(node);
}
@Override
public void remove(E x) {
if (!contains(x)) {
return;
}
Node<E> newTree;
//代码执行到这里,说明找到了x,它已经被伸展到了树根root
if (root.left == null) {
//没有左孩子,指向右孩子即可
newTree = root.right;
} else {
//连接root的左子树与右子树 合并成新树
newTree = join(root.left, root.right);
}
//newTree即将成为新的root,当然不能有parent了,同时消除对旧root的引用
if (newTree != null) {
newTree.parent = null;
}
root = newTree;
}
@Override
public boolean contains(E x) {
if (isEmpty()) {
return false;
}
Node<E> node = contains(x, root);
if (node != null) {
splay(node);
return true;
}
return false;
}
private Node<E> contains(E x, Node<E> node) {
if (node == null) {
return null;
}
int cmp = x.compareTo(node.data);
if (cmp < 0) {
return contains(x, node.left);
} else if (cmp > 0) {
return contains(x, node.right);
}
return node;
}
@Override
public boolean isEmpty() {
return root == null;
}
@Override
public void makeEmpty() {
root = null;
}
@Override
public void printTree() {
if (isEmpty()) {
System.out.println("Empty Tree.");
} else {
printer.printTree(root);
}
}
private Node<E> join(Node<E> left, Node<E> right) {
if (left == null) {
return right;
}
if (right == null) {
return left;
}
//找到left的最大节点
Node<E> x = findMax(left);
//伸展置left子树根
splay(x);
//将right作为x的右节点
x.attachRightChild(right);
return x;
}
public E findMax() {
if (isEmpty()) {
throw new NoSuchElementException();
}
return findMax(root).data;
}
private Node<E> findMax(Node<E> node) {
while (node.right != null) {
node = node.right;
}
return node;
}
/**
* 伸展
* <p>
* 对于zig-zag与zag-zig应用逐层伸展方法
* 对于zig-zig与zag-zag应用双层伸展方式
*
* @param v
*/
private void splay(Node<E> v) {
//只要还没到点顶部
while (v.parent != null) {
//如果没有祖父节点
if (v.grandparent() == null) {
//如果v是左孩子
if (v.isLeft()) {
// p v
// / \ / \
// v Z -> X p
// / \ / \
// X Y Y Z
rotateRight(v.parent);
} else {
// p v
// / \ / \
// X v -> p Z
// / \ / \
// Y Z X Y
rotateLeft(v.parent);
}
} else if (v.isLeft() && v.parent.isLeft()) { //zig-zig
// g v
// / \ / \
// p Z p W p
// / \ -> / \ -> / \
// v Y v g X g
// / \ / \ / \ / \
// W X W X Y Z Y Z
//双层伸展的精髓:先旋转g-p,再旋转p-v
rotateRight(v.grandparent());
rotateRight(v.parent);
} else if (v.isRight() && v.parent.isRight()) { //zag-zag
// g v
// / \ / \
// Z p p p X
// / \ -> / \ -> / \
// Y v g v g W
// / \ / \ / \ / \
// W X Z Y W X Z Y
rotateLeft(v.grandparent());
rotateLeft(v.parent);
} else if (v.isRight() && v.parent.isLeft()) {
// g g v
// / \ / \ / \
// p Z -> v Z -> p g
// / \ / \ / \ / \
// Y v p X Y W X Z
// / \ / \
// W X Y W
//zig-zag 采用的是逐层伸展的方式
rotateLeft(v.parent);//先对p-v执行左旋
rotateRight(v.parent);//再对g-v执行右旋
} else { //v.isLeft() && v.parent.isRight()
// g g v
// / \ / \ / \
// W p -> W v -> g p
// / \ / \ / \ / \
// v Z X p W X Y Z
// / \ / \
// X Y Y Z
//对称的 zag-zig也采用逐层伸展
rotateRight(v.parent);
rotateLeft(v.parent);
}
}
}
/**
* 以p的左孩子为中心,做一次右旋
*
* @param p
*/
private void rotateRight(Node<E> p) {
// p v
// / \ / \
// v Z -> X p
// / \ / \
// X Y Y Z
Node<E> v = p.left;
//将Y作为p的左孩子
p.attachLeftChild(v.right);
//更新v的parent引用
v.parent = p.parent;
if (p.parent == null) {
//v就是新根
root = v;
} else if (p.isRight()) {
//p是其父节点的右孩子,更新parent的右孩子引用
p.parent.attachRightChild(v);
} else {
p.parent.attachLeftChild(v);
}
v.attachRightChild(p);
}
private void rotateLeft(Node<E> p) {
// p v
// / \ / \
// X v -> p Z
// / \ / \
// Y Z X Y
Node<E> v = p.right;
//Y附加到p的右孩子上
p.attachRightChild(v.left);
v.parent = p.parent;
if (p.parent == null) {
root = v;
} else if (p.isLeft()) {
p.parent.attachLeftChild(v);
} else {
p.parent.attachRightChild(v);
}
v.attachLeftChild(p);
}
private static class Node<E> {
E data;
Node<E> left;
Node<E> right;
//指向父节点 维护起来要复杂一点, 但是很多方法不需要返回Node了
Node<E> parent;
Node(E data) {
this.data = data;
}
/**
* @return 当前节点是否为父节点的左孩子
*/
boolean isLeft() {
return parent != null && parent.left == this;
}
/**
* @return 当前节点是否为父节点的右孩子
*/
boolean isRight() {
return parent != null && parent.right == this;
}
/**
* 返回祖父节点,如果有的话
*
* @return
*/
Node<E> grandparent() {
if (parent != null) {
return parent.parent;
}
return null;
}
/**
* 为当前节点附加一个右孩子
*
* @param child
*/
void attachRightChild(Node<E> child) {
this.right = child;
if (child != null) {
child.parent = this;
}
}
/**
* 为当前节点附加一个左孩子
*
* @param child
*/
void attachLeftChild(Node<E> child) {
this.left = child;
if (child != null) {
child.parent = this;
}
}
}
public static void main(String[] args) {
SplayTree<Integer> tree = new SplayTree<>();
int[] values = IntStream.rangeClosed(1, 15).toArray();
for (int value : values) {
tree.insert(value);
}
System.out.println("-------------------------");
System.out.println(tree.contains(1));
tree.printTree();
}
}
在main
中打印的树为:
1
└┐
14
┌──┴──┐
12 15
┌──┴──┐
10 13
┌──┴──┐
8 11
┌──┴──┐
6 9
┌──┴──┐
4 7
┌──┴──┐
2 5
└┐
3
优缺点
它的性能如下:
其中遍历(Traversal)操作的摊还(Amortized)时间复杂度为
O
(
n
)
O(n)
O(n),整体还是不错的。
优点
- 无需记录节点高度或平衡因子(我们的实现多了个
parent
引用,但是也不复杂对吧) - 编程实现比AVL树简单
- 局部性强时,缓存命中率极高(
k
<
<
n
<
<
m
)
k << n << m)
k<<n<<m)
- 任何连续的m次查找,都在 o ( m l o g k + n l o g n ) o(mlogk + nlogn) o(mlogk+nlogn)时间内完成
缺点
- 它有可能会变成一条链
- 不适用于对效率敏感的场景
在以非降顺序访问n个元素之后,比如我们执行这样的代码:
SplayTree<Integer> tree = new SplayTree<>();
int[] values = IntStream.rangeClosed(1, 15).toArray();
for (int value : values) {
tree.insert(value);
}
for (int value : values) {
tree.contains(value);
}
tree.printTree();
打印树结构如下:
15
┌┘
14
┌┘
13
┌┘
12
┌┘
11
┌┘
10
┌┘
9
┌┘
8
┌┘
7
┌┘
6
┌┘
5
┌┘
4
┌┘
3
┌┘
2
┌┘
1
哇,又变成一条链表了。