1. 程式人生 > 實用技巧 >「演算法筆記」二叉查詢樹

「演算法筆記」二叉查詢樹

一、二叉查詢樹

二叉查詢樹(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\) 為根結點,執行以下過程:

  1. \(val(p)=k\),則已找到。

  2. \(val(p)>k\):若 \(lc(p)\) 為空,則不存在 \(k\);否則,在 \(p\) 的左子樹中遞迴進行檢索。

  3. \(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\)

  1. 沒有找到 \(k\)。此時 \(k\) 的後繼就在已經經過的節點中,\(ans\) 即為所求。

  2. 找到了節點 \(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