文章目录
1. 概述
伸展树(Splay Tree)是一种能够自我平衡的二叉查找树,它能在均摊 O ( log n ) O(\log n) O(logn) 的时间内完成基于伸展(Splay)操作的插入、查找、修改和删除操作。它是由 Daniel Sleator 和 Robert Tarjan 在1985年发明的。
在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些节点就应当经常处于靠近树根的位置。于是想到设计一个简单方法,在每次查找之后对树进行调整,把被查找的节点搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。
1.1 优点
伸展树的自我平衡使其拥有良好的性能,频繁访问的节点会被移动到更靠近根节点的位置,进而获得更快的访问速度。
-
可靠的性能——它的平均效率不输于其他平衡树。
-
存储所需的内存少——伸展树无需记录额外的什么值来维护树的信息,相对于其他平衡树,内存占用要小。
1.2 缺点
-
伸展树并不总是平衡树,在某些操作后可能变得不平衡,有可能会变成一条链。例如,在以非递减顺序访问全部 n 个节点之后,此时树的高度对应于最坏情况的时间效率,操作的实际时间效率可能很低。然而,均摊的最坏情况是对数级的—— O ( log n ) O( \log n) O(logn)
-
即使以“只读”方式(例如通过查找操作)访问伸展树,其结构也可能会发生变化。这使得伸展树在多线程环境下会变得很复杂。具体而言,如果允许多个线程同时执行查找操作,则需要额外的维护和操作。
2. 2种单旋转和4种双旋转
2.1 Zig Rotation
Zig Rotation 是单旋转。如果 x x x 是左子节点且 x x x 没有祖父母(即 x x x 的父节点是根节点),我们将对节点 x x x 进行 Zig Rotation。 为了完成 Zig Rotation,我们将 x x x 的父节点向右旋转。如下图所示。
2.2 Zag Rotation
Zag Rotation 是 Zig Rotation 的镜像。如果 x x x 是右子节点且 x x x 没有祖父母,我们将对节点 x x x 进行 Zag Rotation。为了完成 Zag Rotation,我们将 x x x 的父节点向左旋转。 如下图所示。
2.3 Zig-Zig Rotation
Zig-Zig Rotation 是双旋转。如果 x x x 是左子节点并且 x x x 的父节点也是左子节点,我们将对节点 x x x 进行 Zig-Zig Rotation。为了完成 Zig-Zig Rotation,我们首先将 x x x 的祖父母节点向右旋转,然后将 x x x 的父节点向右旋转。如下图所示。
2.4 Zag-Zag Rotation
Zag-Zag Rotation 是 Zig-Zig Rotation 的镜像。如果 x x x 是右子节点并且 x x x 的父母也是右子节点,我们将对节点 x x x 进行 Zag-Zag Rotation。为了完成 Zag-Zag Rotation,我们首先将 x x x 的祖父母节点向左旋转,然后将 x x x 的父节点向左旋转。如下图所示。
2.5 Zig-Zag Rotation
Zig-Zag Rotation 也是双旋转。如果 x x x 是右子节点而 x x x 的父母是左子节点,我们会对 x x x 进行 Zig-Zag Rotation。为了完成 Zig-Zag Rotation,我们首先将 x x x 的父节点向左旋转,然后将 x x x 的祖父母(新父)节点向右旋转。如下图所示。
2.6 Zag-Zig Rotation
Zag-Zig Rotation 是 Zig-Zag Rotation 的镜像。如果 x x x 是左子节点而 x x x 的父母是右子节点,我们将对 x x x 进行 Zag-Zig Rotation。为了完成 Zag-Zig Rotation,我们首先将 x x x 的父节点向右旋转,然后将 x x x 的祖父母(新父)节点向左旋转。如下图所示。
3. 操作
3.1 伸展Splay
每次双旋转都会将节点 x x x 移到其祖父母的位置,而每次单旋转都会将节点 x x x 移到其父母的位置,通过执行这些旋转,直至节点 x x x 到达树根,此过程就被称为伸展(Splay)操作。
下面给出了 Splay 操作的伪代码。
3.2 连接Join
给出两棵树 S S S 和 T T T,且 S S S 的所有元素都比 T T T 的元素要小。下面的步骤可以把它们连接成一棵树:
- 伸展 S S S 中最大的节点。现在这个节点变成了 S S S 的根节点,且没有右儿子。
- 令 T T T 的根节点变为其右儿子。
连接操作的伪代码如下。
3.3 分割Split
给出一棵树和一个节点 x x x,返回两棵树 S S S 和 T T T,其中 S S S 中所有的元素均小于等于 x x x 的值, T T T 中所有的元素大于 x x x 的值。下面的步骤可以完成这个操作:
-
伸展节点 x x x。这样的话,节点 x x x 就成为了这棵树的根,所以它的左子树包含了所有比 x x x 的值小的元素,右子树包含了所有比 x x x 的值大的元素。
-
把 x x x 的右子树从树中分割出来。
分割操作的伪代码如下。
3.4 搜索Search
如果我们要搜索 key,下面的步骤可以完成这个操作:
- 首先,执行普通二叉查找树的搜索操作,假设在节点 x x x 中找到了 key。
- 然后,伸展节点 x x x。
搜索 key 的伪代码如下。
3.5 插入Insert
如果我们要插入 key,新建一个节点 node 保存 key 值,下面的步骤可以完成插入节点 node。
- 首先,执行普通二叉查找树的插入操作,在适当的位置插入节点 node。
- 然后,伸展节点 node,将其移动到树的根。
插入 key 的伪代码如下。
3.6 删除Delete
如果我们要删除 key,假设在节点 x x x 中找到了 key,下面的步骤可以完成删除节点 x x x。
-
根据节点 x x x 将整棵树分割为两棵树 S S S 和 T T T,其中 S S S 中所有的元素均小于等于 key, T T T 中所有的元素大于 key。
-
从 S 的根部删除节点 x x x。
-
将 S S S 的左子树和 T T T 连接成一棵新树。
删除节点 x x x 的伪代码如下。
4. C++代码实现
// Splay tree implementation in C++
// Author: Algorithm Tutor
// Tutorial URL: http://algorithmtutor.com/Data-Structures/Tree/Splay-Trees/
#include <iostream>
using namespace std;
// data structure that represents a node in the tree
struct Node {
int data; // holds the key
Node *parent; // pointer to the parent
Node *left; // pointer to left child
Node *right; // pointer to right child
};
typedef Node *NodePtr;
// class SplayTree implements the operations in Splay tree
class SplayTree {
private:
NodePtr root;
void preOrderHelper(NodePtr node) {
if (node != nullptr) {
cout<<node->data<<" ";
preOrderHelper(node->left);
preOrderHelper(node->right);
}
}
void inOrderHelper(NodePtr node) {
if (node != nullptr) {
inOrderHelper(node->left);
cout<<node->data<<" ";
inOrderHelper(node->right);
}
}
void postOrderHelper(NodePtr node) {
if (node != nullptr) {
postOrderHelper(node->left);
postOrderHelper(node->right);
cout<<node->data<<" ";
}
}
NodePtr searchTreeHelper(NodePtr node, int key) {
if (node == nullptr || key == node->data) {
return node;
}
if (key < node->data) {
return searchTreeHelper(node->left, key);
}
return searchTreeHelper(node->right, key);
}
void deleteNodeHelper(NodePtr node, int key) {
NodePtr x = nullptr;
NodePtr t, s;
while (node != nullptr){
if (node->data == key) {
x = node;
}
if (node->data <= key) {
node = node->right;
} else {
node = node->left;
}
}
if (x == nullptr) {
cout<<"Couldn't find key in the tree"<<endl;
return;
}
split(x, s, t); // split the tree
if (s->left){
// remove x
s->left->parent = nullptr;
}
root = join(s->left, t);
delete(s);
s = nullptr;
}
void printHelper(NodePtr root, string indent, bool last) {
// print the tree structure on the screen
if (root != nullptr) {
cout<<indent;
if (last) {
cout<<"└────";
indent += " ";
} else {
cout<<"├────";
indent += "| ";
}
cout<<root->data<<endl;
printHelper(root->left, indent, false);
printHelper(root->right, indent, true);
}
}
// rotate left at node x
void leftRotate(NodePtr x) {
NodePtr y = x->right;
x->right = y->left;
if (y->left != nullptr) {
y->left->parent = x;
}
y->parent = x->parent;
if (x->parent == nullptr) {
this->root = y;
} else if (x == x->parent->left) {
x->parent->left = y;
} else {
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
// rotate right at node x
void rightRotate(NodePtr x) {
NodePtr y = x->left;
x->left = y->right;
if (y->right != nullptr) {
y->right->parent = x;
}
y->parent = x->parent;
if (x->parent == nullptr) {
this->root = y;
} else if (x == x->parent->right) {
x->parent->right = y;
} else {
x->parent->left = y;
}
y->right = x;
x->parent = y;
}
// splaying
void splay(NodePtr x) {
while (x->parent) {
if (!x->parent->parent) {
if (x == x->parent->left) {
// zig rotation
rightRotate(x->parent);
} else {
// zag rotation
leftRotate(x->parent);
}
} else if (x == x->parent->left && x->parent == x->parent->parent->left) {
// zig-zig rotation
rightRotate(x->parent->parent);
rightRotate(x->parent);
} else if (x == x->parent->right && x->parent == x->parent->parent->right) {
// zag-zag rotation
leftRotate(x->parent->parent);
leftRotate(x->parent);
} else if (x == x->parent->right && x->parent == x->parent->parent->left) {
// zig-zag rotation
leftRotate(x->parent);
rightRotate(x->parent);
} else {
// zag-zig rotation
rightRotate(x->parent);
leftRotate(x->parent);
}
}
}
// joins two trees s and t
NodePtr join(NodePtr s, NodePtr t){
if (!s) {
return t;
}
if (!t) {
return s;
}
NodePtr x = maximum(s);
splay(x);
x->right = t;
t->parent = x;
return x;
}
// splits the tree into s and t
void split(NodePtr &x, NodePtr &s, NodePtr &t) {
splay(x);
if (x->right) {
t = x->right;
t->parent = nullptr;
} else {
t = nullptr;
}
s = x;
s->right = nullptr;
x = nullptr;
}
public:
SplayTree() {
root = nullptr;
}
// Pre-Order traversal
// Node->Left Subtree->Right Subtree
void preorder() {
preOrderHelper(this->root);
}
// In-Order traversal
// Left Subtree -> Node -> Right Subtree
void inorder() {
inOrderHelper(this->root);
}
// Post-Order traversal
// Left Subtree -> Right Subtree -> Node
void postorder() {
postOrderHelper(this->root);
}
// search the tree for the key k
// and return the corresponding node
NodePtr searchTree(int k) {
NodePtr x = searchTreeHelper(this->root, k);
if (x) {
splay(x);
}
return x;
}
// find the node with the minimum key
NodePtr minimum(NodePtr node) {
while (node->left != nullptr) {
node = node->left;
}
return node;
}
// find the node with the maximum key
NodePtr maximum(NodePtr node) {
while (node->right != nullptr) {
node = node->right;
}
return node;
}
// find the successor of a given node
NodePtr successor(NodePtr x) {
// if the right subtree is not null,
// the successor is the leftmost node in the
// right subtree
if (x->right != nullptr) {
return minimum(x->right);
}
// else it is the lowest ancestor of x whose
// left child is also an ancestor of x.
NodePtr y = x->parent;
while (y != nullptr && x == y->right) {
x = y;
y = y->parent;
}
return y;
}
// find the predecessor of a given node
NodePtr predecessor(NodePtr x) {
// if the left subtree is not null,
// the predecessor is the rightmost node in the
// left subtree
if (x->left != nullptr) {
return maximum(x->left);
}
NodePtr y = x->parent;
while (y != nullptr && x == y->left) {
x = y;
y = y->parent;
}
return y;
}
// insert the key to the tree in its appropriate position
void insert(int key) {
// normal BST insert
NodePtr node = new Node;
node->parent = nullptr;
node->left = nullptr;
node->right = nullptr;
node->data = key;
NodePtr y = nullptr;
NodePtr x = this->root;
while (x != nullptr) {
y = x;
if (node->data < x->data) {
x = x->left;
} else {
x = x->right;
}
}
// y is parent of x
node->parent = y;
if (y == nullptr) {
root = node;
} else if (node->data < y->data) {
y->left = node;
} else {
y->right = node;
}
// splay the node
splay(node);
}
NodePtr getRoot(){
return this->root;
}
// delete the node from the tree
void deleteNode(int data) {
deleteNodeHelper(this->root, data);
}
// print the tree structure on the screen
void prettyPrint() {
printHelper(this->root, "", true);
}
};
int main() {
SplayTree bst;
bst.insert(33);
bst.insert(44);
bst.insert(67);
bst.insert(5);
bst.insert(89);
bst.insert(41);
bst.insert(98);
bst.insert(1);
bst.prettyPrint();
bst.searchTree(33);
bst.searchTree(44);
bst.prettyPrint();
bst.deleteNode(89);
bst.deleteNode(67);
bst.deleteNode(41);
bst.deleteNode(5);
bst.prettyPrint();
bst.deleteNode(98);
bst.deleteNode(1);
bst.deleteNode(44);
bst.deleteNode(33);
bst.prettyPrint();
return 0;
}
输出结果:
5. 参考资料
[1] https://en.wikipedia.org/wiki/Splay_tree
[2] https://algorithmtutor.com/Data-Structures/Tree/Splay-Trees/