面试官问我:什么是“伸展树”?

伸展树(Splay Tree)是一种自平衡二叉查找树,由 Daniel Sleator 和 Robert Tarjan 发明。它通过旋转操作来保持树的平衡,避免查找效率降低到链表水平。本文详细介绍了伸展树的结点、旋转操作(左旋、右旋)、伸展(Splay)过程,以及查找、查询排名、插入、删除等操作,同时对比了伸展树与其他平衡树的区别。
摘要由CSDN通过智能技术生成

图片

学过数据结构的小伙伴,一定都知道二叉查找树,也叫二叉排序树,英文缩写是BST。

为了维持二叉查找树的高效率查找,就需要对二叉查找树进行平衡调整。在数据结构当中大名鼎鼎的红黑树AVL,就是典型的自平衡二叉查找树。

今天,我们来介绍一种更有意思的自平衡二叉树:伸展树。它的英文名字是Splay Tree

Part 1 为什么要伸展

我们来回顾一下,二叉搜索树满足:左子结点 < 当前结点 < 右子结点 

为什么要有平衡树呢?因为当二叉搜索树如下图“瘸腿”时,搜索左侧的结点,原来的速度  会掉到 ,与链表一个速度,失去了价值。

图片

 

为了避免树瘸腿,我们可以通过适当的旋转来调整树的结构。

伸展树的核心,是通过不断的将结点旋转到根结点(即为splay操作),来保证整棵树不会跟链表一样。它由 Daniel Sleator 和 Robert Tarjan 发明 。

1.1 结点

node中记录的信息:

  • parent:父结点的指针。

  • child[0/1]child[0]为左子结点的指针,child[1]为右子结点的指针。

  • value:这个结点存了啥。

  • count:二叉树中不存在两个值相同的结点,如果需要记录多个就需要一个变量来记录这个数值出现了多少次。(count就是来干这个活的)

  • size:这个结点为根结点的子树中有多少个结点。

基础操作:

  • maintain:更新结点的size。(更新要自底向上)

  • get:获取自己的类型,0为左子结点,1为右子结点。

  • connect: 连接两个结点。

class node {
  public:
      node *parent; // 父结点的指针
      node *child[2]; // child[0]为左子结点的指针,child[1]为右子结点的指针。
      int value, count, size; // 数据,出现次数,子树大小

      node(int _value) {
          value = _value;
          parent = child[0] = child[1] = NULL;
          count = size = 1;
      }
  };

我们把对指针是否为NULL提到了两个基础操作中,所以只能放在伸展树的类中。

destroy:销毁整个树。

因为结点使用的是堆空间(new出来的),所以必须要销毁(delete),否则会内存泄漏。

class splayTree {
public:

    node *root;
    
    splayTree() {
        root = NULL;
    }
    
    ~splayTree() {
        destroy(root); // 从root开始销毁
    }

    void destroy(node *current) { // 销毁结点
        if (current) {
            destroy(current->child[0]); // 后序遍历
            destroy(current->child[1]);
            delete current;
        }
    }
    
    void maintain(node *current) { // 更新size
        if (current == NULL) { // 可能传入的是一个空指针
            return;
        }
        current->size = current->count; // 先将自己加上
        if (current->child[0]) { // 防止没儿子,NULL,报错
            current->size += current->child[0]->size; // 加上儿子
        }
        if (current->child[1]) {
            current->size += current->child[1]->size;
        }
    }

    int get(node *current) { // 获得结点是左结点还是右结点,0左1右
        if (current == NULL || current->parent == NULL) { // 传入空指针;根结点没有父亲,特判一下
            return -1;
        }
        return current->parent->child[1] == current; // 父亲的右子结点 是不是 自己
    }
    
    void connect(node *parent, node *current, int type) { // 父结点指针,当前结点指针,类型(0左1右)
        if (parent) { // parent 可能为NULL
            parent->child[type] = current;
        }
        if (current) { // current 也可能为NULL
            current->parent = parent;
        }
    }
};

可能会有读者好奇:这个size是用来干什么的呢?别急,等到查询排名时就会用到。

1.2 左旋 & 右旋

通过旋转,我们能在保证旋转可以保证左子结点 < 当前结点 < 右子结点的情况下调整结点之间的关系。

旋转有两种定义

  1. 以x为根的子树进行旋转。

  2. 把x向上旋转。

1.2.1 以x为根的子树进行旋转

 

1.2.2 把x向上旋转

 

上面的动画使用文字叙述即为:

  • 左旋

    1. 将被旋转结点的左结点变为父结点的右结点。

    2. 父结点变为被旋转结点的左子结点

    3. 原父结点的父结点(爷爷)变为被旋转结点的父结点。

  • 右旋

    1. 将被旋转结点的右结点变为父结点的左结点。

    2. 父结点变为被旋转结点的右子结点

    3. 原父结点的父结点(爷爷)变为被旋转结点的父结点。

虽然1、2两种旋转定义不同,但是只看移动这分明就是一种嘛。

有细心的读者发现:左右旋的方式与AVL、红黑树等其他二叉树相同。
因为这是唯一一种不改变中序遍历的旋转方式。

左旋与右旋都不会改变中序遍历的结果,如上方动图,中序遍历始终为1 y 2 x 3

除了举例论证,你也可以这样理解:

这是因为左旋和右旋会保证旋转后的二叉树左子结点 < 当前结点 < 右子结点,因为旋转没有插入或删除结点,所以二叉树的中序遍历没变。

1.2.3 合并左右旋

想要合并左右旋,只能使用定义二:把x向上旋转。(原因见下)

左旋和右旋虽然属于两种操作,但是细想想:
一个结点不是左子结点,就是右子结点。因为我们要将结点向上旋转,所以每一个结点(除了根结点),只能朝一个方向旋转,也就是父结点的方向。

所以可以将两种操作整合为一种:
当被旋转的结点的类型为左子结点时,进行右旋
当被旋转的结点的类型为右子结点时,进行左旋<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值