算法導(dǎo)論 - 二叉搜索樹(BST)

二叉搜索樹

顧名思義,一棵二叉搜索樹是以一棵二叉樹來組織的。如下圖所示,這樣一棵樹可以使用一個鏈表數(shù)據(jù)結(jié)構(gòu)來表示,其中每個結(jié)點就是一個對象,除了key外,每個結(jié)點還包括屬性left 、right、和p,它們分別指向結(jié)點的左孩子、右孩子和雙親。如果某個孩子結(jié)點和父結(jié)點不存在,則相應(yīng)屬性的值為nil,根結(jié)點是樹中唯一父指針為nil的結(jié)點

1.png

二叉搜索樹中的關(guān)鍵字總是以滿足二叉搜索樹性質(zhì)的方式來存儲:

設(shè)x是二叉搜索樹中的一個結(jié)點,如果y是x的左子樹中的一個結(jié)點,那么y.key \leq x.key。如果y是x的右子樹中的一個結(jié)點,那么y.key \geq x.key

如圖(a)中,樹根的關(guān)鍵字是6,在其左子樹中有關(guān)鍵字2、5和5,它們均不大于6;而其右子樹中有關(guān)鍵字7和8,它們均不小于6。所以這個性質(zhì)在樹中的每個結(jié)點都成立。

二叉搜索樹的性質(zhì)允許我們通過一個簡單的遞歸算法來按序輸出二叉搜索樹中的所有關(guān)鍵字,算法是我們熟悉的中序遍歷

typedef struct Node
{
    struct Node *p;
    struct Node *left;
    struct Node *right;
    int key;
} Node;

void InorderTreeWalk(Node *x) // 中序遍歷樹x
{
    if (x != nullptr) {
        InorderTreeWalk(x->left); // 遞歸左孩子
        cout<<x->key<<endl; // 輸出根節(jié)點關(guān)鍵字
        InorderTreeWalk(x->right); // 遞歸右孩子
    }
}

遍歷一棵有n個結(jié)點的二叉搜索樹,需要耗費O(n)的時間,因為初次調(diào)用后,對于樹中的每個結(jié)點這個過程恰好要自己調(diào)用兩次;一次是它的左孩子,另一次是它的右孩子

查詢二叉搜索樹

2.jpeg

我們經(jīng)常需要查找一個存儲在二叉搜索樹中的關(guān)鍵字,下面將討論并且說明在任何高度為h的二叉搜索樹上,如何在O(h)時間內(nèi)完成每個操作

查找

使用下面的代碼在一棵二叉搜索樹中查找一個具有給定關(guān)鍵字的結(jié)點,輸入一個指向樹根的指針和一個關(guān)鍵字k,如果這個節(jié)點存在,那么返回關(guān)鍵字為k的指針,否則為nil

Node * TreeSearch(Node *x, int k)
{
    if (x == nullptr || k == x->key) {
        return x;
    }

    if (k < x->key)
    {
        return TreeSearch(x->left, k);
    }
    else
    {
        return TreeSearch(x->right, k);
    }
}
  • 這個過程是從樹根開始查找的,并沿著這顆樹中的一條簡單路徑向下進行。對于遇到的每個結(jié)點x,比較關(guān)鍵字k與x.key,如果兩個關(guān)鍵字相等,查找就終止了,該結(jié)點就是我們需要查找的結(jié)點。

  • 如果k小于x.key,則查找x的左子樹,因為二叉搜索樹的性質(zhì)告訴了我們k不可能在右子樹中。相應(yīng)的,如果k大于x.key,查找在右子樹進行。

  • 從樹根開始遞歸期間遇到的結(jié)點就形成了一條向下的簡單路徑,所以TreeSearch的運行時間是O(h),其中h是樹的高度

除了遞歸方式,采用迭代方式是效率更高的方式

Node * InteractiveTreeSearch(Node *x, int k)
{
    while (x != nullptr && k != x->key) {
        if (k < x->key)
        {
            x = x->left;
        }
        else
        {
            x = x->right;
        }
    }
    return x;
}

最大關(guān)鍵字元素和最小關(guān)鍵字元素

通過從樹根開始沿著left孩子指針直到遇到一個nil,

  • 最小關(guān)鍵字元素
Node *TreeMinimum(Node *x)
{
    while (x->left != nullptr) {
        x = x->left;
    }
    return x;
}

二叉樹的性質(zhì)保證了上面查找的正確性。

如果結(jié)點x沒有左子樹,那么由于x的右子樹中的每個關(guān)鍵字都至少大于或等于x.key,則以x為根的子樹中的最小關(guān)鍵字是x.key。如果結(jié)點x有左子樹,那么由于其右子樹中沒有關(guān)鍵字小于x.key,且在左子樹中的每個關(guān)鍵字不大于x.key,則以x為根的子樹中的最小關(guān)鍵字一定在以x.left為根的子樹中

  • 最大關(guān)鍵字元素

最大元素一定是沿著right孩子指針不斷向下查找,直到遇到第一個空指針;

Node *TreeMaxmum(Node *x)
{
    while (x->right != nullptr) {
        x = x->right;
    }
    return x;
}

這兩個過程在一棵高度為h的樹上均能在O(h)的時間內(nèi)執(zhí)行完。

后繼和前驅(qū)

給定一棵二叉搜索樹,有時候需要按中序遍歷的次序查找它的后繼。如果所有的關(guān)鍵字互不相同,則一個結(jié)點x的后繼是大于x.key的最小關(guān)鍵字的結(jié)點

后繼

Node *TreeSuccessor(Node *T, int t)
{
    // 查找結(jié)點
    Node *x = TreeSearch(T, t);
    Node *y = nullptr;

    // 右子樹非空,即查找最小關(guān)鍵字
    if (x->right != nullptr) {
        y = TreeMinimum(x->right);
        return y;
    }

    // 查找結(jié)點的父指針
    y = x->p;
    while (y != nullptr && x == y->right) {
        x = y;
        y = y->p;
    }
    return y;
}

如果結(jié)點x的右子樹非空,那么x的后繼恰好是x右子樹中的最左結(jié)點。也可以理解為最小關(guān)鍵字

2.jpeg

如果結(jié)點x的右子樹為空并有一個后繼y,那么y就是x的最底層祖先,并且y的左孩子也是x的一個祖先(比如:上圖中關(guān)鍵字為13的結(jié)點的后繼是關(guān)鍵字為15的結(jié)點)。為了找到y(tǒng),只需要簡單地從x開始沿樹而上直到遇到這樣一個結(jié)點:這個結(jié)點是它的雙親的左孩子

前驅(qū)

Node *TreePredecessor(Node *T, int t)
{
    // 查找元素t
    Node *x = TreeSearch(T, t);
    Node *y = nullptr;

    // 左子樹非空
    if (x->left != nullptr)
    {
        return  TreeMaxmum(x->left);
    }

    // 查結(jié)點的父指針
    y = x->p;
    while ( y != nullptr && x == y->left) {
        x = y;
        y = y->p;
    }
    return y;
}

對于樹的后繼和前驅(qū)的查找,運行時間為O(h)

插入和刪除

插入和刪除操作會引起由二叉搜索樹表示的動態(tài)集合的變化。所以一定要修改數(shù)據(jù)結(jié)構(gòu)來反映這個變化,但修改要保持二叉搜索樹性質(zhì)的成立

插入

向一個二叉搜索樹中插入一個x結(jié)點,只需要不斷比較x.key與當(dāng)前結(jié)點z.key的大小,若小于,則肯定是向z的左子樹插入,否則向z的右子樹插入,循環(huán)比較,直到遇到當(dāng)前結(jié)點的左/右子樹為空為止。這時候已經(jīng)找到了要插入的結(jié)點的父結(jié)點的位置,最后判斷是左子樹還是右子樹即可

Node * TreeInsert(Node *root, Node *z)
{
    Node *y = nullptr;
    Node *x = root;

    // 節(jié)點非空,看是左子樹還是右子樹插入
    while (x != nullptr) {
        y = x;
        if (z->key < x->key)
        {
            x = x->left;
        }
        else
        {
            x = x->right;
        }
    }

    z->p = y;
    if (y == nullptr) // 建立第一個節(jié)點
    {
        root = z;
    }
    else if (z->key < y->key) // 小于父結(jié)點即位左子樹
    {
        y->left = z;
    }
    else // 大于即為右子樹
    {
        y->right = z;
    }

    return root;
}
3.jpeg

比如:上圖插入結(jié)點,關(guān)鍵字為13,可以對照代碼和圖形理解

刪除

從一棵二叉搜索樹T中刪除一個結(jié)點z,存在3種情況:

1、如果z沒有孩子結(jié)點,那么只是簡單地將它刪除,并修改父結(jié)點,用nil作為孩子來替換z
2、如果z只有一個孩子,那么將孩子提升到樹中z的位置,并修改z的父結(jié)點,用z的孩子來替換z
3、如果z有兩個孩子,那么找z的后繼y(一定在z的右子樹中),并讓y占據(jù)樹中z的位置。z的原來右子樹部分成為y的新的右子樹,并且z的左子樹成為y的新的左子樹

對照圖來理解上面幾種情況

  • 如果z沒有左孩子(如下圖),那么用其右孩子來替換z,這個右孩子可以是nil,也可以不是nil。當(dāng)右孩子是nil的時候,此時就是z沒有孩子結(jié)點的情形。當(dāng)右孩子非nil時,這就是z僅有一個孩子的情況,該孩子是其右孩子
屏幕快照 2018-12-12 上午9.16.42.png
  • 如果z僅有一個孩子且為左孩子(如下圖),那么用其左孩子替換z
屏幕快照 2018-12-12 上午9.16.50.png
  • z既有左孩子也有右孩子。如果y是z的右孩子(如下圖),那么用y替換z,并僅留下y的右孩子
屏幕快照 2018-12-12 上午9.17.03.png
  • 如果y位于z的右子樹中但并不是z的右孩子(如下圖),這種情況下,先用y的右孩子替換y,然后再用y替換z
屏幕快照 2018-12-12 上午9.17.19.png

在二叉搜索樹內(nèi)移動子樹,它是用另一棵子樹替換一棵子樹并成為其雙親的孩子結(jié)點

移動子樹

void Transplant(Node **T, Node *u, Node *v) // 用結(jié)點v替換結(jié)點u
{
    if (u->p ==  nullptr) // 父結(jié)點不存在,那么u為根結(jié)點
    {
        *T = v;
    }
    else if (u == u->p->left) // u為左孩子,那么將v作為左孩子
    {
        u->p->left = v;
    }
    else // u為右孩子,那么將v作為右孩子
    {
        u->p->right = v;
    }

    if (v != nullptr) // v不為nil,那么更新父結(jié)點
    {
        v->p = u->p;
    }
}

刪除結(jié)點

Node * TreeDelete(Node *T, int k)
{
    Node *z = TreeSearch(T, k); // 查找結(jié)點z
    Node *y = nullptr;

    if (z->left == nullptr) // 結(jié)點z沒有左孩子
    {
        Transplant(&T, z, z->right);
    }
    else if (z->right == nullptr) // 結(jié)點z沒有右孩子
    {
        Transplant(&T, z, z->left);
    }
    else // 結(jié)點z有兩個孩子
    {
        y = TreeMinimum(z->right); // 找右子樹的最小結(jié)點

        if (y->p != z) { // y的父結(jié)點不是z
            Transplant(&T, y, y->right);
            y->right = z->right;
            y->right->p = y;
        }

        Transplant(&T, z, y);
        y->left = z->left;
        y->left->p = y;
    }
    return T;
}

簡單使用

// 創(chuàng)建搜索二叉樹
Node * TreeEstablish(int *a, int length)
{
    Node *root = nullptr;
    for (int i=0 ; i < length; i++) {
        Node *node = (Node *)malloc(sizeof(Node));
        node->key = a[i];
        node->p = nullptr;
        node->left = nullptr;
        node->right = nullptr;
        root = TreeInsert(root, node);
    }
    return root;
}

int main(int argc, const char * argv[]) {
    int a[] = {2,3,4,6,15,7,18,17,20,13,9};
    int length = sizeof(a)/sizeof(a[0]);

    Node *T;
    T = TreeEstablish(a, length); // 建立二叉搜索樹
    InorderTreeWalk(T);  // 中序遍歷打印
    cout<<"6 successor is "<<TreeSuccessor(T, 6)->key<<endl; // 后繼查找
    cout<<"18 predecessor is "<<TreePredecessor(T, 18)->key<<endl; // 前驅(qū)查找
    T = TreeDelete(T, 2); // 刪除結(jié)點
    TreeInorderPrint(T); // 刪除結(jié)點之后的二叉搜索樹
    return 0;
}

參考

《算法導(dǎo)論》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容