下图无比现实
二叉查找树又名二叉排序树、二叉搜索树、BST(Binary Search Tree)。首先知道它的基础数据结构。
它是一颗二叉树(带权),通常会用结构体实现(数组或者指针——个人比较喜欢用数组,之后的代码也是用数组来实现的)。
struct node{ int l, r, val;//左孩子,右孩子,权值 int sz, cnt;//子树大小,出现几次 (update:用来求kth,val的排名) }tree[SIZE]; int tot; //记录共有几个节点 int root; //记录根节点
不过它有一个性质:对于任意一个非叶子节点,它的左子树所有的节点的权值都<他的权值,它的右子树所有的节点的权值>它的权值。
其次它可以实现以下功能:
• 建立关键码为x的节点
• 检索
• 插入关键码为x的节点
• 求关键码为x的节点的前驱
• 求关键码为x的节点的后继
• 删除关键码为x的节点
update:求排名和排名的关键码
• 求关键码为x的节点的排名
• 求排名为x的节点的关键码
1,建立关键码
这个没什么好讲的吧。
//插入一个权值为v的新节点 int New(int v) { BST[++tot].val = v; BST[tot].cnt = BST[tot].sz = 1; }
2,BST的检索
在BST中检索是否存在权值为v的节点。
设变量p等于根节点root,执行以下过程:
1.若p的权值等于v,则已经找到
2.若p的权值大于v
a.若p的左子节点为空,则说明不存在v
b.若p的左子节点不空,在p的左子树中递归进行检索
3.若p的权值小于v
a.若p的右子节点为空,则说明不存在v
b.若p的右子节点不空,在p的右子树中递归进行检索
//检索关键码为v的编号 int Find(int p, int v) { if (p == 0) return -1;//没有此数 if (v == BST[p].val) return v;//找到了 if (v > BST[p].val) return Find(BST[p].ron, v);//比他大,在他的右子树里找 else return Find(BST[p].lson, v);//比他小,在他的左子树里找 }
3,BST的插入
插入其实跟检索差不多,只不过找的合适的位置后要把元素插入进去。
用一个引用记录其父节点的lson或rson值,这样就可以轻松插入了。
//更新节点信息 void Up(int p) { BST[p].sz = BST[BST[P].lson].sz + BST[BST[p].rson].sz + 1; //p的子树大小等于p的左儿子的子树大小加p的右儿子的子树大小再加1 } //插入一个关键码为v的数 void Insert(int &p, int v) { if (p == 0) { //之前没有v这个数 //需要新建一个节点 p = New(v);//p是引用,其父节点的lson或rson值会随着改变 return; } if (v == BST[p].val) BST[P].cnt++;//之前已经有v这个数了,直接次数+1 if (v > BST[p].val) Insert(BST[p].ron, v);//比他大,在他的右子树里插 else Insert(BST[p].lson, v);//比他小,在他的左子树里插 Up(p);//需要更新p的信息(这里是子树大小) }
Up和线段树类似,因为改变了p的儿子的信息所以p的信息肯定也要更改。
4,BST的后继
首先检索v,然后开始分类讨论:
设v的节点编号为p。
I p有右子树,则它的后继一定在它的右子树中否则它的右子树将不是“它的右子树”。
而且一定是它右孩子后一直往左孩子找。
II 若p没有右子树,则它的后继就是它的直系祖先中 > 它且最小的一个(在检索的时候记录)
//找关键码为v的后继 int Next(int v) { int ret = -1; //检索 int p = root; while (p) { if (v == BST[p].val) { //找到了 if (BST[p].rson) { //有右子树 p = BST[p].rson; while (BST[p].lson) p = BST[p].lson;//一直往左走 ret = p; } break; } if (BST[p].val > v && (ret == -1 || BST[p].val < BST[ret].val)) ret = p, p = BST[p].lson;//检索时记录 else p = BST[p].rson; } return ret; //返回后缀的节点编号,若没有返回-1(如在2,3,4中找5的后缀) }
5,BST找前驱
更后继差不多,不过是左子树一直往右走,纪录时也是<它最大的一个。
//找关键码为v的前驱 int Pre(int v) { int ret = -1; //检索 int p = root; while (p) { if (v == BST[p].val) { //找到了 if (BST[p].lson) { //有左子树 p = BST[p].lson; while (BST[p].rson) p = BST[p].rson;//一直往右走 ret = p; } break; } if (BST[p].val < v && (ret == -1 || BST[p].val > BST[ret].val)) ret = p, p = BST[p].rson;//检索时记录 else p = BST[p].lson; } return ret; //返回前驱的节点编号,若没有返回-1(如在2,3,4中找1的前驱) }
6,BST中删除节点
在往下看之前,读者可以先结合上述知识思考一下如何删除(提示:和前驱后继有关)。
首先也是先检索它得到节点p
若v出现的大于1,那么直接cnt--即可。
若p的子节点个数小于2,则直接删除p,并令p的子节点代替p的位置,与p 的父节点相连。
若p既有左子树又有右子树,则在BST中求出v的后继节点next。
因为next没有左子树(想想为什么?),所以可以直接删除next,并令next的右子树代替next的位置。
最后, 再让next节点代替p节点,删除p即可。
void Del(int &p, int v) { if (p == 0) return;//没有v这个数 if (v == BST[p].val) { //找到了 if (BST[p].cnt > 1) BST[p].cnt--;//出现次数大于1,直接减1 else { //出现次数等于1,现在要删掉次数,此节点没有存在的必要了 if (!BST[p].lson) { p = BST[p].rson;//没有左儿子,此处依然是引用,p变成它的右儿子即可 } else if (!BST[p].rson) { p = BST[p].lson;//没有右儿子,p变成它的左儿子即可 } else { //p有两个儿子 int nxt = Next(v);//求出v的后继 Del(root, BST[nxt].val);//删除nxt //用nxt代替p BST[nxt].lson = BST[p].lson, BST[nxt].rson = BST[p].rson; p = nxt; } } Up(p); return; } if (BST[p].val < v) Del(BST[p].rson, v); else Del(BST[p].lson, v); Up(p); }
update:
7,BST查询排名
我们用循环的方式来求
对于当前节点p开始分类讨论
(1)valp=v,ret+=p左子树大小并退出循环
(2)valp<v,则valp及其左子树均排在v前面,ret+=cntp+p左子树的大小,跳p
(3)valp>v,此时ret并不能增加,直接跳p
//查询v的排名 int Rank(int v) { int ret = 0; int p = root; while (p) { if (v == BST[p].val) { ret += BST[BST[p].lson].sz; break; } if (v > BST[p].val) { ret += BST[p].cnt + BST[BST[p].lson].sz; p = BST[p].rson; } else { p = BST[p].lson; } } return ret + 1; }
8,BST根据排名查询数
有点类似主席树,时间不早了,这就不讲了
//查询排名为k的数 int Kth(int k) { int ret = -1; int p = root; while (p) { if (k <= BST[BST[p].lson].sz) p = BST[p].lson; else { k -= BST[BST[p].lson].sz; if (k <= BST[p].cnt) { ret = p; break; } k -= BST[p].cnt; p = BST[p].rson; } } return ret; }
update1:3-8
讲真BST没什么n用,但它是学习平衡树的基础。