![980f7b928d1d3df09372cf212e634e6d.png](https://i-blog.csdnimg.cn/blog_migrate/b3ec085dd533917b7305899e5208b309.jpeg)
二叉搜索树(Binary Search Tree, BST)是一种常用的数据结构,在理想情况下,它可以以
- 插入一个数
- 删除一个数
- 查询某数的排名(排名定义为比该数小的数的个数+1)
- 查询指定排名的数
- 求某数的前驱(前驱定义为小于该数,且最大的数)
- 求某数的后继(后继定义为大于该数,且最小的数)
维护一个有序的数组,配合二分查找,也可以实现这些操作,但插入和删除的复杂度是
例如,如果我们依次插入6、2、5、1、7、4、2、5、9、5,得到BST的结构如下:
![540fab445aeb385395f482810ce89f7a.png](https://i-blog.csdnimg.cn/blog_migrate/6e51ebef2bac5e9e07ae04b616a5dd88.jpeg)
显然,二叉搜索树是一个二叉树,每个节点对应的左子树中的所有数都小于它,右子树中的所有数都大于它。而且每个节点对应的左右子树也是二叉搜索树(这是一个天然递归的结构)。由于我们这里维护的是可重集,所以每个节点还带有额外信息,即该节点储存的数出现的次数。此外,为了方便查询排名,我们还保存每个子树的大小。
// 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,所以我们直接实现两个函数countl
和countg
,用来求比某数小的数的数量和比某数大的数的数量。(这两个函数后面也会用到)
以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);
}
以上是二叉搜索树的介绍。然而,这种数据结构本身很少使用,因为它的各种操作复杂度是
![882895622bff5169ad69d7fbb16a5a2e.png](https://i-blog.csdnimg.cn/blog_migrate/829136c3d4b0a26425587bb40f602c04.jpeg)
这样,BST就退化成了链表,复杂度变为
为了使BST能够大致平衡,人们想了很多种改进方法,它们称为平衡树。今后的笔记会介绍一些常用的平衡树。
Pecco:算法学习笔记(目录)zhuanlan.zhihu.com