一. 定義
- 二叉搜索樹,是指具有如下性質(稱作”BST”性質)的二叉樹:
- 給定一棵二叉樹,每個結點帶有一個數值,稱作這個結點的“關鍵碼”(或”關鍵字”、”鍵值”等,英文是”key”)
- BST性質:對於樹中的任意結點,滿足以下兩條性質
- 它的關鍵碼不小於左子樹中任何結點的關鍵碼
- 它的關鍵碼不大於右子樹中任何結點的關鍵碼
二. 支持的操作
- insert():新增一個關鍵碼為 \(val\) 的結點
- get():查找關鍵碼為 \(val\) 的結點
- getnext():查找 \(val\) 的后繼
- getpre():查找 \(val\) 的前驅
- remove():刪除 \(val\) 的結點
- getrank():查找 \(val\) 的排名
- getkth():查找第 \(k\) 大的 \(val\)
三. 二叉搜索樹的存儲與初始化
- 存儲:
const int MAXLEN=100000;
struct NODE
{
int l,r;//左右孩子編號,0代表孩子不存在
int val;//關鍵碼
}tree[MAXLEN+2];
int tot;//當前結點總數
int root;//根結點下標
- 初始化
- 為了避免越界,減少邊界情況的特殊判斷,一般在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\) 。