二叉搜索树(BST)
Brilliant11001
2024-01-31 17:33:47
## 二叉搜索树的定义及性质
二叉搜索树($\texttt{Binaty Search Tree}$)简称 $\texttt{BST}$,它拥有“$\texttt{BST}$ 性质”,这也是平衡树的基础。
给定一棵二叉树,树上的每一个节点带有一个权值。所谓“$\texttt{BST}$ 性质”是指,对于树中的任意一个节点:
1. 该节点的权值不小于它的左子树中任意节点的权值;
2. 该结点的权值不大于它的右子树中任意节点的权值。
满足上述性质的二叉树就是一棵 **“二叉查找树”($\texttt{BST}$)**。
比如:
![](https://cdn.luogu.com.cn/upload/image_hosting/3jph0xlt.png)
同时珂以发现,它的中序遍历为 $\{1,2,3,4,5,6,7,8,9,10\}$,就是将这所有权值从小到大排序后的结果。
------------
## 二叉搜索树的一些操作
------------
### 1. 建立
为了避免越界,减少边界情况的特殊判断,一般在 $\texttt{BST}$ 中额外插入一个权值为 $+\infty$ 和一个权值为 $-\infty$ 的节点。
仅由这两个节点构成的 $\texttt{BST}$ 就是一棵初始的空的 $\texttt{BST}$。
为了方便起见,在接下来的操作中,假设 $\texttt{BST}$ 不会含有权值相同的节点(即使有也可以用一个 $cnt$ 数组记录出现的次数)。
```cpp
struct BST {
int ls, rs;
int val;
int cnt;
int siz;
}a[N];
int tot, root, inf = 0x7fffffff;
int New(int val) {
a[++tot].val = val;
return tot;
}
void build() {
New(-inf);
New(inf);
root = 1, a[1].rs = 2;
}
```
### 2. 检索
这个比较简单,就不放代码了,只给出思路。
假如说要找一个权值为 $val$ 的节点。
设当前搜索到的节点为 $p$,则一开始 $p = root = 1$。
1. 若 $a[p].val == val$,则已经找到;
2. 若 $a[p].val < val$
(1)若 $p$ 无左子节点,则说明不存在 $val$。
(2)若 $p$ 有左子节点,则继续检索 $p$ 的左子树。
3. 若 $a[p].val > val$,
(1)若 $p$ 无右子节点,则说明不存在 $val$。
(2)若 $p$ 有右子节点,则继续检索 $p$ 的右子树。
### 3. 插入
在 $\texttt{BST}$ 中插入一个新的值 $val$。
与 $\texttt{BST}$ 的检索过程相似。
在发现要走向的 $p$ 无子节点,说明 $val$ 不存在时,直接建立权值为 $val$ 的新节点作为 $p$ 的新节点。
```cpp
void insert(int &p, int val) { //注意p是引用,其父节点的 l 或 r 值会被同时更新
if(p == 0) {
p = New(val);
return ;
}
if(val == a[p].val) {
a[p].cnt++;
return ;
}
if(val < a[p].val) insert(a[p].l, val);
else insert(a[p].r, val);
}
```
### 4. 求前驱/后缀
$val$ 的前驱是指在 $\texttt{BST}$ 中权值小于 $val$ 中的权值中最大的那个。同理,$val$ 的后继是指在 $\texttt{BST}$ 中权值大于 $val$ 中的权值中最小的那个。
以求前驱为例,先初始化 $ans$ 为权值为 $-\infty$ 的那个节点的编号,然后在 $\texttt{BST}$ 中检索 $val$。在检索过程中,每经过一个节点 $p$,都尝试能不能让它更新 $ans$ 作为要求的前驱的候选答案。
检索完成后,有三种可能的结果:
1. 没有找到 $val$,则 $val$ 的前驱就是 $ans$。
2. 找到了权值为 $val$ 的节点 $p$,但是 $p$ 没有左子树,则 $val$ 无前驱,$ans$ 还是答案。
3. 找到了权值为 $val$ 的节点 $p$,且 $p$ 有左子树,则说明答案是 $p$ 左子树中的最大值,从 $p$ 的左子节点出发,然后一直向右走,就找到了答案。
```cpp
int get_pre(int val) { //求前驱
int ans = 1; //a[1].val = -0x7fffffff
int p = root;
while(p) {
if(val == a[p].val) { //检索成功
if(a[p].ls) {
p = a[p].ls; //从左子节点出发
while(a[p].rs) p = a[p].rs; //一直向右走
ans = p;
}
break;
}
//每经过一个点,都尝试更新前驱
if(val > a[p].val && a[p].val > a[ans].val) ans = p;
p = val > a[p].val ? a[p].rs : a[p].ls; //检索
}
return ans;
}
int get_next(int val) { //求后继
int ans = 2; //a[2].val = 0x7fffffff
int p = root;
while(p) {
if(val == a[p].val) {
if(a[p].rs) {
p = a[p].rs;
while(a[p].ls) p = a[p].ls;
ans = p;
}
break;
}
if(val < a[p].val && a[p].val < a[ans].val) ans = p;
p = val > a[p].val ? a[p].rs : a[p].ls;
}
return ans;
}
//递归写法
int get_pre(int p, int val) {
if(!p) return -inf;
if(val <= tr[p].val) return get_pre(tr[p].ls, val);
return max(tr[p].val, get_pre(tr[p].rs, val));
}
int get_next(int p, int val) {
if(!p) return inf;
if(val >= tr[p].val) return get_next(tr[p].rs, val);
return min(tr[p].val, get_next(tr[p].ls, val));
}
```
### 5. 删除
从 $\texttt{BST}$ 中删除权值为 $val$ 的节点。
首先通过检索找到权值为 $val$ 的节点 $p$。
若 $p$ 是叶节点,则直接删除就行了。
若 $p$ 有一个儿子,则删除 $p$,再让 $p$ 的儿子代替 $p$ 的位置。
若 $p$ 有两个儿子,就有点麻烦了,要先求出 $val$ 的后继节点 $next$,因为 $next$ 无左子树,所以可以直接删除 $next$,并让 $next$ 的右子树代替 $next$ 的位置,再让 $next$ 代替 $p$ 的位置,删除 $p$ 即可。
```cpp
void remove(int &p, int val) {
if(p == 0) return ;
if(val == a[p].val) {
if(!a[p].ls) p = a[p].rs; //没有左子树,则右子树代替 p 的位置,注意 p 是引用
else if(!a[p].rs) p = a[p].ls; //没有右子树,则左子树代替 p 的位置,注意 p 是引用
else { //有两个儿子
//求后继节点
int next = a[p].rs;
while(a[next].ls > 0) next = a[next].ls;
//next 一定无左子树
remove(a[p].rs, a[next].val);
//让节点 next 代替节点 p 的位置
a[next].ls = a[p].ls, a[next].rs = a[p].rs;
p = next;
}
return ;
}
if(val < a[p].val) remove(a[p].ls, val);
else remove(a[p].rs, val);
}
```
## 二叉搜索树的时间复杂度
在随机数据中,$\texttt{BST}$ 一次操作的期望时间复杂度为 $O(\log n)$,但是,要是按照顺序向 $\texttt{BST}$ 中插入一个有序序列,$\texttt{BST}$ 就会退化成一条链,这时候平均每次操作的时间复杂度就会变为 $O(n)$。这时候我们称这种左右子树大小相差很大的 $\texttt{BXT}$ 是“不平衡”的。
**而为了维持 $\texttt{BST}$ 的平衡,就产生了许多数据结构,我们称之为平衡树**。
### $\texttt{To be continue}\dots \dots$