二叉搜索树学习笔记


一. 定义

  • 二叉搜索树,是指具有如下性质(称作”BST”性质)的二叉树:
  • 给定一棵二叉树,每个结点带有一个数值,称作这个结点的“关键码”(或”关键字”、”键值”等,英文是”key”)
  • BST性质:对于树中的任意结点,满足以下两条性质
    • 它的关键码不小于左子树中任何结点的关键码
    • 它的关键码不大于右子树中任何结点的关键码

二. 支持的操作

  1. insert():新增一个关键码为 \(val\) 的结点
  2. get():查找关键码为 \(val\) 的结点
  3. getnext():查找 \(val\) 的后继
  4. getpre():查找 \(val\) 的前驱
  5. remove():删除 \(val\) 的结点
  6. getrank():查找 \(val\) 的排名
  7. getkth():查找第 \(k\) 大的 \(val\)

三. 二叉搜索树的存储与初始化

  1. 存储:
const int MAXLEN=100000;
struct NODE 
{
  int l,r;//左右孩子编号,0代表孩子不存在
  int val;//关键码
}tree[MAXLEN+2];
int tot;//当前结点总数
int root;//根结点下标
  1. 初始化
  • 为了避免越界,减少边界情况的特殊判断,一般在BST中额外插入一个关键码为正无穷和一个关键码为负无穷的节点。仅由这两个节点构成的BST就是一棵初始的空BST。
int newnode(int val)//新建一个节点,返回其编号
{
	tree[++tot].val=val;
	tree[tot].l=tree[tot].r=0;
	return tot;
}
void build()//建树
{
	newnode(-INF);
	newnode(INF);
	root=1;
	tree[1].r=2;
}

四. 二叉搜索树树的检索

  • \(BST\) 中检索是否存在关键码为 \(val\) 的节点。

  • 设变量 \(p\) 等于根节点 \(root\),执行以下过程:

    • \(p\) 的关键码等于 \(val\),则已经找到
    • \(p\) 的关键码大于 \(val\)
    • \(p\) 的左子节点为空,则说明不存在 \(val\)
    • \(p\) 的左子节点不空,在 \(p\) 的左子树中递归进行检索
  • \(p\) 的关键码小于 \(val\)

    • \(p\) 的右子节点为空,则说明不存在 \(val\)
    • \(p\) 的右子节点不空,在 \(p\) 的右子树中递归进行检索

//在以tree[p]为根的子树中查找val
//主函数调用get(root,val)
int get(int p,int val)
{
	if(p==0) return 0;
	if(tree[p].val==val) return p;
	else if(tree[p].val>val) return get(tree[p].l,val);
	else return get(tree[p].r,val);
}

五. 二叉搜索树的插入(新增)

  • 在BST中插入一个新的值 \(val\)(假设目前BST中不存在关键码为 \(val\) 的节点,若存在则不插入),与BST的检索过程类似。
  • 在发现要走向的 \(p\) 的子节点为空,说明 \(val\) 不存在时,直接建立关键码为 \(val\) 的新节点作为 \(p\) 的子节点
  • 例如插入 \(3\)\(8\)
//在以tree[p]为根的子树中插入val
//主函数调用insert(root,val)
void insert(int& p,int val)
{
	if(p==0)
	{
		p=newnode(val);//注意p是引用,其父节点的l或r会被同时更新

		return ;
	}
	if(tree[p].val==val) return ;
	if(tree[p].val>val) insert(tree[p].l,val);
	else insert(tree[p].r,val);	
}

六. 二叉查找树的找最小最大

  • 任意子树中的最小值,是其左链的顶点

  • 任意子树中的最大值,是其右链的顶点

////在以tree[p]为根的子树中找最小值结点
int getmin(int p)
{
	if(tree[p].l==0) return p;
	return getmin(tree[p].l);
}
////在以tree[p]为根的子树中找最大值结点
int getmax(int p)
{
	if(tree[p].r==0) return p;
	return getmax(tree[p].r);
}

七. 二叉查找树中的前驱与后继

后继:

  • 如果BST中存在val
    • 如果val有右子树,那么后继是val右子树的最小值结点
    • 如果val没有右子树,那么后继是val所有祖先结点中大于val的最小值结点
  • 如果BST中不存在val
    • 后继是查找val的路径上的所有结点中大于val的最小值结点

证明:

  • 首先后继不可能在 \(val\) 左子树
  • 如果后继不在 \(val\) 左右子树、也不在 \(val~root\) 的路径上,那么
    • \(s\)\(val\) 的后继,考虑 \(s\)\(val\) 的最近公共祖先 \(a\)
    • 因为 \(a\)\(s\)\(val\) 最近公共祖先,所以 \(s\)\(val\) 分别属于 \(a\) 左右子树
    • 如果 \(s\)\(val\) 右,\(s\) 不会比 \(val\) 大,不可能是后继
    • 如果 \(s\)\(val\) 左,那 \(a\)\(s\) 小,比 \(val\) 大,\(s\) 也不可能是 \(val\) 后继
  • 最后证明当 \(val\) 有右子树时,后继结点一定是右子树的最小值结点
    • 因为祖先结点中所有大于 \(val\) 的结点也都大于它
int getnext(int val)
{
	int p=root;
	int ans=2;//tree[2]是INF
	while(p!=0)
	{
		if(tree[p].val==val)//BST中存在val
		{
			if(tree[p].r!=0)//val有右子树
			{
				p=tree[p].r;
				while(tree[p].l!=0) p=tree[p].l;
				return p;
			}
			break;//val 没有右子树,此时答案已经在ans中
		}
                //路径经过的结点都检查一遍
		if(tree[p].val>val and tree[p].val<tree[ans].val) ans=p;
		if(tree[p].val>val) p=tree[p].l;
		else p=tree[p].r;
	}
	return ans;//BST中没有val,此时答案以在 ans 中
	
}

同理可得前驱。

  • 如果 \(BST\) 中存在 \(val\)
    • 如果 \(val\) 有左子树,那么前驱是 \(val\) 左子树的最大值结点
    • 如果 \(val\) 没有左子树,那么前驱是 \(val\) 所有祖先结点中小于 \(val\) 的最大值结点
  • 如果 \(BST\) 中不存在 \(val\)
    • 后继是查找 \(val\) 的路径上的所有结点中小于 \(val\) 的最大值结点
int getpre(int val)
{
	int p=root;
	int ans=1;
	while(p!=0)
	{
		if(tree[p].val==val)
		{
			if(tree[p].l!=0)
			{
				p=tree[p].l;
				while(tree[p].r!=0) p=tree[p].r;
				return p;
			}
			break;
		}
		if(tree[p].val<val and tree[p].val>tree[ans].val) ans=p;
		if(tree[p].val>val) p=tree[p].l;
		else p=tree[p].r;
	}
	return ans;
}

七. 二叉查找树的删除

  • 从BST中删除关键码为 \(val\) 的节点
  • 首先,在 \(BST\) 中检索 \(val\) ,得到节点 \(p\)
    • \(p\) 的子节点个数小于 \(2\),则直接删除 \(p\),并令 \(p\) 的子节点代替 \(p\) 的位置,与 \(p\) 的父节点相连。
    • \(p\) 既有左子树又有右子树,则在 \(BST\) 中求出 \(val\) 的后继节点 \(next\) 。因为 \(next\) 没有左子树,所以可以直接删除 \(next\),并令 \(next\) 的右子树代替 \(next\) 的位置。最后,再让\(next\) 节点代替 \(p\) 节点,删除 \(p\) 即可。

void remove(int &p, int val) { 
    if (p == 0) return;
    if (val == tree[p].val) { // 已经检索到值为val的节点
        if (tree[p].l == 0) { // 没有左子树
            p = tree[p].r; // 右子树代替p的位置,注意p是引用
        }
        else if (tree[p].r == 0) { // 没有右子树
            p = tree[p].l; // 左子树代替p的位置,注意p是引用
        }
        else { // 既有左子树又有右子树
            // 求后继节点
            int next = tree[p].r;
            while (tree[next].l > 0) next = tree[next].l;
            // next一定没有左子树,直接删除
            remove(tree[p].r, tree[next].val);
            // 令节点next代替节点p的位置
            tree[next].l = tree[p].l;
            tree[next].r = tree[p].r;
            p = next; // 注意p是引用
        }
        return;
    }
    if (val < tree[p].val) {
        remove(tree[p].l, val);
    } else {
        remove(tree[p].r, val);
    }
}

八. 二叉查找树的效率

  • 二叉查找树的操作都和树的高度 \(h\) 有关,是 \(O(h)\)
  • 新增元素可能会增加 \(BST\) 的高度
  • 如果新增顺序比较随机,\(BST\) 期望高度是 \(O(logn)\)
  • 如果按从小到大(或从大到小)顺序新增元素,\(BST\) 高度可能退化到 \(O(n)\)
  • 某些数据结构可以在新增元素时能保证高度维持在 \(O(logn)\) 级别,这样的数据结构叫做平衡二叉树。

以上就是二叉查找树的基本操作,但我们仍有很多问题没解决,因此我们考虑扩展 \(BST\)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM