伸展树

来源:互联网 发布:腾讯招聘程序员 编辑:程序博客网 时间:2024/05/16 07:09

我们讨论过,树的搜索效率与树的深度有关。二叉搜索树的深度可能为n,这种情况下,每次搜索的复杂度为n的量级。AVL树通过动态平衡树的深度,单次搜索的复杂度为log(n) (以上参考纸上谈兵 AVL树)。我们下面看伸展树(splay tree),它对于m次连续搜索操作有很好的效率。

伸展树会在一次搜索后,对树进行一些特殊的操作。这些操作的理念与AVL树有些类似,即通过旋转,来改变树节点的分布,并减小树的深度。但伸展树并没有AVL的平衡要求,任意节点的左右子树可以相差任意深度。与二叉搜索树类似,伸展树的单次搜索也可能需要n次操作。但伸展树可以保证,m次的连续搜索操作的复杂度为mlog(n)的量级,而不是mn量级。

体来说,在查询到目标节点后,伸展树会不断进行下面三种操作中的一个,直到目标节点成为根节点 (注意,祖父节点是指父节点的父节点)

1. zig: 当目标节点是根节点的左子节点或右子节点时,进行一次单旋转,将目标节点调整到根节点的位置。

zig

2. zig-zag: 当目标节点、父节点和祖父节点成"zig-zag"构型时,进行一次双旋转,将目标节点调整到祖父节点的位置。

zig-zag

3. zig-zig:当目标节点、父节点和祖父节点成"zig-zig"构型时,进行一次zig-zig操作,将目标节点调整到祖父节点的位置。

zig-zig

单旋转操作和双旋转操作见AVL树。下面是zig-zig操作的示意图:

zig-zig operation

在伸展树中,zig-zig操作(基本上)取代了AVL树中的单旋转。通常来说,如果上面的树是失衡的,那么A、B子树很可能深度比较大。相对于单旋转(想一下单旋转的效果),zig-zig可以将A、B子树放在比较高的位置,从而减小树总的深度。

下面我们用一个具体的例子示范。我们将从树中搜索节点2:

Original

zig-zag (double rotation)

zig-zig

zig (single rotation at root)

上面的第一次查询需要n次操作。然而经过一次查询后,2节点成为了根节点,树的深度大减小。整体上看,树的大部分节点深度都减小。此后对各个节点的查询将更有效率。

伸展树的另一个好处是将最近搜索的节点放在最容易搜索的根节点的位置。在许多应用环境中,比如网络应用中,某些固定内容会被大量重复访问(比如江南style的MV)。伸展树可以让这种重复搜索以很高的效率完成。

伸展树的C实现

复制代码
/* By Vamei *//* Splay Tree */#include <stdio.h>#include <stdlib.h>typedef struct node *position;typedef int ElementTP;struct node {    position parent;    ElementTP element;    position lchild;    position rchild;};/* pointer => root node of the tree */typedef struct node *TREE;TREE find_value(TREE, ElementTP);position insert_value(TREE, ElementTP);static void splay_tree(TREE, position);static position search_value(TREE, ElementTP);static void with_grandpa(TREE, position);static void insert_node_to_nonempty_tree(TREE, position);static TREE left_single_rotate(TREE);static TREE left_double_rotate(TREE);static TREE right_single_rotate(TREE);static TREE right_double_rotate(TREE);static TREE left_zig_zig(TREE);static TREE right_zig_zig(TREE);void main(void) {    TREE tr;    tr = NULL;    tr = insert_value(tr, 6);    tr = insert_value(tr, 5);    tr = insert_value(tr, 4);    tr = insert_value(tr, 3);    tr = insert_value(tr, 1);     tr = insert_value(tr, 2);     tr = find_value(tr, 2);    printf("%d\n", tr->rchild->lchild->element);}/*  * insert a value into the tree * return root address of the tree */position insert_value(TREE tr, ElementTP value) {    position np;    /* prepare the node */    np = (position) malloc(sizeof(struct node));    np->element = value;    np->parent  = NULL;    np->lchild  = NULL;    np->rchild  = NULL;     if (tr == NULL) tr = np;    else {        insert_node_to_nonempty_tree(tr, np);    }    return tr;}/* * * return NUll if not found  */TREE find_value(TREE tr, ElementTP value){    position np;    np = search_value(tr, value);    if (np != NULL && np != tr) {        splay_tree(tr, np);    }    return np;}/* * splaying the tree after search */static void splay_tree(TREE tr, position np){    while (tr->lchild != np && tr->rchild != np) {        with_grandpa(tr, np);    }    if (tr->lchild == np) {        right_single_rotate(tr);    }    else if (tr->rchild == np) {        left_single_rotate(tr);    }}/* * dealing cases with grandparent node */static void with_grandpa(TREE tr, position np){    position parent, grandPa;    int i,j;     parent  = np->parent;    grandPa = parent->parent;     i = (grandPa->lchild == parent) ? -1 : 1;    j = (parent->lchild == np) ? -1 : 1;    if (i == -1 && j == 1) {        right_double_rotate(grandPa);    }    else if (i == 1 && j == -1) {        left_double_rotate(grandPa);    }    else if (i == -1 && j == -1) {        right_zig_zig(grandPa);    }    else {        left_zig_zig(grandPa);    }}/* * search for value */static position search_value(TREE tr, ElementTP value) {    if (tr == NULL) return NULL;     if (tr->element == value) {        return tr;    }    else if (value < tr->element) {        return search_value(tr->lchild, value);    }    else {        return search_value(tr->rchild, value);    }}/*  * left single rotation  * return the new root */static TREE left_single_rotate(TREE tr) {    TREE newRoot, parent;    parent  = tr->parent;    newRoot = tr->rchild;    /* detach & attach */     if (newRoot->lchild != NULL) newRoot->lchild->parent = tr;    tr->rchild = newRoot->lchild;       /* raise new root node */    newRoot->lchild = tr;    newRoot->parent = parent;    if (parent != NULL) {        if (parent->lchild == tr) {        parent->lchild = newRoot;    }    else {        parent->rchild = newRoot;    }    }    tr->parent = newRoot;    return newRoot;}/*  * right single rotation  * return the new root */static TREE right_single_rotate(TREE tr) {    TREE newRoot, parent;    parent  = tr->parent;    newRoot = tr->lchild;    /* detach & attach */    if (newRoot->rchild != NULL) newRoot->rchild->parent = tr;    tr->lchild = newRoot->rchild;      /* raise new root node */    newRoot->rchild = tr;    newRoot->parent = parent;    if (parent != NULL) {        if (parent->lchild == tr) {        parent->lchild = newRoot;    }    else {        parent->rchild = newRoot;    }    }    tr->parent = newRoot;    return newRoot;}/* * left double rotation * return */static TREE left_double_rotate(TREE tr) {    right_single_rotate(tr->rchild);    return left_single_rotate(tr);}/* * right double rotation * return */static TREE right_double_rotate(TREE tr) {    left_single_rotate(tr->lchild);    return right_single_rotate(tr);}/* * insert a node to a non-empty tree * called by insert_value() */static void insert_node_to_nonempty_tree(TREE tr, position np){    /* insert the node */    if(np->element <= tr->element) {        if (tr->lchild == NULL) {            /* then tr->lchild is the proper place */            tr->lchild = np;            np->parent = tr;            return;        }        else {            insert_node_to_nonempty_tree(tr->lchild, np);        }    }    else if(np->element > tr->element) {        if (tr->rchild == NULL) {            tr->rchild = np;            np->parent = tr;            return;        }        else {            insert_node_to_nonempty_tree(tr->rchild, np);        }    }}
/*
* right zig-zig operation
*/
static TREE right_zig_zig(TREE tr){ position parent,middle,newRoot; parent = tr->parent; middle = tr->lchild; newRoot = tr->lchild->lchild; tr->lchild = middle->rchild; if (middle->rchild != NULL) middle->rchild->parent = tr; middle->rchild = tr; tr->parent = middle; middle->lchild = newRoot->rchild; if (newRoot->rchild != NULL) newRoot->rchild->parent = middle; newRoot->rchild = middle; middle->parent = newRoot; newRoot->parent = parent; if (parent != NULL) { if (parent->lchild == tr) { parent->lchild = newRoot; } else { parent->rchild = newRoot; } } return newRoot; }
/*
* left zig-zig operation
*/
static TREE left_zig_zig(TREE tr){ position parent,middle,newRoot; parent = tr->parent; middle = tr->rchild; newRoot = tr->rchild->rchild; tr->rchild = middle->lchild; if (middle->lchild != NULL) middle->lchild->parent = tr; middle->lchild = tr; tr->parent = middle; middle->rchild = newRoot->lchild; if (newRoot->lchild != NULL) newRoot->lchild->parent = middle; newRoot->lchild = middle; middle->parent = newRoot; newRoot->parent = parent; if (parent != NULL) { if (parent->rchild == tr) { parent->rchild = newRoot; } else { parent->lchild = newRoot; } } return newRoot; }
复制代码

运行结果:

4

以上来自:http://www.cnblogs.com/vamei/archive/2013/03/24/2976545.html


1、 概述

二叉查找树(Binary Search Tree,也叫二叉排序树,即Binary Sort Tree)能够支持多种动态集合操作,它可以用来表示有序集合、建立索引等,因而在实际应用中,二叉排序树是一种非常重要的数据结构。

从算法复杂度角度考虑,我们知道,作用于二叉查找树上的基本操作(如查找,插入等)的时间复杂度与树的高度成正比。对一个含n个节点的完全二叉树,这些操作的最坏情况运行时间为O(log n)。但如果因为频繁的删除和插入操作,导致树退化成一个n个节点的线性链(此时即为一个单链表),则这些操作的最坏情况运行时间为O(n)。为了克服以上缺点,很多二叉查找树的变形出现了,如红黑树、AVL树,Treap树等。

本文介绍了二叉查找树的一种改进数据结构–伸展树(Splay Tree)。它的主要特点是不会保证树一直是平衡的,但各种操作的平摊时间复杂度是O(log n),因而,从平摊复杂度上看,二叉查找树也是一种平衡二叉树。另外,相比于其他树状数据结构(如红黑树,AVL树等),伸展树的空间要求与编程复杂度要小得多。

2、 基本操作

伸展树的出发点是这样的:考虑到局部性原理(刚被访问的内容下次可能仍会被访问,查找次数多的内容可能下一次会被访问),为了使整个查找时间更小,被查频率高的那些节点应当经常处于靠近树根的位置。这样,很容易得想到以下这个方案:每次查找节点之后对树进行重构,把被查找的节点搬移到树根,这种自调整形式的二叉查找树就是伸展树。每次对伸展树进行操作后,它均会通过旋转的方法把被访问节点旋转到树根的位置。

为了将当前被访问节点旋转到树根,我们通常将节点自底向上旋转,直至该节点成为树根为止。“旋转”的巧妙之处就是在不打乱数列中数据大小关系(指中序遍历结果是全序的)情况下,所有基本操作的平摊复杂度仍为O(log n)。

伸展树主要有三种旋转操作,分别为单旋转,一字形旋转和之字形旋转。为了便于解释,我们假设当前被访问节点为X,X的父亲节点为Y(如果X的父亲节点存在),X的祖父节点为Z(如果X的祖父节点存在)。

(1)    单旋转

节点X的父节点Y是根节点。这时,如果X是Y的左孩子,我们进行一次右旋操作;如果X 是Y 的右孩子,则我们进行一次左旋操作。经过旋转,X成为二叉查找树T的根节点,调整结束。



(2)    一字型旋转

节点X 的父节点Y不是根节点,Y 的父节点为Z,且X与Y同时是各自父节点的左孩子或者同时是各自父节点的右孩子。这时,我们进行一次左左旋转操作或者右右旋转操作。

(3)    之字形旋转

节点X的父节点Y不是根节点,Y的父节点为Z,X与Y中一个是其父节点的左孩子而另一个是其父节点的右孩子。这时,我们进行一次左右旋转操作或者右左旋转操作。

3、伸展树区间操作

在实际应用中,伸展树的中序遍历即为我们维护的数列,这就引出一个问题,怎么在伸展树中表示某个区间?比如我们要提取区间[a,b],那么我们将a前面一个数对应的结点转到树根,将b 后面一个结点对应的结点转到树根的右边,那么根右边的左子树就对应了区间[a,b]。原因很简单,将a 前面一个数对应的结点转到树根后, a 及a 后面的数就在根的右子树上,然后又将b后面一个结点对应的结点转到树根的右边,那么[a,b]这个区间就是下图中B所示的子树。

利用区间操作我们可以实现线段树的一些功能,比如回答对区间的询问(最大值,最小值等)。具体可以这样实现,在每个结点记录关于以这个结点为根的子树的信息,然后询问时先提取区间,再直接读取子树的相关信息。还可以对区间进行整体修改,这也要用到与线段树类似的延迟标记技术,即对于每个结点,额外记录一个或多个标记,表示以这个结点为根的子树是否被进行了某种操作,并且这种操作影响其子结点的信息值,当进行旋转和其他一些操作时相应地将标记向下传递。

与线段树相比,伸展树功能更强大,它能解决以下两个线段树不能解决的问题:

(1) 在a后面插入一些数。方法是:首先利用要插入的数构造一棵伸展树,接着,将a 转到根,并将a 后面一个数对应的结点转到根结点的右边,最后将这棵新的子树挂到根右子结点的左子结点上。

(2)  删除区间[a,b]内的数。首先提取[a,b]区间,直接删除即可。

4、实现

代码全部来自【参考资料2】。

(1)旋转操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// node 为结点类型,其中ch[0]表示左结点指针,ch[1]表示右结点指针
 
// pre 表示指向父亲的指针
 
// Rotate函数用于(左/右)旋转x->pre
 
voidRotate(node *x, intd) // 旋转操作,d=0 表示左旋,d=1 表示右旋
 
{
 
  node *y = x->pre;
 
  Push_Down(y), Push_Down(x);
 
  // 先将Y 结点的标记向下传递(因为Y 在上面),再把X 的标记向下传递
 
  y->ch[! d] = x->ch[d];
 
  if(x->ch[d] != Null) x->ch[d]->pre = y;
 
  x->pre = y->pre;
 
  if(y->pre != Null)
 
  if(y->pre->ch[0] == y) y->pre->ch[0] = x; elsey->pre->ch[1] = x;
 
  x->ch[r] = y, y->pre = x, Update(y); // 维护Y 结点
 
  if(y == root) root = x; // root 表示整棵树的根结点
 
}

(2)splay操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
voidSplay(node *x, node *f) // Splay 操作,表示把结点x 转到结点f 的下面
 
{
 
  for(Push_Down(x) ; x->pre != f; ) // 一开始就将X 的标记下传
 
  if(x->pre->pre == f) // 父结点的父亲即为f,执行单旋转
 
    if(x->pre->ch[0] == x) Rotate(x, 1); elseRotate(x, 0);
 
  else
 
  {
 
    node *y = x->pre, *z = y->pre;
 
    if(z->ch[0] == y)
 
      if(y->ch[0] == x)
 
        Rotate(y, 1), Rotate(x, 1); // 一字形旋转
 
      else
 
        Rotate(x, 0), Rotate(x, 1); // 之字形旋转
 
    else
 
      if(y->ch[1] == x)
 
        Rotate(y, 0), Rotate(x, 0); // 一字形旋转
 
      else
 
        Rotate(x, 1), Rotate(x, 0); // 之字形旋转
 
  }
 
  Update(x);// 最后再维护X 结点
 
}

(3)将第k个数转到要求的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 找到处在中序遍历第k 个结点,并将其旋转到结点f 的下面
 
voidSelect(intk, node *f)
 
{
 
  inttmp;
 
  node *t;
 
  for(t = root; ; ) // 从根结点开始
 
  {
 
    Push_Down(t);// 由于要访问t 的子结点,将标记下传
 
    tmp = t->ch[0]->size; // 得到t 左子树的大小
 
    if(k == tmp + 1) break;// 得出t 即为查找结点,退出循环
 
    if(k <= tmp) // 第k 个结点在t 左边,向左走
 
      t = t->ch[0];
 
    else// 否则在右边,而且在右子树中,这个结点不再是第k 个
 
      k -= tmp + 1, t = t->ch[1];
 
  }
 
  Splay(t, f); // 执行旋转
 
}

5、 应用

(1)     数列维护问题

题目:维护一个数列,支持以下几种操作:

1. 插入:在当前数列第posi 个数字后面插入tot 个数字;若在数列首位插入,则posi 为0。

2. 删除:从当前数列第posi 个数字开始连续删除tot 个数字。

3. 修改:从当前数列第posi 个数字开始连续tot 个数字统一修改为c 。

4. 翻转:取出从当前数列第posi 个数字开始的tot 个数字,翻转后放入原来的位置。

5. 求和:计算从当前数列第posi 个数字开始连续tot 个数字的和并输出。

6. 求和最大子序列:求出当前数列中和最大的一段子序列,并输出最大和。

(2)     轻量级web服务器lighttpd中用到数据结构splay tree.

6、 参考资料

(1)     杨思雨《伸展树的基本操作与应用》

(2)     Crash《运用伸展树解决数列维护问题》

以上来自:http://dongxicheng.org/structure/splay-tree/
0 0
原创粉丝点击