[C++ 学习笔记] 二叉搜索树-BST

1. 基本知识


BST(Binary Search Tree),二叉搜索树,又叫二叉排序树,是一棵空树或满足以下几种性质的树

  1. 若左子树不空,则左子树上所有节点的值均小于根节点的值
  2. 若右子树不空,则右子树上所有节点的值均大于根节点的值
  3. 左、右子树也分别为二叉搜索树
  4. 没有权值相等的节点

对于任一棵非空二叉搜索树,容易发现,其中序遍历单调递增

2. 初始化二叉搜索树


一般二叉搜索树的一个节点有如下属性:

  1. 当前节点的权值
  2. 左孩子的下标
  3. 右孩子的下标
  4. 计数器,代表当前的值出现了几次
  5. 子树大小(包括自己) => 这个属性用来求节点的 “排名”

于是可以得到节点的代码:

struct Node{
    int val;    // 权值
    int lhs;    // 左孩子下标
    int rhs;    // 右孩子下标
    int cnt;    // 计数器
    int siz;    // 子树大小
}BST[1000005];

3. 添加节点


对于要添加的节点的值 (不妨设为 val),按照以下的步骤添加节点:

  1. 比较 val 与当前节点的值
  2. 如果 val> 当前节点的值,则搜索当前节点的右子树(若没有右子树就直接赋为右孩子
  3. 如果 val < 当前节点的值,则搜索当前节点的左子树(若没有左子树就直接赋为左孩子

图例:

目标:将权值为 4 的点插入到给出的二叉搜索树中

1. 将 4 与根节点的值 5 比较,发现 4<5,于是搜索其左子树

2. 将 4 与左子树的根节点的值 3 比较,发现 4>3,于是搜索其右子树

3. 发现当前节点没有右子树,于是将其右子树指向要插入的节点

插入完毕!

于是可以得到插入的代码:

void insert_node(int val, int pos) {    // 要插入节点的权值,当前递归到的节点的位置
	BST[pos].siz++;    // 查到了这个节点,说明这个节点的子树包含要插入的节点,因此 siz++

	if (BST[pos].val == val) {    // 如果节点已经存在,则 cnt++
		BST[pos].cnt++;
		return;
	}

	if (BST[pos].val < val) {    // 如果要插入的权值大于当前节点权值,向右子树搜索
		if (BST[pos].rhs == 0) {    // 没有右子树,则直接让右子树指向新节点
			recent_pos++;    // recent_pos 指当前一共有多少个节点
			BST[recent_pos].cnt = 1;
			BST[recent_pos].siz = 1;
			BST[recent_pos].val = val;
			BST[pos].rhs = recent_pos;
			return;
		}

		insert_node(val, BST[pos].rhs);    // 递归右子树
	} else {    // 如果要插入的权值小于当前节点权值,向左子树搜索
		if (BST[pos].lhs == 0) {    // 没有左子树,则直接让左子树指向新节点
			recent_pos++;
			BST[recent_pos].cnt = 1;
			BST[recent_pos].siz = 1;
			BST[recent_pos].val = val;
			BST[pos].lhs = recent_pos;
			return;
		}

		insert_node(val, BST[pos].lhs);    // 递归左子树
	}
}

 4. 找给定权值的前驱


前驱定义为,小于当前权值且最大的数

大体思路与插入相同,具体注释可以看代码:

int find_pre(int val, int pos, int ans) {    // 要找前驱的权值,当前节点的下标,当前找到的比 val 小的最大值
	if (BST[pos].val >= val) {    // 如果当前节点权值大于 val,则向左子树寻找
		if (BST[pos].lhs == 0) {    // 若当前节点没有左子树,说明在当前节点的子树上没有比 val 更小的数了,那么当前 ans 即为答案,直接返回找到的 ans
			return ans;
		}

		return find_pre(val, BST[pos].lhs, ans);    // 若当前节点有左子树,则向左子树寻找
	}

    // 如果当前节点权值小于 val, 则向右子树寻找

	if (BST[pos].rhs == 0) {    // 若当前节点没有右子树,说明当前节点的子树上没有节点的权值 大于当前节点 且 小于要找前驱的权值,那么当前节点即为答案,直接返回当前节点的权值
		return BST[pos].val;
	}

	return find_pre(val, BST[pos].rhs, BST[pos].val);    // 若当前节点有右子树,则向右子树寻找
}

5. 找给定权值的后继


后继定义为,大于当前权值且最小的数

大体思路与找前驱相同,具体注释可以看代码:

int find_nxt(int val, int pos, int ans) {    // 要找后继的权值,当前节点的下标,当前找到的比 val 大的最小值
	if (BST[pos].val <= val) {    // 如果当前节点权值小于 val,则向右子树寻找
		if (BST[pos].rhs == 0) {    // 若当前节点没有右子树,说明在当前节点的子树上没有比 val 更大的节点了,那么先前找到的 ans 即为答案,直接返回 ans
			return ans;
		}

		return find_nxt(val, BST[pos].rhs, ans);    // 若当前节点有右子树,则向右子树寻找
	}
   
    // 如果当前节点权值大于 val,则向左子树寻找

	if (BST[pos].lhs == 0) {    // 若当前节点没有左子树,说明在当前节点的子树上没有满足 大于val 且 小于当前节点权值 的节点了,那么当前节点权值即为答案,直接返回当前节点权值    
		return BST[pos].val;    
	}

	return find_nxt(val, BST[pos].lhs, BST[pos].val);    // 若当前节点有左子树,则向左子树寻找
}

6. 找给定权值的排名


排名定义为,比当前权值小的权值的个数 +1(注意重复的也算哦)

因此,找排名就是找比当前节点权值小的权值的个数

那么,我们可以整理出大致的思路:

  1. 如果当前节点的权值 大于 要找排名的权值,那么往当前节点的左子树上找
  2. 如果当前节点的权值 等于 要找排名的权值,那么容易发现,当前节点子树上所有 小于当前节点权值的 权值个数正好为当前节点左子树的大小,那么直接返回当前节点左子树的大小
  3. 如果当前节点的权值 小于 要找排名的权值,那么容易发现,当前节点的权值 与 当前节点左子树的所有节点的权值 必然都小于要找排名的权值,且要找排名的权值必然在当前节点的右子树上,那么往当前节点的右子树上找,并将结果加上当前节点的出现次数以及当前节点左子树的大小

于是可以得到找排名的代码:

int val_find_rank(int val, int pos) {    // 要找排名的权值,当前节点的位置
	if (pos == 0) {    // 没有排名,即找到的是空节点
		return 0;
	}

	if (val == BST[pos].val) {   // 如果当前节点的权值 等于 要找排名的权值
		return BST[BST[pos].lhs].siz;    // 返回当前节点左子树的大小
	}

	if (val < BST[pos].val) {    // 如果当前节点的权值 大于 要找排名的权值
		return val_find_rank(val, BST[pos].lhs);    // 向当前节点的左子树找
	}

	// 如果当前节点的权值 小于 要找排名的权值
    // 向当前节点的右子树找,并将结果加上 当前节点的出现次数 与 当前节点左子树的大小

    return val_find_rank(val, BST[pos].rhs) + BST[BST[pos].lhs].siz + BST[pos].cnt;
}

7. 找给定排名的权值


容易发现,一个节点的左子树上节点的排名一定小于当前节点的排名

一个节点的右子树上节点的排名一定为其的排名+当前节点的出现次数+当前节点左子树的大小

那么,其实根据排名找权值的思路 与 根据权值找排名的思路 大致相反:

  1. 如果当前节点左子树的大小 大于等于 要查找排名的大小,那么往当前节点的左子树搜索
  2. 如果当前节点左子树的大小 加上 当前节点的出现次数 大于等于 要查找排名的大小,那么当前节点即为要找的点,直接返回当前节点的值(在判断过步骤 1 的基础上)
  3. 如果当前节点左子树的 大小 加上 当前节点的出现次数 小于 要查找排名的大小,那么往当前节点的右子树搜索,并将排名减去 (当前节点左子树的大小+当前节点的出现次数)

于是可以得到找权值的代码:

int rank_find_val(int rank, int pos) {
	if (pos == 0) {    // 如果找的点不存在
		return 2147483647;
	}

	if (BST[BST[pos].lhs].siz >= rank) {    // 如果当前节点左子树的大小 大于等于 要找的排名
		return rank_find_val(rank, BST[pos].lhs);    // 向当前节点的左子树搜索
	}

	if (BST[BST[pos].lhs].siz + BST[pos].cnt >= rank) {    // 如果当前节点左子树的大小 加上 当前节点的出现次数 大于等于 要找的排名
		return BST[pos].val;    // 直接返回当前节点的权值
	}

    // 否则,向右子树搜索,并将要查找的排名 减去 (当前节点左子树的大小+当前节点的出现次数)

	return rank_find_val(rank - BST[BST[pos].lhs].siz - BST[pos].cnt, BST[pos].rhs);
}

8. 练习题目


相信你已经基本掌握 BST 的基本操作了,那么做道板子题练练手吧 ~

P5076 【深基16.例7】普通二叉树(简化版)

创作不易,各位看官若觉得有用就点个赞吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值