二叉搜索树

二叉搜索树

二叉搜索树(Binary Search Tree, BST)是一种常用的数据结构,在理想情况下,它可以以 的复杂度完成一系列修改和查询,包括:

  • 插入一个数
  • 删除一个数
  • 查询某数的排名(排名定义为比该数小的数的个数+1)
  • 查询指定排名的数求某数的前驱(前驱定义为小于该数,且最大的数)
  • 求某数的后继(后继定义为大于该数,且最小的数)

维护一个有序的数组,配合二分查找,也可以实现这些操作,但插入和删除的复杂度是0(N) 。相对地,链表可以 0(1)插入和删除,但其他操作的复杂度不够优秀。二叉搜索树即BST对于以上每个操作,都拥有不差的复杂度。

例如,如果我们依次插入6、2、5、1、7、4、2、5、9、5,得到BST的结构如下:

在这里插入图片描述
显然,二叉搜索树是一个二叉树,每个节点对应的左子树中的所有数都小于它,右子树中的所有数都大于它。而且每个节点对应的左右子树也是二叉搜索树(这是一个天然递归的结构)。由于我们这里维护的是可重集,所以每个节点还带有额外信息,即该节点储存的数出现的次数。此外,为了方便查询排名,我们还保存每个子树的大小。

// L: 左子树根节点编号,R:右子树根节点编号,N:该节点储存的数出现的次数
// val:该节点储存的数,size:以该节点为根节点的子树的节点数目(即子树大小)
int L[MAXN], R[MAXN], N[MAXN], val[MAXN], size[MAXN], cnt = 1;

现在我们分别实现上述的操作。

插入

从根节点开始,递归地搜索。若插入的值小于当前节点的值,则向左搜;反之向右搜。这样最后如果找到一个已有节点,则令其计数+1;否则若到达空节点,则用该节点存储这个值。

void insert(int v, int pos = 1) // 插入
{
    size[pos]++; // 树大小+1
    if (N[pos] == 0 && L[pos] == 0 && R[pos] == 0) // 空节点
    {
        val[pos] = v;
        N[pos] = 1;
    }
    else if (v < val[pos]) // 向左搜索
    {
        if (L[pos] == 0) // 如果应该向左搜,但不存在左节点,则创建一个新节点
            L[pos] = ++cnt;
        insert(v, L[pos]);
    }
    else if (v > val[pos]) // 向右搜索
    {
        if (R[pos] == 0)
            R[pos] = ++cnt;
        insert(v, R[pos]);
    }
    else // 已经存在值相同的节点
        N[pos]++;
}

删除

这里直接采取惰性删除的方法:找到要删除的数,令其计数-1。这样写起来比较简单,不用进行较复杂的分类讨论,而且不会增加时间复杂度。稍微注意一下,采取惰性删除时判断一个节点是不是空节点要用N[pos]==0 && L[pos]==0 && R[pos]==0 而不能仅仅判断N,因为N[pos]==0的点也可能是被删除的中间节点。(注意被删除的叶子节点可以当作空节点处理)

void remove(int v, int pos = 1) // 删除
{
    size[pos]--; // 树大小-1
    if (v < val[pos])
        remove(v, L[pos]);
    else if (v > val[pos])
        remove(v, R[pos]);
    else
        N[pos]--;
}

求排名

int countl(int v, int pos = 1) // 求比某数小的数的个数
{
    if (v < val[pos])
        return L[pos] ? countl(v, L[pos]) : 0;
    else if (v > val[pos])
        return size[L[pos]] + N[pos] + (R[pos] ? countl(v, R[pos]) : 0);
    else
        return size[L[pos]];
}

而countg完全类似。

int countg(int v, int pos = 1) // 求比某数大的数的个数
{
    if (v > val[pos])
        return R[pos] ? countg(v, R[pos]) : 0;
    else if (v < val[pos])
        return size[R[pos]] + N[pos] + (L[pos] ? countg(v, L[pos]) : 0);
    else
        return size[R[pos]];
}

rank函数其实可以不写了:

int rank(int v)
{
    return countl(v) + 1;
} 

注意,假如某个数的排名为2,且它出现了3次,那么这个函数传入2、3、4都会返回这个数,这也提供了一些方便。

求前驱

根据我们kth函数的性质,直接找到排名比当前数小1的那个数即可。

int pre(int v) // 求前驱
{
    int r = countl(v);
    return kth(r);
}

求后继

后继的排名则是小于等于当前数的数的数量+1。

int suc(int v) // 求后继
{
    int r = size[1] - countg(v) + 1;
    return kth(r);
}

以上是二叉搜索树的介绍。然而,这种数据结构本身很少使用,因为它的各种操作复杂度是O(h) ,其中h 为层数。只有在它大致平衡(平衡指所有叶子的深度趋于相同),才具有优秀的复杂度(当它是完全二叉树时,为O(logn) ),然而这只是理想情况。假如我们依次加入6、5、4、3、2、1,BST的结构会是这样:

在这里插入图片描述
这样,BST就退化成了链表,复杂度变为 。为了使BST能够大致平衡,人们想了很多种改进方法,它们称为平衡树。今后的笔记会介绍一些常用的平衡树。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值