伸展树(splay tree)
对于二叉查找树而言,每次操作的最坏时间复杂度是O(N)。(当树退化为链表的时候)。为了解决这个问题,我们给树附加了一个平衡条件。平衡条件限制了任何节点的深度都不能过深。其中一种限制条件是:一颗二叉查找树的左子树和右子树的高度差不能超过1,这个条件限制产生了AVL树。
二叉查找树的最坏操作是O(N)。但是这样的操作并不常见。所以累加起来的时间就变得比单次操作时间的最坏情形要重要的多。但是二叉查找树保证了对M次的连续操作花费的时间是O(MlogN)(因为二叉查找树的平均操作时间是O(logN))。这样的数据结构基本上可行的。
伸展树的基本想法是,当一个节点被访问后,它要经过一系列AVL树的旋转操作,被放置到根上。如果该节点很深,那么我们必须通过重构使这些节点的访问花费的时间也变少。(说的通俗点就是指,伸展树的想法是把访问过的节点尽量的放到靠近根的地方。以便这些节点下一次被访问时,能快速的找到。在实际应用中,很多情形下,一个节点被访问过后,在不久的将来,它就会迎来下一次访问。研究表明,这种情况发生的比我们想象的还更加频繁。)当然伸展树在理论上也能给出M次连续对树的操作最多花费O(MlogN)的时间,注意,这里是给出了对M次连续操作的情形的保证,它并不保证一次操作不会出现O(N)这样的可能性。正如二八原则指出的一样,我们需要查找的数据往往只是那20%而已。(学过的数据结构也许只有20%是经常使用的,其他的可能很久都不会使用)另外,伸展树不需要保存节点的高度信息,这样还能节省点存储空间。
一个较为简单的想法是,对一棵二叉查找树实施单旋转,从下至上进行。这种操作意味着我们将在访问路径上的每一个节点和它们的父节点实施单旋转。旋转的效果是将节点K一直推向树根。这使得对K的访问变得很快速(暂时性的)。但是它把路径上的另外一个节点(祖父节点)几乎推向了和K一起一样的深度。而如果对那个被本次变深的节点进行访问,这又会将另外的节点推得很深。可以证明,这种策略对M次连续操作一共需要Ω(MN)的时间。一个例子就是斜二叉树。
展开(Splaying)是类似于旋转的,访问节点K(非根),在访问节点K的路径上实施这样一个旋转操作。如果K的父节点是这棵树的树根,那么直需要旋转K和树根。否则,X就有父节和祖父,存在两种情形以及它们的对称(对于编程实现而言就是4种情形)情形。第一种情况是之字形(zig-zag),另一种情形是一字形(zig-zig)。
之字形:K是右儿子,K的父亲是左儿子(或者是K是左儿子,K的父亲是右儿子)。
一字形:K和K的父亲都是左子树(或者都是右子树)。
自顶向下的展开方式是如下图所示的。(自顶向下)
之字形的旋转可以简化为下图所示(这样之字形就和单旋转一致了)
展开操作不仅仅是把要查找的节点移动到根节点,它还把访问路径上的大多数节点的深度减少了一半。在展开操作中,不会出现在简单旋转策略中出现的那种最坏的情形。当访问路径是相当深的时候,这些旋转对未来的操作是有益的。当访问较浅的时候,这些旋转有可能是有害的。经过多次访问之后,伸展树变得几乎平衡。在伸展树中,每个操作的最坏时间是O(logN),即使在最坏的情形下,也不会超过它。我们可以通过访问要被删除的节点实行删除操作,当然操作会将被删除的节点先推到根处。删除该节点将会得到左右两棵子树。其中一种策略是访问左子树的最大值(这个操作将把最大值放到左子树的根处,同时使得左子树的右子树为空),然后将删除后产生的右子树放到左子树的右子树处即可完成删除操作。
对伸展树的分析比较困难,因为它是动态变化的。因此可能需要使用到摊还分析的技巧。
如果按照最简单方式来旋转,这样的伸展树实现的效率并不高,只有按照展开的双层调整的方式,才是有效的。这个方式真是导致树每次深度能降低一半的操作。我们在之字形的旋转过程中,和AVL树的双旋转并没有什么区别。不同之处在于,一字形的情形。这是这点不同导致了伸展树的效果是很好的。
下面的这组图是自底向上的展开策略
正是在这种情形下的双层伸展,才导致树平均每次会降低一半深度。我们来看一下《数据结构与算法分析——C语言描述》这本书上给出的一字形情形下展开后树所发生的变化。假设这棵树刚开始是右斜树。从32到1。现在我们从1这个最深的节点进行访问,树最后变成了这样。
这样的伸展操作将深度大大降低了。
下面给出伸展树的实现(C语言版)
抽象数据类型ADT,写在头文件SplayTree.h
#ifndef SPLAYTREE
#define SPLAYTREE
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<limits.h>
typedef struct SplayNode *PSplay;
typedef PSplay Position;
typedef int ElementType;
struct SplayNode
{
ElementType data;
Position left;
Position right;
};
static Position NullNode;
PSplay InitTree();
PSplay Splaying(PSplay T, ElementType x);
PSplay LLRotate(PSplay K);
PSplay RRRotate(PSplay K);
PSplay FindMax(PSplay T);
PSplay FindMin(PSplay T);
PSplay Insert(ElementType x, PSplay T);
PSplay Delete(ElementType x, PSplay T);
void Traversal(PSplay T);
void Traversal(PSplay T,int i);
#endif // !SPLAYTREE
SplayTree.cpp文件里实现了自顶向下的伸展树
#include "SplayTree.h"
PSplay InitTree() //初始化树,将NullNode看做NULL指针,避免了检测空树
{
if (NullNode == NULL)
{
NullNode = (PSplay)malloc(sizeof(SplayNode));
if (NULL == NullNode)
{
printf("error,memery is full");
}
//该节点的左子树和右子树指向该节点本身。
NullNode->left = NullNode->right = NullNode;
}
return NullNode;
}
PSplay Splaying(PSplay T, ElementType x)
{
//带有左指针和右指针的头结点包含了左子树和右子树的根
struct SplayNode Header;
Header.left = Header.right = NullNode;
Position LeftTreeMax, RightTreeMin;
LeftTreeMax = RightTreeMin = &Header;
NullNode->data = x; //这里NullNode的数据域是x
//当查找到NullNode时或者是找到了x时,循环结束
while (x != T->data)
{
if (x < T->data)
{
if (x < T->left->data)
{
T = LLRotate(T);
}
if (NullNode == T->left) //左子树为空
{
break;
}
RightTreeMin->left = T;
RightTreeMin = T;
//只要第一次左子树变为非空,那么T = T->left将会使LeftTreeMax在初始化后不再改变
T = T->left;
//这样的话LeftTreeMax将仍然指向Header,它将包含右子树的根
}
else
{
if (x > T->right->data)
{
T = RRRotate(T);
}
if (NullNode == T->right) //右子树为空
{
break;
}
LeftTreeMax->right = T;
LeftTreeMax = T;
//只要第一次右子树变为非空,那么T = T->right将会使RightTreeMin在初始化后不再改变
T = T->right;
}
}
//连接
LeftTreeMax->right = T->left;
RightTreeMin->left = T->right;
T->left = Header.right;
T->right = Header.left;
return T;
}
PSplay LLRotate(PSplay K)
{
Position M;
M = K->left;
K->left = M->right;
M->right = K;
return M;
}
PSplay RRRotate(PSplay K)
{
Position M;
M = K->right;
K->right = M->left;
M->left = K;
return M;
}
PSplay FindMax(PSplay T)
{
T = Splaying(T, INT_MAX); //最大值处展开
return T;
}
PSplay FindMin(PSplay T)
{
T = Splaying(T, INT_MIN); //最小值处展开
return T;
}
PSplay Insert(ElementType x, PSplay T)
{
PSplay NewNode;
NewNode = (PSplay)malloc(sizeof(SplayNode));
if (NewNode == NULL)
printf("error ,memory is full");
NewNode->data = x;
if (T == NullNode) //T是空的情形
{
NewNode->left = NewNode->right = NullNode;
T = NewNode;
}
else
{
T = Splaying(T, x); //伸展
if (x < T->data)
{
NewNode->left = T->left;
NewNode->right = T;
T->left = NullNode;
T = NewNode;
}
else if (x > T->data)
{
NewNode->right = T->right;
NewNode->left = T;
T->right = NullNode;
T = NewNode;
}
else if (x == T->data)
{
/* 重复值不做插入 */
free(NewNode);
}
}
return T;
}
PSplay Delete(ElementType x, PSplay T)
{
Position newnode;
if (T != NullNode)
{
T = Splaying(T, x);
if (x == T->data) //找得到该元素
{
if (T->left == NullNode) //左子树为空
{
newnode = T->right; //根就是该节点的右子树
}
else
{
newnode = T->left; //该节点的左子树成为新根
newnode = Splaying(newnode, x); //x作为展开点,那么这个伸展操作将会把最大的节点提升至根处
//那么这个根节点没有右子树(右子树为空)
newnode->right = T->right; //将原来的右子树挂上
}
free(T);
T = newnode; //newnode作为新的根节点
}
}
return T;
}
void Traversal(PSplay T) //中序遍历
{
if (NullNode != T)
{
Traversal(T->left);
printf("%d ", T->data);
Traversal(T->right);
}
}
void Traversal(PSplay T, int i) //前序遍历
{
if (NullNode != T)
{
printf("%d ", T->data);
Traversal(T->left, 1);
Traversal(T->right, 1);
}
}
main.cpp文件里进行测试
#include"splaytree.h"
int main()
{
PSplay T = InitTree();
T = Insert(7, T);
T = Insert(6, T);
T = Insert(5, T);
T = Insert(4, T);
T = Insert(3, T);
T = Insert(2, T);
T = Insert(1, T);
T = Insert(0, T);
printf("前序遍历输出结果:");
Traversal(T, 1);
printf("\n");
printf("中序遍历输出结果:");
Traversal(T);
printf("\n");
T = Delete(4, T);
printf("删除4以后前序遍历输出结果:");
Traversal(T, 1);
printf("\n");
printf("中序遍历输出结果:");
Traversal(T);
printf("\n");
printf("树中最小值是:%d\n",FindMin(T)->data);
printf("树中最大值是:%d\n", FindMax(T)->data);
system("pause");
return 0;
}
测试结果如下所示:
测试结果也说明了伸展树并没有避免生成斜二叉树,但是它在后续的伸展过程中不会出现恶性循环,使得树最终还可能是斜二叉树。一般而言经过为数不多的操作之后,伸展树都将几乎是平衡的,并且深度是较浅的。在实际的使用过程之中。伸展树的表现是良好的,它的代码运行的很快。
自底向上实现伸展树代码
#include<stdio.h>
#include<stdlib.h>
//伸展树的实现
typedef struct splayNode {
int element;
struct splayNode* left;
struct splayNode* right;
}*SplayTree;
SplayTree SingleTotateWithLeft(SplayTree T);
SplayTree SingleTotateWithRight(SplayTree T);
SplayTree DoubleTotateWithLeft(SplayTree T);
SplayTree DoubleTotateWithRight(SplayTree T);
SplayTree splaying(SplayTree Root, SplayTree T, SplayTree P);
void print(SplayTree T);
void print(SplayTree T) {
//中序
if (T->left != NULL) {
print(T->left);
}
printf("%d ", T->element);
if (T->right != NULL) {
print(T->right);
}
}
void print2(SplayTree T) {
//前序 根左右
printf("%d ", T->element);
if (T->left != NULL) {
print2(T->left);
}
if (T->right != NULL) {
print2(T->right);
}
}
SplayTree insert(int X, SplayTree T) {
if (T == NULL) {
//树为空
T = (SplayTree)malloc(sizeof(struct splayNode));
T->element = X;
T->left = T->right = NULL;
}
else {
//不空
if (X > T->element) {
//往右子树走
T->right = insert(X, T->right);
}
else if (X < T->element) {
//往左子树走
T->left = insert(X, T->left);
}
}
return T;
}
//查找
SplayTree find(int X, SplayTree T) {
if (T == NULL) {
printf("tree is NULL");
return NULL;
}
else {
SplayTree P = T;
while (P) {
if (X > P->element) {
//在右子树中
P = P->right;
}
else if (X < P->element) {
//在左子树中
P = P->left;
}
else if (X == P->element) {
//找到了
//开始调整伸展树
T = splaying(T, T, P);
return T;
}
}
return NULL;
}
}
SplayTree splaying(SplayTree Root, SplayTree T, SplayTree P) {
if (P->element > T->element) {
//比T大 往右走
T->right = splaying(Root, T->right, P);
}
else if (P->element < T->element) {
//比T小 往左走
T->left = splaying(Root, T->left, P);
}
//三种情况
//1要查找的结点为根结点的左右孩子 zig
if (Root->left != NULL && Root->left == P) {
//是根的左孩子,实行左旋
Root = SingleTotateWithLeft(Root);
return Root;
}
else if (Root->right != NULL && Root->right == P) {
//是根的右孩子,实行右旋
Root = SingleTotateWithRight(Root);
return Root;
}
//第二种情况 zig-zag
if (T->left != NULL && T->left != P && T->left->right != NULL && T->left->right == P) {
//先右旋 后左旋 即左双旋
T = DoubleTotateWithLeft(T);
}
else if (T->right != NULL && T->right != P && T->right->left != NULL && T->right->left == P) {
//先左旋 后右旋 即右双旋
T = DoubleTotateWithRight(T);
}
//第三种情况 zig-zig
if (T->left != NULL && T->left != P && T->left->left != NULL && T->left->left == P) {
//先左旋 再左旋
T = SingleTotateWithLeft(T);
T = SingleTotateWithLeft(T);
}
else if (T->right != NULL && T->right != P && T->right->right != NULL && T->right->right == P) {
//先右旋 再右旋
T = SingleTotateWithRight(T);
T = SingleTotateWithRight(T);
}
return T;
}
//左单旋
SplayTree SingleTotateWithLeft(SplayTree T) {
SplayTree M;
M = T->left;
T->left = M->right;
M->right = T;
return M;
}
//右单旋
SplayTree SingleTotateWithRight(SplayTree T) {
SplayTree M;
M = T->right;
T->right = M->left;
M->left = T;
return M;
}
//左双旋
SplayTree DoubleTotateWithLeft(SplayTree T) {
//先进行右旋 再进行左旋
T->left = SingleTotateWithRight(T->left);
return SingleTotateWithLeft(T);
}
//右双旋
SplayTree DoubleTotateWithRight(SplayTree T) {
//先进行左旋,再进行右旋
T->right = SingleTotateWithLeft(T->right);
return SingleTotateWithRight(T);
}
int main() {
//测试
SplayTree T = NULL;
T = insert(7, T);
T = insert(6, T);
T = insert(5, T);
T = insert(4, T);
T = insert(3, T);
T = insert(2, T);
T = insert(1, T);
print2(T);
T = find(1, T);
printf("查找后:");
print2(T);
T = find(2, T);
T = find(5, T);
printf("再次查找后:");
print2(T);
system("pause");
return 0;
}
自底向上实现的伸展树,一般而言没有什么用处,我们很少用到它,因为这种操作存在一个问题,首先是我们在这样的直观展开操作需要从树根向下的一次遍历,以及而后自底向上的一次遍历(旋转操作)。这个过程可以通过递归(或者栈来保存一些父指针)来完成。因此这种方式需要大量的额外开销,而且这种展开需要处理的情形是3种(实际编程是6种),并且需要特殊处理空树。因此,我们通常使用自顶向下的伸展树,它只用到了O(1)的额外空间,但是却保持了O(logN)的摊还时间。无疑,自顶向下的伸展树比自底向上的伸展树要好得多。