【数据结构-树论】二叉查找树(BinarySearchTree)
引入
回想二叉树。
二叉树可扩展为两种数据结构:堆和平衡树。
定义及思想
满足 BST 性质的二叉树就是一颗“二叉查找树”。
BST 性质:
- 任意一个节点的值不小于其左子树的任意节点的值;
- 任意一个节点的值不大于其右子树的任意节点的值;
(这样:左子树
\(\leq\)父亲
\(\leq\)右子树
。) - 不存在重复节点。
作用及实现
通过将大小不同的两组数据分别置于根的两边,达到排序的效果。
节点:
要存 左孩子、右孩子、自身值、子树大小、本身数量(就是该值的数共有几个)。
点击查看代码
struct node {
int l, r, val, siz, cnt;
} tr[man];
插入:
首先判断:
-
若树还没有节点(
tot == 0
),则直接将根节点赋值(大小为 \(1\))。 -
否则执行插入操作:
递归进入适合的位置:- 当前节点的
val
与要插入的val
相同就直接将cnt
加一; - 大了就递归进入左子树,若没有左子树就将孩子赋值;
- 小了就递归进入右子树,若没有右子树就将孩子赋值。
(由于每个经过的点必然是将要插入节点的父亲,所以要将子树大小增加 \(1\)。)
- 当前节点的
①4
/ \
②2 ③6
/ \ / \
④1 N ⑤5 ⑥8
点击查看插入代码
void ins (int x, int val) {
++ tr[x].siz;
if (tr[x].val == val) ++ tr[x].cnt;
else if (tr[x].val > val)
if (ls) ins(ls, val);
else ls = ++ tot, tr[tot] = {0, 0, val, 1, 1};
else
if (rs) ins(rs, val);
else rs = ++ tot, tr[tot] = {0, 0, val, 1, 1};
return ;
}
查询值的排名:
(这里排名的定义是前面有几个小于本身的数 +1 而不是有几种小于本身的数 +1。)
在查询函数中只返回前面有几个小于本身,所以输出时要加一。
- 如果不小心进入了奇怪的节点返回 \(0\)(就不会影响排名);
- 如果值是相同返回左子树的大小;
- 否则如果当前值大于查询值就返回对左孩子的递归搜索;
- 否则(当前值小于查询值)就返回左子树大小、本身数量和对右孩子的递归搜索结果之和。
E.G.:
查询 5 的排名:先从根开始,4 < 5,进入右子树(+3);③6 > 5,进入左子树;5 = 5,左子树大小加本身 = 1,所以排名是 4。
①4
/ \
②2 ③6
/ / \
④1 ⑤5 ⑥8
点击查看查询排名代码
int querk (int x, int val) {
if (!x) return 0;
if (tr[x].val == val) return tr[ls].siz;
else if (tr[x].val > val) return querk(ls, val);
else return tr[ls].siz + tr[x].cnt + querk(rs, val);
}
查询某个排名上的值:
排名意为包括自己比自己小的有几个。
先从根开始搜索:
- 如果不小心进入了奇怪的节点返回 \(0\);
- 如果左子树的大小大于等于排名(该排名的数在左子树上),就返回对左子树递归搜索;
- 否则如果左子树的大小加上本身数量大于等于排名(本身就是这个排名了),就返回自己的值;
- 否则(该排名的数在右子树上)就返回对右孩子的递归搜索结果(那么递归进去的排名要减去左子树大小和本身数量)。
E.G.:
第 4 大:先从根开始,左子树加上本身 = 3 < 4,进入右子树,递归进去(4-3=)1;③的左子树大小 = 1,所以结果是 ⑤5。
①4
/ \
②2 ③6
/ / \
④1 ⑤5 ⑥8
点击查看查询值代码
int find (int x, int rk) {
if (!x) return 0;
if (tr[ls].siz >= rk) return find(ls, rk);
else if (tr[ls].siz+tr[x].cnt >= rk) return tr[x].val;
else return find(rs, rk-tr[ls].siz-tr[x].cnt);
}
查询前驱后继:
前驱:在数列中小于自己的最大的数;
后继:在数列中大于自己的最小的数。
这是前驱:
先设置一个答案,再从根开始搜索:
如果当前值等于查询值,那么先进入他的左子树,再找左子树中最靠右的叶子,直接退出;
否则判断当前值是否小于查询值且大于已记录答案值,若成立,更改答案。
从当前节点进入左子树或右子树(看当前值和查询值的大小关系),再进行下一轮循环。
返回的时候判断当前是否是真正的节点,若不是返回约定值。
点击查看前驱代码
int finlst (int val){
int res = miv, x = 1;
while (x) {
if (val == tr[x].val) {
if (ls) {
x = ls;
while (rs) x = rs;
res = tr[x].val;
} break;
} if (tr[x].val<val && tr[x].val>res) res = tr[x].val;
x = val<tr[x].val? ls : rs;
} return res;
}
点击查看后继代码
int finext (int val) {
int res = mav, x = 1;
while (x) {
if (val == tr[x].val) {
if (rs) {
x = rs;
while (ls) x = ls;
res = tr[x].val;
} break;
} if (tr[x].val>val && tr[x].val<res) res = tr[x].val;
x = val<tr[x].val? ls: rs;
} return res;
}
E.G. 1:
6 的前驱:先从根开始,进入第二个 if,res 更新为 ①4,进入 x 的右孩子,当前值 == 6,进入左子树,找到叶子,答案是 ⑤5.
①4
/ \
②2 ③6
/ \ / \
④1 N ⑤5 ⑥8
E.G. 2:
N 的后继:先从根开始,进入左孩子,res 更新为 ①4,进入 ② 的右孩子,当前值 == N,没有右子树,无法更新,答案是 ①4.
删除
完整代码
class BST {
public:
int tot;
struct node {
int l, r, val, siz, cnt;
} tr[man];
#define ls tr[x].l
#define rs tr[x].r
void ins (int x, int val) {
++ tr[x].siz;
if (tr[x].val == val) ++ tr[x].cnt;
else if (tr[x].val > val)
if (ls) ins(ls, val);
else ls = ++ tot, tr[tot] = {0, 0, val, 1, 1};
else
if (rs) ins(rs, val);
else rs = ++ tot, tr[tot] = {0, 0, val, 1, 1};
return ;
}
int querk (int x, int val) {
if (!x) return 0;
if (tr[x].val == val) return tr[ls].siz;
else if (tr[x].val > val) return querk(ls, val);
else return tr[ls].siz + tr[x].cnt + querk(rs, val);
}
int find (int x, int rk) {
if (!x) return 0;
if (tr[ls].siz >= rk) return find(ls, rk);
else if (tr[ls].siz+tr[x].cnt >= rk) return tr[x].val;
else return find(rs, rk-tr[ls].siz-tr[x].cnt);
}
int finlst (int val) {
int res = miv, x = 1;
while (x) {
if (val == tr[x].val) {
if (ls) {
x = ls;
while (rs) x = rs;
res = tr[x].val;
} break;
} if (tr[x].val<val && tr[x].val>res) res = tr[x].val;
x = val<tr[x].val? ls : rs;
} return res;
}
int finext (int val) {
int res = mav, x = 1;
while (x) {
if (val == tr[x].val) {
if (rs) {
x = rs;
while (ls) x = ls;
res = tr[x].val;
} break;
} if (tr[x].val>val && tr[x].val<res) res = tr[x].val;
x = val<tr[x].val? ls: rs;
} return res;
}
#undef ls
#undef rs
} B;