1. 二叉搜索树的定义
二叉搜索树 (BST) 递归定义为具有以下属性的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
- 若它的右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值
- 它的左、右子树也分别为二叉搜索树
二叉搜索树有一个特殊的性质:二叉搜索树的中序遍历是一个有序序列。
2. 二叉搜索树经典模板
这里的二叉搜索树的构建是基于一个序列元素的依次插入。
2.1 插入操作(建树操作)
二叉搜索树的插入操作时间复杂度是 O ( h ) O(h) O(h)。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 2010, INF = 1e8;
// idx 代表下一个可以使用的空白结点,其中 0 代表空状态,第1个结点位置应该是从1开始
int l[N], r[N], v[N], idx;
// 最核心的插入操作
void insert(int& u, int w)
{
if (!u)
{
u = ++idx; // 类似于Trie字典树,0作为空状态不用(Trie中0代表根节点)
v[u] = w;
}
else if (w < v[u]]) insert(l[u], w);
else insert(r[u], w);
}
int main()
{
int n;
scanf("%d", &n); // 要插入结点的个数
int root = 0;
for (int i = 0; i < n; i++)
{
int x;
scanf("%d", &x);
insert(root, x); // 依次插入到二叉搜索树中
}
}
2.2 删除操作
删除结点操作需要分三种情况讨论:
(1)若删除的结点是叶结点:直接删除;
(2)若删除的结点仅有一个子树:直接将子树根节点替换到删除结点的位置上;
【解释】:直接用儿子结点替换删除结点是不会影响中序遍历有序性的。假设待删除结点只有左子树,则左子树上所有结点都是小于删除结点、大于或等于待删除结点的父结点的,因此用左子树的根替换待删除结点并不会破坏整棵树中序遍历的有序性。
(3)若删除的结点同时拥有左右子树:则将待删除结点的前驱结点(或后继结点)的值直接覆盖到待删除结点上,然后删除前驱结点(或后继结点)。这里的前驱结点是指“待删除结点的左子树中最大结点”,后继结点指的是“待删除结点右子树中最小结点”。
【解释】:如果放在二叉搜索树中序遍历序列中,可以理解为下图:
二叉搜索树的删除操作时间复杂度是
O
(
h
)
O(h)
O(h)。
// 删除操作
void remove(int& u, int w)
{
if (!u) return; // 如果找不到要删除的值,直接结束
if (w < v[u]) remove(l[u], w);
else if (w > v[u]) remove(r[u], w);
else
{
if (!l[u] && !r[u]) u = 0; // 如果是叶子结点,直接删除
else if (!r[u]) u = l[u]; // 如果右子树为空,则左子树替换
else if (!l[u]) u = r[u]; // 如果左子树为空,则右子树替换
else
{
int p = l[u];
// 找到左子树的最大值
while (r[p]) p = r[p];
v[u] = v[p]; // 直接将值进行覆盖,注意不是替换
remove(l[u], v[p]); // 然后去左子树中删掉前驱
}
}
}
2.3 查询二叉搜索树中值为 w 的前驱/后继数值
即查询二叉搜索树中,比 w w w 大的第一个数,和比 w w w 小的第一个数。
查找前驱数值
// 查找树中小于 w 的最大值
void get_pre(int u, int w)
{
if (!u) return -INF; // 说明没找到
if (v[u] >= w) return get_pre(l[u], w);
return max(v[u], get_pre(r[u], w));
}
查找后继数值
// 查找树中大于 w 的最小值
void get_post(int u, int w)
{
if (!u) return INF;
if (v[u] <= w) return get_post(r[u], w);
return min(v[u], get_post(l[u], w));
}
3. 经典例题
题目来源:AcWing 3786. 二叉排序树
题目描述
你需要写一种数据结构,来维护一些数,其中需要提供以下操作:
- 插入数值 x x x。
- 删除数值 x x x。
- 输出数值 x x x 的前驱(前驱定义为现有所有数中小于 x x x 的最大的数)。
- 输出数值 x x x 的后继(后继定义为现有所有数中大于 x x x 的最小的数)。
题目保证:
操作
1
1
1 插入的数值各不相同。
操作
2
2
2 删除的数值一定存在。
操作
3
3
3 和
4
4
4 的结果一定存在。
输入格式
第一行包含整数
n
n
n,表示共有
n
n
n 个操作命令。
接下来 n n n 行,每行包含两个整数 o p t opt opt 和 x x x,表示操作序号和操作数值。
输出格式
对于操作
3
,
4
3,4
3,4,每行输出一个操作结果。
数据范围
1
≤
n
≤
2000
,
1≤n≤2000,
1≤n≤2000,
−
10000
≤
x
≤
10000
−10000≤x≤10000
−10000≤x≤10000
输入样例:
6
1 1
1 3
1 5
3 4
2 3
4 2
输出样例:
3
5
AC代码如下:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 2010, INF = 1e8;
int l[N], r[N], v[N], idx;
int n;
// 二叉搜索树的插入
void insert(int& u, int w)
{
if (!u)
{
u = ++idx;
v[u] = w;
}
else if (w < v[u]) insert(l[u], w);
else insert(r[u], w);
}
// 二叉搜索树的删除操作
void remove(int& u, int w)
{
if (!u) return; // 找不到w
if (w < v[u]) remove(l[u], w);
else if (w > v[u]) remove(r[u], w);
else
{
if (!l[u] && !r[u]) u = 0;
else if (!r[u]) u = l[u];
else if (!l[u]) u = r[u];
else
{
int p = l[u];
while (r[p]) p = r[p];
v[u] = v[p];
remove(l[u], v[p]);
}
}
}
// 查询树中比w小的最大的数
int get_pre(int u, int w)
{
if (!u) return -INF;
if (v[u] >= w) return get_pre(l[u], w);
return max(v[u], get_pre(r[u], w));
}
// 查询树中比w大的最小的数
int get_post(int u, int w)
{
if (!u) return INF;
if (v[u] <= w) return get_post(r[u], w);
return min(v[u], get_post(l[u], w));
}
int main()
{
scanf("%d", &n);
int root = 0;
while (n--)
{
int t, x;
scanf("%d%d", &t, &x);
if (t == 1) insert(root, x);
else if (t == 2) remove(root, x);
else if (t == 3) printf("%d\n", get_pre(root, x));
else printf("%d\n", get_post(root, x));
}
}