「演算法筆記」二叉查詢樹
一、二叉查詢樹
二叉查詢樹(Binary Search Tree,下文簡稱 BST)是一種二叉樹的樹形資料結構。
樹上的每個節點帶有一個數值,稱為節點的“關鍵碼”(也可以叫其他的 QAQ)。對於樹中的任意一個節點:
- 該節點的關鍵碼不小於它的左子樹中任意節點的關鍵碼。
- 該節點的關鍵碼不大於它的右子樹中任意節點的關鍵碼。
即左子樹任意節點的值 \(<\) 根節點的值 \(<\) 右子樹任意節點的值。
滿足上述性質的二叉樹就是一棵 BST。顯然,BST 的中序遍歷是一個關鍵碼單調遞增的節點序列。
二、基本操作
約定:\(lc(u)\) 和 \(rc(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\)
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