二叉搜索树(BST)

二叉搜索树的定义及性质

二叉搜索树(Binaty Search Tree)简称 BST,它拥有“BST 性质”,这也是平衡树的基础。

给定一棵二叉树,树上的每一个节点带有一个权值。所谓“BST 性质”是指,对于树中的任意一个节点:

  1. 该节点的权值不小于它的左子树中任意节点的权值;
  2. 该结点的权值不大于它的右子树中任意节点的权值。

满足上述性质的二叉树就是一棵 “二叉查找树”(BST)

比如:

同时珂以发现,它的中序遍历为 {1,2,3,4,5,6,7,8,9,10},就是将这所有权值从小到大排序后的结果。


二叉搜索树的一些操作


1. 建立

为了避免越界,减少边界情况的特殊判断,一般在 BST 中额外插入一个权值为 + 和一个权值为 的节点。
仅由这两个节点构成的 BST 就是一棵初始的空的 BST

为了方便起见,在接下来的操作中,假设 BST 不会含有权值相同的节点(即使有也可以用一个 cnt 数组记录出现的次数)。

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. 插入

BST 中插入一个新的值 val

BST 的检索过程相似。

在发现要走向的 p 无子节点,说明 val 不存在时,直接建立权值为 val 的新节点作为 p 的新节点。

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 的前驱是指在 BST 中权值小于 val 中的权值中最大的那个。同理,val 的后继是指在 BST 中权值大于 val 中的权值中最小的那个。

以求前驱为例,先初始化 ans 为权值为 + 的那个节点的编号,然后在 BST 中检索 val。在检索过程中,每经过一个节点 p,都尝试能不能让它更新ans 作为要求的前驱的候选答案。

检索完成后,有三种可能的结果:

  1. 没有找到 val,则 val 的前驱就是 ans

  2. 找到了权值为 val 的节点 p,但是 p 没有左子树,则 val 无前驱,ans 还是答案。

  3. 找到了权值为 val 的节点 p,且 p 有左子树,则说明答案是 p 左子树中的最大值,从 p 的左子节点出发,然后一直向右走,就找到了答案。

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. 删除

BST 中删除权值为 val 的节点。

首先通过检索找到权值为 val 的节点 p

p 是叶节点,则直接删除就行了。

p 有一个儿子,则删除 p,再让 p 的儿子代替 p 的位置。

p 有两个儿子,就有点麻烦了,要先求出 val 的后继节点 next,因为 next 无左子树,所以可以直接删除 next,并让 next 的右子树代替 next 的位置,再让 next 代替 p 的位置,删除 p 即可。

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);
}

二叉搜索树的时间复杂度

在随机数据中,BST 一次操作的期望时间复杂度为 O(logn),但是,要是按照顺序向 BST 中插入一个有序序列,BST 就会退化成一条链,这时候平均每次操作的时间复杂度就会变为 O(n)。这时候我们称这种左右子树大小相差很大的 BXT 是“不平衡”的。

而为了维持 BST 的平衡,就产生了许多数据结构,我们称之为平衡树

To be continue

posted @   Brilliant11001  阅读(75)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
点击右上角即可分享
微信分享提示