l2-004 这是二叉搜索树吗?_算法学习笔记(45): 二叉搜索树

980f7b928d1d3df09372cf212e634e6d.png

二叉搜索树Binary Search Tree, BST)是一种常用的数据结构,在理想情况下,它可以以

的复杂度完成一系列修改和查询,包括:
  • 插入一个数
  • 删除一个数
  • 查询某数的排名(排名定义为比该数小的数的个数+1)
  • 查询指定排名的数
  • 求某数的前驱(前驱定义为小于该数,且最大的数)
  • 求某数的后继(后继定义为大于该数,且最小的数)

维护一个有序的数组,配合二分查找,也可以实现这些操作,但插入和删除的复杂度是

。相对地,
链表可以
插入和删除,但其他操作的复杂度不够优秀。
二叉搜索树即BST对于以上每个操作,都拥有不差的复杂度。

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

540fab445aeb385395f482810ce89f7a.png

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

// 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]--;
}

求排名

因为排名被定义为比某数小的数+1,所以我们直接实现两个函数countlcountg,用来求比某数小的数的数量和比某数大的数的数量。(这两个函数后面也会用到)

countl为例,我们递归地求。如果要找的值比当前节点小,则向左边搜;反之则向右边搜,但这时要加上size[L[pos]]+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;
} 

求指定排名的数

与上面的方法类似,每个节点处判断应该往左边还是右边找,递归地往下搜寻。

int kth(int k, int pos = 1) // 求指定排名的数
{
    if (size[L[pos]] + 1 > k) // 答案在左,在左子树中找排名为k的数
        return kth(k, L[pos]);
    else if (size[L[pos]] + N[pos] < k)  // 答案在右,在右子树中找排名为k - size[L[pos]] - N[pos]的数
        return kth(k - size[L[pos]] - N[pos], R[pos]);
    else
        return val[pos];
}

注意,假如某个数的排名为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);
}

以上是二叉搜索树的介绍。然而,这种数据结构本身很少使用,因为它的各种操作复杂度是

,其中
为层数。只有在它大致平衡(平衡指所有叶子的深度趋于相同),才具有优秀的复杂度(当它是完全二叉树时,为
),然而这只是理想情况。假如我们依次加入6、5、4、3、2、1,BST的结构会是这样:

882895622bff5169ad69d7fbb16a5a2e.png

这样,BST就退化成了链表,复杂度变为

为了使BST能够大致平衡,人们想了很多种改进方法,它们称为平衡树。今后的笔记会介绍一些常用的平衡树。


Pecco:算法学习笔记(目录)​zhuanlan.zhihu.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值