一、二叉查找树
二叉查找树(Binary Search Tree,下文简称 BST)是一种二叉树的树形数据结构。
树上的每个节点带有一个数值,称为节点的“关键码”(也可以叫其他的 QAQ)。对于树中的任意一个节点:
- 该节点的关键码不小于它的左子树中任意节点的关键码。
- 该节点的关键码不大于它的右子树中任意节点的关键码。
即左子树任意节点的值 (<) 根节点的值 (<) 右子树任意节点的值。
满足上述性质的二叉树就是一棵 BST。显然,BST 的中序遍历是一个关键码单调递增的节点序列。
二、基本操作
约定:(lc(u)) 和 (rc(u)) 分别表示节点 (u) 的左子节点和右子节点,(val(u)) 表示节点 (u) 的关键码。
1. BST 的建立
为了避免越界,减少边界情况的判断,我们在 BST 中额外插入关键码为 (+infty) 和 (-infty) 的节点。仅由这两个节点构成的 BST 就是一棵初始的空 BST。
简便起见,在接下来的操作中,假设 BST 不含有关键码相同的节点。
int tot,rt,lc[N],rc[N],val[N]; //rt 为根结点在数组中的下标,lc(u) 和 rc(u) 分别表示 u 的左右子节点在数组中的下标,val(u) 表示节点 u 的关键码 void build(){ val[++tot]=-1e18,val[++tot]=1e18; //新建关键码为负无穷和正无穷的节点(它们在数组中的下标分别为 1、2) rt=1,rc[1]=2; //1 为根结点(对应关键码为负无穷的节点),它的右儿子为关键码为正无穷的节点 }
2. BST 的检索
在 BST 中检索是否存在关键码为 (k) 的节点。
设 (p) 为根结点,执行以下过程:
-
若 (val(p)=k),则已找到。
-
若 (val(p)>k):若 (lc(p)) 为空,则不存在 (k);否则,在 (p) 的左子树中递归进行检索。
-
若 (val(p)<k):若 (rc(p)) 为空,则不存在 (k);否则,在 (p) 的右子树中递归进行检索。
int find(int p,int k){ if(!p) return 0; //检索失败 if(val[p]==k) return p; //检索成功 return k<val[p]?find(lc[p],k):find(rc[p],k); }
3. BST 的插入
在 BST 中插入一个关键码为 (k) 的节点。(假设目前 BST 中不存在关键码为 (k) 的节点)
与 BST 的检索类似。要走向的 (p) 的子节点为空,说明 (k) 不存在时,直接建立关键码为 (k) 的新节点作为 (p) 的子节点。
void insert(int &p,int k){ if(!p){val[++tot]=k,p=tot;return ;} //注意 p 是引用,其父节点的 lc 或 rc 值会被同时更新 if(val[p]==k) return ; if(k<val[p]) insert(lc[p],k); else insert(rc[p],k); }
4. BST 求前驱/后继
以“后继”为例。(k) 的后继指在 BST 中关键码大于 (k) 的节点中,关键码最小的节点。
初始化 (ans) 为关键码为 (+infty) 的节点。然后,在 BST 中检索 (k)。每经过一个节点,都尝试更新 (ans)。
-
没有找到 (k)。此时 (k) 的后继就在已经经过的节点中,(ans) 即为所求。
-
找到了节点 (p) 使得 (val(p)=k)。若 (rc(p)) 为空,则 (ans) 即为所求;否则,从 (rc(p)) 出发,一直向左走,就找到了 (k) 的后继。
int getnxt(int k){ int ans=2,p=rt; //val(2)=+∞ while(p){ if(val[p]==k){ if(rc[p]>0){p=rc[p]; while(lc[p]>0) p=lc[p]; ans=p;} break; } if(val[p]>k&&val[p]<val[ans]) ans=p; //尝试更新 ans p=k<val[p]?lc[p]:rc[p]; } return ans; }
5. BST 的节点删除
在 BST 中删除关键码为 (k) 的节点。
首先,在 BST 中搜索 (k),得到节点 (p)。
若 (p) 没有左子树或没有右子树,则直接删除 (p),并令 (p) 的子节点代替 (p) 的位置,与 (p) 的父节点相连。
若 (p) 左右子树都有,则在 BST 中求出 (k) 的后继节点 (next)。因为 (next) 没有左子树(因为 (next) 是从 (p) 的右子节点出发,一直向左走得到的),所以可以直接删除 (next),并令 (next) 的右子树代替 (next) 的位置。最后,再让 (next) 节点代替 (p) 节点,删除 (p) 即可。举个栗子:
应该还是比较好理解哒,具体见代码。
void del(int &p,int k){ //从子树 p 中删除值为 k 的阶段 if(!p) return ; if(val[p]==k){ //已经检索到值为 k 的阶段 if(!lc[p]) p=rc[p]; //没有左子树,右子树代替 p 的位置,注意 p 是引用 else if(!rc[p]) p=lc[p]; //没有右子树,左子树代替 p 的位置,注意 p 是引用 else{ //既有左子树又有右子树 int nxt=rc[p]; while(lc[nxt]>0) nxt=lc[nxt]; //求后继节点(从 p 的右子节点出发,一直向左走) del(rc[p],val[nxt]); //next 一定没有左子树,直接删除 lc[nxt]=lc[p],rc[nxt]=rc[p],p=nxt; //令节点 next 代替节点 p 的位置。注意 p 是引用 } return ; } if(k<val[p]) del(lc[p],k); else del(rc[p],k); }
三、参考资料
- 《算法竞赛进阶指南》(大棒子,做摘抄 233)
注:这篇文章可能会有锅 QAQ