AVL、Splay和性能分析(伪)

先说明一下,这个玩意是学校数据结构课的作业。完全是赶着ddl写完的,所以后面我都有点不知道自己在写什么。gprof也不太会,瞎弄的。主要放这里做个归档,如果对大家有帮助就更好了。当然,有问题也欢迎大家批评指正。

平衡二叉搜索树性能分析

问题描述

在AVL、Splay和红黑树三种平衡二叉搜索树中至少实现两种,需要支持以下操作:元素插入、删除和查询。在题目中,所有数据均为非负整数。查询时给出一非负整数 x x x,需要求出树中满足 y ≤ x y\le x yx的最大整数 y y y;若树中不存在满足条件的 y y y,查询返回-1。

数据结构

选择AVL和Splay树进行实现。

二叉搜索树(公共部分)

AVL和Splay树都基于二叉搜索树并共享部分操作。二叉搜索树的节点可由以下结构描述:

template <typename T>
struct BSTNode {
    T data;
    BSTNode *parent;
    union {
        struct {
            BSTNode *lc, *rc;
        };
        BSTNode *ch[2]; // 通用的子节点访问,`ch[0]`为左子,`ch[1]`为右子
    };
};

类型T在此题中为int。每个节点除实际数据外,保存指向父亲和左右孩子的指针。具体实现中可以使用ch[0]ch[1]访问相应的左右子节点,以节省重复的相似代码。一般而言,若某节点的父指针为空,则认为它是树的根节点。下文中以NodePtr代指树节点对应的指针类型。

二叉树结构维护roothot指针。root指向根节点,hot通常指向某次操作后目标节点的父节点。

查找

给定一个键值(key,或关键码),查找树中相应的节点。若该节点存在,返回指向该节点的指针(的引用);若不存在,则返回父节点中相应孩子指针的引用(此时该指针域的值为空,而它应当指向新节点)。从树的根节点开始,比较当前节点的数据值与待查找的值,并相应地前进到该节点的子节点,直到该子节点为空或查找成功。代码中需要注意树根为空或根节点即为目标节点的情况。另外,此查找函数具有副作用,会使得hot指向目标节点的父节点,这一特性会在其它函数中起作用。

节点信息更新

每个树节点可能会维护额外的信息,并在该节点与其他节点的连接关系发生改变后需要有对应的信息更新操作。基本的Splay节点无需此操作,而AVL节点需要维护高度。对于AVL节点,更新操作可由以下代码描述:

void AVLNode::update()
{
    this->height = 1 + max(get_height(lc), get_height(rc));
}
旋转

为了更好地描述旋转操作,定义节点o与其父亲的相对关系rel(o)如下:

int rel(NodePtr o)
{
    return o == o->parent->rc;
}

若节点o为其父亲的左孩子,rel(o)为0;若为右孩子,rel(o)为1。需要注意的是,调用此函数前应确保o的父指针o->parent非空,并且o应当确实为o->parent的左或右孩子。

然后引入辅助函数connect,用于将一对节点连接为父子:

void connect(NodePtr o, NodePtr p, int r)
{
    if (o) o->parent = p;
    if (p) p->ch[r] = o;
}

其中的判断用于处理含有空指针的情况。函数调用后,o成为p的相对关系为r子节点。

对于非根节点(父指针非空),定义对它的旋转操作为:将该节点旋转至其父亲的位置。旋转后,原父节点成为目标节点的子节点。代码如下所示:

void BST::rotate(NodePtr o)
{
    int r = rel(o);
    NodePtr p = o->parent, g = p->parent; // p为父亲,g为祖父
    connect(o->ch[r ^ 1], p, r); // 将o的原孩子过继给p以填充o的空位
    connect(o, g, g && rel(p)); // g为空时rel(p)不合法,需逻辑短路
    connect(p, o, r ^ 1); // 旋转后p将成为o的!r孩子
    p->update(); // 根据需求更新,此时p的高度较低,先p后o
    o->update();
    if (!g) root = o; // 如果g为空,认为原父亲p是树根,更新根节点
}

此函数的后三行需视具体情况修改。原祖父g也可能需要调用update()

节点删除

设节点o为待删除的节点(已通过查找定位到)。若o的某一子树为空(单分支),则可直接将其替换为另一子树以达到删除的目的。若o的左右子树均非空(双分支),那么可以找到o的直接后继ss必然无左孩子。然后将os互换,问题规约至删除单分支的s。代码如下所示:

NodePtr BST::remove_at(NodePtr o)
{
    int r;
    if (!o->lc || !o->rc) { // 单分支,只有左子/右子(也可能左右子均为空)
        r = o->rc ? 1 : 0;
        if (root == o) // 被删节点o可能为根
            root = o->ch[r];
    } else { // 双分支
        NodePtr s = successor(o); // o的直接后继s必定在以o为根的子树中
        std::swap(o->data, s->data);
        o = s; // 原o的后继被删去,树根root不会变化
        r = 1; // r设为1,o的左子必然为空,用其右子替代
    }
    NodePtr p = o->parent;
    connect(o->ch[r], p, p && rel(o)); // 短路逻辑
    if (p) p->update(); // 视具体情况修改
    delete o;
    return p; // 返回被实际删除节点的父节点
}

AVL

适度平衡

为了实现整棵树的适度平衡,AVL树中引入平衡因子。对于某AVL树节点,其对应子树的平衡因子定义如下:

int AVLNode::balance_factor() const
{
    return get_height(lc) - get_height(rc);
}

其中get_height函数接收树节点指针并返回该子树的高度(空树 => -1)。

AVL树的平衡条件要求对于树中的任意节点,都满足其平衡因子的绝对值不大于1( ∣ b a l F a c ∣ ≤ 1 |balFac|\le1 balFac1)。记 S ( h ) S(h) S(h)为高为 h h h的AVL树所含的最少节点数,那么有
S ( h ) = 1 + S ( h − 1 ) + S ( h − 2 ) S(h)=1+S(h-1)+S(h-2) S(h)=1+S(h1)+S(h2)
变形得
[ S ( h ) + 1 ] = [ S ( h − 1 ) + 1 ] + [ S ( h − 2 ) + 1 ] [S(h)+1]=[S(h-1)+1]+[S(h-2)+1] [S(h)+1]=[S(h1)+1]+[S(h2)+1]
这满足斐波那契数列的递推式。因为 S ( 0 ) = 1 S(0)=1 S(0)=1,所以
S ( h ) = f i b ( h + 3 ) − 1 S(h)=fib(h+3)-1 S(h)=fib(h+3)1
其中 f i b ( n ) fib(n) fib(n)是斐波那契数列的第 n n n项( f i b ( 1 ) = f i b ( 2 ) = 1 fib(1)=fib(2)=1 fib(1)=fib(2)=1)。由于 f i b ( n ) fib(n) fib(n) n n n指数增长,故由 n n n个节点构成的AVL树的高度 h = O ( log ⁡ n ) h=O(\log{n}) h=O(logn)

在一棵高为 h h h的二叉树中查找节点的时间复杂度为 O ( h ) O(h) O(h)。对于含 n n n个节点的AVL树,查找的时间复杂度即为 O ( log ⁡ n ) O(\log{n}) O(logn)

失衡与复衡

在向AVL树插入或删除节点后,某一子树可能失衡。若出现失衡,必然存在节点g,其对应子树的平衡因子绝对值为2;因此节点g必然有儿子p和孙子o,它们对应的子树高度参与了g子树的失衡。这也意味着子树op均为它们相应父亲的两个子树中较高的那个。

使用旋转可以解决局部g子树的失衡。参考课件中的示意图,当三代节点opg的关系为zig-zig或zag-zag(为了便于理解,后文用LL或RR替代,LR和RL同理)时,对p节点进行一次旋转就可以使得g复衡。而当它们的关系为zig-zag或zag-zig时,先将o旋转至p的原来位置,然后继续将o旋转至g的原来位置,可以让新的以o为根的子树复衡。

进行局部复衡的代码如下所示:

void AVL::rebalance(NodePtr o, NodePtr p)
{
    int ro = rel(o), rp = rel(p);
    if (ro == rp) // LL或RR,单旋父亲p
        rotate(p);
    else { // LR或RL,连续双旋o
        rotate(o);
        rotate(o);
        // 虽然旋转的是同一个节点,但其位置会两次改变
    }
}

调用这一函数前需保证相应的opg全部非空。单次旋转的时间复杂度为 O ( 1 ) O(1) O(1),所以局部复衡操作的时间复杂度也是 O ( 1 ) O(1) O(1)

节点插入

AVL树的节点插入基于二叉搜索树的节点插入。首先查找插入的值对应的节点,若该节点存在,则无需进一步操作;否则在目标位置创建新节点,该节点的父亲可通过hot指针访问。创建新节点后,更新其父亲的相关信息。

此时,AVL树的局部可能发生失衡。沿着新节点的父指针向上回溯,就能找到导致失衡的祖孙三代opg。参考课件上的示意图,当使用单旋或双旋进行局部复衡后,该子树的高度会复原(恢复到插入节点前的高度),所以树中的其它位置不会发生失衡,不必再继续向根回溯。

节点插入操作的代码如下所示:

bool AVL::insert(const T& v)
{
    NodePtr &x = search(v);
    if (x)
        return false; // 没有真正进行插入
    x = new AVLNode<T>(v, hot); // 注意:此构造函数并不将父亲(hot)的某个孩子指向新节点

    if (hot) hot->update(); // 更新父节点(如果非空)
    NodePtr o, p, g;
    for (o = x, p = hot; p && (g = p->parent); o = p, p = g) {
        // 祖父g不存在时不可能失衡
        if (!g->balanced()) {
            rebalance(o, p);
            break;
        }
        g->update();
    }
    return true; // 插入成功
}

节点插入操作需要进行一次查找,并可能回溯至根,时间复杂度 O ( log ⁡ n ) O(\log{n}) O(logn)

节点删除

AVL树的节点删除基于二叉搜索树的节点删除。在移除节点后,树局部可能发生失衡。但修复局部失衡后,该子树的高度未必复原,可能小于删除节点前的高度;子树高度变化可能导致更高祖先随之失衡,失衡的传播要求回溯必须进行至根节点,并处理所有的失衡。在回溯的过程中,无法由回溯路径上的节点直接得到op,而需要通过判断高度得到失衡子树g的较高儿子p和较高孙子o。(注:选择较高儿子时,若左右子高度相同,选择与父亲同侧的孩子)

节点删除操作的代码如下所示:

bool AVL::remove(const T& v)
{
    NodePtr o = search(v);
    if (!o)
        return false; // 节点不存在,删除失败
    
    NodePtr p, g;
    // remove_at为二叉搜索树的节点删除
    for (g = remove_at(o); g; g = g->parent) {
        if (!g->balanced()) {
            p = taller_child_of(g);
            o = taller_child_of(p);
            rebalance(o, p);
        }
        g->update();
    }
    return true;
}

节点删除操作需要进行一次查找,然后完整回溯至根节点,途中进行单次 O ( 1 ) O(1) O(1

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值