1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        面試官問我:什么是 “伸展樹” ?

        共 44914字,需瀏覽 90分鐘

         ·

        2021-06-02 08:48

        學(xué)過數(shù)據(jù)結(jié)構(gòu)的小伙伴,一定都知道二叉查找樹,也叫二叉排序樹,英文縮寫是BST。

        為了維持二叉查找樹的高效率查找,就需要對二叉查找樹進(jìn)行平衡調(diào)整。在數(shù)據(jù)結(jié)構(gòu)當(dāng)中大名鼎鼎的紅黑樹、AVL,就是典型的自平衡二叉查找樹。

        今天,我們來介紹一種更有意思的自平衡二叉樹:伸展樹。它的英文名字是Splay Tree。

        Part 1 為什么要伸展

        我們來回顧一下,二叉搜索樹滿足:左子結(jié)點(diǎn) < 當(dāng)前結(jié)點(diǎn) < 右子結(jié)點(diǎn)

        為什么要有平衡樹呢?因?yàn)楫?dāng)二叉搜索樹如下圖“瘸腿”時(shí),搜索左側(cè)的結(jié)點(diǎn),原來的速度 會掉到 ,與鏈表一個(gè)速度,失去了價(jià)值。


        為了避免樹瘸腿,我們可以通過適當(dāng)?shù)男D(zhuǎn)來調(diào)整樹的結(jié)構(gòu)。

        伸展樹的核心,是通過不斷的將結(jié)點(diǎn)旋轉(zhuǎn)到根結(jié)點(diǎn)(即為splay操作),來保證整棵樹不會跟鏈表一樣。它由 Daniel Sleator 和 Robert Tarjan 發(fā)明 。

        1.1 結(jié)點(diǎn)

        node中記錄的信息:

        • parent:父結(jié)點(diǎn)的指針。
        • child[0/1]: child[0]為左子結(jié)點(diǎn)的指針,child[1]為右子結(jié)點(diǎn)的指針。
        • value:這個(gè)結(jié)點(diǎn)存了啥。
        • count:二叉樹中不存在兩個(gè)值相同的結(jié)點(diǎn),如果需要記錄多個(gè)就需要一個(gè)變量來記錄這個(gè)數(shù)值出現(xiàn)了多少次。(count就是來干這個(gè)活的)
        • size:這個(gè)結(jié)點(diǎn)為根結(jié)點(diǎn)的子樹中有多少個(gè)結(jié)點(diǎn)。

        基礎(chǔ)操作:

        • maintain:更新結(jié)點(diǎn)的size。(更新要自底向上)
        • get:獲取自己的類型,0為左子結(jié)點(diǎn),1為右子結(jié)點(diǎn)。
        • connect: 連接兩個(gè)結(jié)點(diǎn)。
        class node {
          public:
              node *parent; // 父結(jié)點(diǎn)的指針
              node *child[2]; // child[0]為左子結(jié)點(diǎn)的指針,child[1]為右子結(jié)點(diǎn)的指針。
              int value, count, size; // 數(shù)據(jù),出現(xiàn)次數(shù),子樹大小

              node(int _value) {
                  value = _value;
                  parent = child[0] = child[1] = NULL;
                  count = size = 1;
              }
          };

        我們把對指針是否為NULL提到了兩個(gè)基礎(chǔ)操作中,所以只能放在伸展樹的類中。

        destroy:銷毀整個(gè)樹。

        因?yàn)榻Y(jié)點(diǎn)使用的是堆空間(new出來的),所以必須要銷毀(delete),否則會內(nèi)存泄漏。

        class splayTree {
        public:

            node *root;
            
            splayTree() {
                root = NULL;
            }
            
            ~splayTree() {
                destroy(root); // 從root開始銷毀
            }

            void destroy(node *current) // 銷毀結(jié)點(diǎn)
                if (current) {
                    destroy(current->child[0]); // 后序遍歷
                    destroy(current->child[1]);
                    delete current;
                }
            }
            
            void maintain(node *current) // 更新size
                if (current == NULL) { // 可能傳入的是一個(gè)空指針
                    return;
                }
                current->size = current->count; // 先將自己加上
                if (current->child[0]) { // 防止沒兒子,NULL,報(bào)錯
                    current->size += current->child[0]->size; // 加上兒子
                }
                if (current->child[1]) {
                    current->size += current->child[1]->size;
                }
            }

            int get(node *current) // 獲得結(jié)點(diǎn)是左結(jié)點(diǎn)還是右結(jié)點(diǎn),0左1右
                if (current == NULL || current->parent == NULL) { // 傳入空指針;根結(jié)點(diǎn)沒有父親,特判一下
                    return -1;
                }
                return current->parent->child[1] == current; // 父親的右子結(jié)點(diǎn) 是不是 自己
            }
            
            void connect(node *parent, node *current, int type) // 父結(jié)點(diǎn)指針,當(dāng)前結(jié)點(diǎn)指針,類型(0左1右)
                if (parent) { // parent 可能為NULL
                    parent->child[type] = current;
                }
                if (current) { // current 也可能為NULL
                    current->parent = parent;
                }
            }
        };

        可能會有讀者好奇:這個(gè)size是用來干什么的呢?別急,等到查詢排名時(shí)就會用到。

        1.2 左旋 & 右旋

        通過旋轉(zhuǎn),我們能在保證旋轉(zhuǎn)可以保證左子結(jié)點(diǎn) < 當(dāng)前結(jié)點(diǎn) < 右子結(jié)點(diǎn)的情況下調(diào)整結(jié)點(diǎn)之間的關(guān)系。

        旋轉(zhuǎn)有兩種定義

        1. 以x為根的子樹進(jìn)行旋轉(zhuǎn)。
        2. 把x向上旋轉(zhuǎn)。

        1.2.1 以x為根的子樹進(jìn)行旋轉(zhuǎn)

        1.2.2 把x向上旋轉(zhuǎn)

        上面的動畫使用文字?jǐn)⑹黾礊椋?/p>

        • 左旋
          1. 將被旋轉(zhuǎn)結(jié)點(diǎn)的左結(jié)點(diǎn)變?yōu)楦附Y(jié)點(diǎn)的右結(jié)點(diǎn)。
          2. 父結(jié)點(diǎn)變?yōu)楸恍D(zhuǎn)結(jié)點(diǎn)的左子結(jié)點(diǎn)
          3. 原父結(jié)點(diǎn)的父結(jié)點(diǎn)(爺爺)變?yōu)楸恍D(zhuǎn)結(jié)點(diǎn)的父結(jié)點(diǎn)。
        • 右旋
          1. 將被旋轉(zhuǎn)結(jié)點(diǎn)的右結(jié)點(diǎn)變?yōu)楦附Y(jié)點(diǎn)的左結(jié)點(diǎn)。
          2. 父結(jié)點(diǎn)變?yōu)楸恍D(zhuǎn)結(jié)點(diǎn)的右子結(jié)點(diǎn)
          3. 原父結(jié)點(diǎn)的父結(jié)點(diǎn)(爺爺)變?yōu)楸恍D(zhuǎn)結(jié)點(diǎn)的父結(jié)點(diǎn)。

        雖然1、2兩種旋轉(zhuǎn)定義不同,但是只看移動這分明就是一種嘛。

        有細(xì)心的讀者發(fā)現(xiàn):左右旋的方式與AVL、紅黑樹等其他二叉樹相同。
        因?yàn)檫@是唯一一種不改變中序遍歷的旋轉(zhuǎn)方式。

        左旋與右旋都不會改變中序遍歷的結(jié)果,如上方動圖,中序遍歷始終為1 y 2 x 3

        除了舉例論證,你也可以這樣理解:

        這是因?yàn)樽笮陀倚龝WC旋轉(zhuǎn)后的二叉樹左子結(jié)點(diǎn) < 當(dāng)前結(jié)點(diǎn) < 右子結(jié)點(diǎn),因?yàn)樾D(zhuǎn)沒有插入或刪除結(jié)點(diǎn),所以二叉樹的中序遍歷沒變。

        1.2.3 合并左右旋

        想要合并左右旋,只能使用定義二:把x向上旋轉(zhuǎn)。(原因見下)

        左旋和右旋雖然屬于兩種操作,但是細(xì)想想:
        一個(gè)結(jié)點(diǎn)不是左子結(jié)點(diǎn),就是右子結(jié)點(diǎn)。因?yàn)槲覀円獙⒔Y(jié)點(diǎn)向上旋轉(zhuǎn),所以每一個(gè)結(jié)點(diǎn)(除了根結(jié)點(diǎn)),只能朝一個(gè)方向旋轉(zhuǎn),也就是父結(jié)點(diǎn)的方向。

        所以可以將兩種操作整合為一種:
        當(dāng)被旋轉(zhuǎn)的結(jié)點(diǎn)的類型為左子結(jié)點(diǎn)時(shí),進(jìn)行右旋。
        當(dāng)被旋轉(zhuǎn)的結(jié)點(diǎn)的類型為右子結(jié)點(diǎn)時(shí),進(jìn)行左旋。

        那么如何用一組代碼來表達(dá)兩種旋轉(zhuǎn)呢?
        我們之前對child定義為數(shù)組,那么可以使用child[數(shù)值]來決定訪問的是左結(jié)點(diǎn)還是右結(jié)點(diǎn)。在內(nèi)嵌套get(...),可以得到與被旋轉(zhuǎn)結(jié)點(diǎn)類型相同的子結(jié)點(diǎn)。

        如果get(...)得到的為0(左),怎樣讓其變成1(右),來訪問右結(jié)點(diǎn)呢?
        這個(gè)問題等同于將get(...)取反。

        1. x ^ 1可以將0變1,1變0。
        2. !x,疑似可以,但get(...)的返回值為int型,不建議使用!

        下面這個(gè)動畫對應(yīng)左旋:

        由于旋轉(zhuǎn)改變了父子關(guān)系,所以當(dāng)前結(jié)點(diǎn)與父結(jié)點(diǎn)的size會發(fā)生變化,需要重新更新。
        旋轉(zhuǎn)之后,父親變?yōu)閮鹤?,所以?strong style="line-height: 1.75em;">先更新原父親(被旋轉(zhuǎn)結(jié)點(diǎn)在旋轉(zhuǎn)前的父結(jié)點(diǎn)),后更新原兒子(被旋轉(zhuǎn)結(jié)點(diǎn))。

        代碼實(shí)現(xiàn),注釋中也藏了一些知識點(diǎn)(關(guān)于maintain的順序):

        void rotate(node *current) {
            if (current == root || current == NULL) { // 根結(jié)點(diǎn)沒有父結(jié)點(diǎn),旋轉(zhuǎn)不了;防止傳入空指針后報(bào)錯
                return;
            }
            node *parent = current->parent; // 爹爹
            node *grandParent = parent->parent; // 爺爺,可能是NULL
            int type = get(current); // 被旋轉(zhuǎn)結(jié)點(diǎn)的類型,0為右旋,1為左旋
            int parentType = get(parent); // 爹爹的類型

            connect(parent, current->child[type ^ 1], type); // 將三角形的結(jié)點(diǎn)掛到父結(jié)點(diǎn)
            connect(current, parent, type ^ 1); // 兒子變父親
            if (parent == root) { // 如果父結(jié)點(diǎn)為根結(jié)點(diǎn),需要將根結(jié)點(diǎn)指針更新
                root = current;
            }
            connect(grandParent, current, parentType); // 爺爺認(rèn)孫子為兒子

            maintain(parent); // 此時(shí)parent(父親)變?yōu)樽畹讓拥慕Y(jié)點(diǎn)了,對其先進(jìn)行更新
            maintain(current); // parent結(jié)點(diǎn)更新之后,位于parent父親位置的current進(jìn)行更新
            // 為啥不更新 grandParent 呢?是因?yàn)樾D(zhuǎn)是在 grandParent 以下的位置進(jìn)行的,子樹大小無變化。
        }

        下面的splay采用定義2,也就是上面的實(shí)現(xiàn)。

        1.3 splay

        splay名字很高大上,其實(shí)很簡單。
        splay其實(shí)就是將某一個(gè)結(jié)點(diǎn)經(jīng)過幾次旋轉(zhuǎn)后達(dá)到根結(jié)點(diǎn)位置。

        當(dāng)前結(jié)點(diǎn)、父結(jié)點(diǎn)與爺爺結(jié)點(diǎn)的位置不同,向上旋轉(zhuǎn)的方式也不同。

        前文,我們將左旋與右旋寫到了一起,使用的定義是把x向上旋轉(zhuǎn),此時(shí)splay的邏輯如下:

        • 當(dāng)前結(jié)點(diǎn)為x
        1. 如果x的父結(jié)點(diǎn)為根結(jié)點(diǎn),直接對x進(jìn)行旋轉(zhuǎn)。
        2. 如果父結(jié)點(diǎn)不是根結(jié)點(diǎn),且x與父結(jié)點(diǎn)的類型相同,對父結(jié)點(diǎn)進(jìn)行旋轉(zhuǎn)。
        3. 如果父結(jié)點(diǎn)不是根結(jié)點(diǎn),且x與父結(jié)點(diǎn)的類型不同,對x進(jìn)行旋轉(zhuǎn)。
        void splay(node *current) {
            if (current == NULL || current->parent == NULL) { // 根結(jié)點(diǎn)沒有父結(jié)點(diǎn),會形成死循環(huán);為current傳入NULL以防萬一
                return;
            }
            while (current->parent->parent) { // 父結(jié)點(diǎn)不是根結(jié)點(diǎn)(是根結(jié)點(diǎn)的話get(current->parent)無法正常執(zhí)行)
                if (get(current) == get(current->parent)) { // 類型相同
                    rotate(current->parent);
                } else {
                    rotate(current);
                }
            }
            rotate(current); // 父親為根結(jié)點(diǎn),旋轉(zhuǎn)自己篡位
        }

        1.4 查找

        find(value)用于查找一個(gè)結(jié)點(diǎn),其數(shù)據(jù)與value相同。

        由于平衡樹終究是在BST(二叉搜索樹)之上進(jìn)行變換,查找方式大體與BST相同:

        1. 如果當(dāng)前結(jié)點(diǎn)的值小于要搜索的值:向右結(jié)點(diǎn)查找(右結(jié)點(diǎn)比當(dāng)前結(jié)點(diǎn)大)
        2. 如果當(dāng)前結(jié)點(diǎn)的值大于要搜索的值:向左結(jié)點(diǎn)查找(左結(jié)點(diǎn)比當(dāng)前結(jié)點(diǎn)?。?/section>
        3. 如果兩個(gè)值相等:恭喜你,找到結(jié)點(diǎn)了。

        其中,如果在向子結(jié)點(diǎn)查找時(shí),發(fā)現(xiàn)子結(jié)點(diǎn)為空,那么必然找不到結(jié)點(diǎn)了。(1、2都是對目標(biāo)值進(jìn)行逼近,不存在結(jié)點(diǎn)存在只是沒有被搜索的情況)

        可是伸展樹有一個(gè)特性:在每執(zhí)行完一次操作(查找、插入、刪除等等)后都要對結(jié)點(diǎn)進(jìn)行splay

        在查找這種操作中,被查找的結(jié)點(diǎn)需要在查找到后進(jìn)行splay

        node* find(int value) {
            node *current = root; // 從根結(jié)點(diǎn)開始搜索
            while (current) {
                if (current->value < value) {
                    current = current->child[1]; // 小了,往大了走
                } else if (current ->value > value) {
                    current = current->child[0]; // 大了,往小了走
                } else { // 不大也不小不就是找到了嗎?
                    splay(current);
                    return current;
                }
            }
            return NULL// 找不到結(jié)點(diǎn),退出。(找到得到結(jié)點(diǎn)的話會在while循環(huán)就退出)
        }

        1.5 查詢排名

        rank(value),值為value的結(jié)點(diǎn)的排名,為這個(gè)結(jié)點(diǎn)在中序遍歷中排第幾位。
        雖然排名為中序遍歷的排名,但是我們并不需要對整個(gè)樹進(jìn)行中序遍歷。
        排名可以看做:結(jié)點(diǎn)左面的結(jié)點(diǎn)個(gè)數(shù) + 1(這個(gè)1對應(yīng)它自己)

        由于我們事先不知道值value對應(yīng)哪一個(gè)結(jié)點(diǎn),所以我們需要將上文的find整合進(jìn)來。
        維護(hù)一個(gè)變量leftSize,用于記錄結(jié)點(diǎn)左側(cè)共多少個(gè)結(jié)點(diǎn),

        1. 如果當(dāng)前結(jié)點(diǎn)的值大于要搜索的值:向左結(jié)點(diǎn)查找(左邊沒有結(jié)點(diǎn),leftSize不改變)
        2. 這個(gè)時(shí)候果斷將leftSize += 左子樹大小,因?yàn)槟繕?biāo)結(jié)點(diǎn)不是在當(dāng)前結(jié)點(diǎn)就是在右結(jié)點(diǎn),這兩種情況都約過了左子樹。
          如果兩個(gè)值相等,那么就是找到了,將leftSize += 1,對找到的結(jié)點(diǎn)進(jìn)行splay,返回leftSize。(+ 1是自己的)
        3. 2沒有返回目標(biāo)結(jié)點(diǎn)必然在右子樹,leftSize += 當(dāng)前結(jié)點(diǎn)的count向右結(jié)點(diǎn)查找吧。
        int rankOfNode(int value) {
            // 這里傳入的value為被查詢排名的結(jié)點(diǎn)的value
            node *current = root; // 從根結(jié)點(diǎn)開始搜索
            int leftSize = 0// 左側(cè)的所有結(jié)點(diǎn)總和
            while (current) {
                if (value < current->value) { // 目標(biāo)結(jié)點(diǎn)小于當(dāng)前結(jié)點(diǎn),向左走
                    current = current->child[0];
                } else {
                    // 無論是目標(biāo)結(jié)點(diǎn)為當(dāng)前結(jié)點(diǎn),還是在右子樹,都跨過了左子樹
                    if (current->child[0]) { // 可能沒有左子樹
                        leftSize += current->child[0]->size;
                    }

                    if (value == current->value) {
                        leftSize += 1// 加上自己的。你就想最左邊結(jié)點(diǎn)leftSize = 0,可是排名是1
                        splay(current);
                        return leftSize;
                    }
                    // 找到結(jié)點(diǎn)的話會return,所以這里必然是目標(biāo)結(jié)點(diǎn)大,向右走
                    leftSize += current->count; // 把當(dāng)前結(jié)點(diǎn)越過,加上
                    current = current->child[1]; // 這個(gè)current變到右子結(jié)點(diǎn)是必須放在最后的,否則前面會亂
                }
            }
            return -1// 找不到結(jié)點(diǎn),輸出-1
        }

        1.6 查詢排名為rank的結(jié)點(diǎn)

        當(dāng)我們知道了排名,怎樣找對應(yīng)的結(jié)點(diǎn)呢?

        勘誤:動畫中判斷應(yīng)為rank <= 0,而非rank == 0。由于例子中所有結(jié)點(diǎn)的count都為1,所以表面看來沒有問題。

        我們設(shè) 以當(dāng)前結(jié)點(diǎn)為根結(jié)點(diǎn)的樹中,需要查詢排名為rank。

        1.如果rank > 左子樹的size

        • 那么目標(biāo)結(jié)點(diǎn)只能存在于當(dāng)前結(jié)點(diǎn)與右子樹之間。我們對rank -= 左子樹的size + 當(dāng)前結(jié)點(diǎn)的count。
          • 如果此時(shí)rank <= 0,那么目標(biāo)結(jié)點(diǎn)就在根結(jié)點(diǎn)。(一個(gè)極端例子,第一個(gè)結(jié)點(diǎn)的count為999,1 ~ 999都對應(yīng)了這一個(gè)結(jié)點(diǎn),rank -= count就會出現(xiàn)負(fù)數(shù))
          • 如果此時(shí)rank > 0,那么目標(biāo)結(jié)點(diǎn)還在右結(jié)點(diǎn),把當(dāng)前結(jié)點(diǎn)設(shè)置為右結(jié)點(diǎn),整個(gè)過程再來一遍。

        2.如果rank <= 左子樹的size

        • 那么目標(biāo)結(jié)點(diǎn)肯定藏匿于左子樹,將目標(biāo)結(jié)點(diǎn)設(shè)置為左結(jié)點(diǎn),再來一遍。

        向右走,越過了左子樹的所有結(jié)點(diǎn)與當(dāng)前結(jié)點(diǎn),所以要對rank減越過的結(jié)點(diǎn)。
        向左走,越過了空氣什么也沒有越過,故rank不動。

        因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(60, 112, 198);">rank在做完減法后就會判斷是否為小于0,小于0就退出了。所以其余時(shí)間里rank > 0。

        node* nodeOfRank(int rank) {
            node *current = root; // 從根結(jié)點(diǎn)查找
            while (current) {
                if ((current->child[0] && rank > current->child[0]->size) || current->child[0] == NULL) {
                    // 1. 左子樹存在,rank > 且左子樹大小
                    // 2. 左子樹不存在,大小可看做0。因?yàn)閞ank > 0,所以無需判斷可知 rank > 左子樹大小。
                    if (current->child[0]) {
                        rank -= current->child[0]->size;
                    }
                    rank -= current->count;

                    if (rank <= 0) {
                        splay(current);
                        return current;
                    }
                    current = current->child[1]; // 不是在左子樹、當(dāng)前結(jié)點(diǎn),就是在右子樹唄
                } else {
                    current = current->child[0]; // 目標(biāo)結(jié)點(diǎn)在左子樹
                }
            }
            return NULL;
        }

        1.7 前趨后繼

        前趨pre:比查詢結(jié)點(diǎn)小的最大結(jié)點(diǎn)。
        后繼next:比查詢結(jié)點(diǎn)大的最小結(jié)點(diǎn)。

        由于結(jié)點(diǎn)的左子樹任意一個(gè)結(jié)點(diǎn)都比當(dāng)前結(jié)點(diǎn)小,在左子樹中取最大的結(jié)點(diǎn)即為前趨。
        后繼同理,右子樹任意一個(gè)結(jié)點(diǎn)都比當(dāng)前結(jié)點(diǎn)大,在右子樹中取最小的結(jié)點(diǎn)即為后繼。

        取最大值最小值與BST相同:
        最大:找樹的最右側(cè)結(jié)點(diǎn)。(右 > 中 > 左)
        最?。赫覙涞淖钭髠?cè)結(jié)點(diǎn)。(左 < 中 < 右)

        node* preNext(int type, node *current) // 0為前趨,1為后繼
            if (current == NULL) {
                return NULL;
            }

            splay(current);
            current = current->child[type]; // 如果current沒有左結(jié)點(diǎn),那么current會變?yōu)镹ULL
            while (current && current->child[type ^ 1]) { // current->child[1]用來避免current為NULL
                current = current->child[type ^ 1];
            }
            splay(current);
            return current;
        }

        1.8 插入

        插入有三種情況:

        1. 整棵樹是空的,root為NULL,這種情況下我們直接將結(jié)點(diǎn)放在root的位置即可。
        2. 在樹的已有結(jié)點(diǎn)中存在新插入的值,由于二叉搜索樹中不能出現(xiàn)兩個(gè)值一樣的結(jié)點(diǎn),所以對已有的結(jié)點(diǎn)的count加1即可。
        3. 樹中沒有這個(gè)新插入的值,那么我們需要找到合適的位置,在插入后進(jìn)行splay

        第三種情況:

        1. find的過程一樣,當(dāng)前結(jié)點(diǎn)小于插入值向右子樹查找,當(dāng)前結(jié)點(diǎn)大于插入值向左查找(等于就屬于第二種情況了),直到當(dāng)前結(jié)點(diǎn)為NULL,找到一個(gè)空位置。(這里我們需要記錄當(dāng)前結(jié)點(diǎn)的parent,因?yàn)镹ULL結(jié)點(diǎn)是記錄不了信息的)
        2. 連接 NULL位置的父結(jié)點(diǎn) 與 插入的結(jié)點(diǎn)。
        3. splay插入結(jié)點(diǎn)。
        node* insert(int value) {
            if (root == NULL) {
                root = new node(value);
                return root;
            }
            node *current = root;
            node *parent = current->parent;
            int type; // 類型
            while (current) { // 和查找一模一樣
                if (current->value < value) {
                    parent = current;
                    current = current->child[1];
                    type = 1;
                } else if (current->value > value) {
                    parent = current;
                    current = current->child[0];
                    type = 0;
                } else { // 已經(jīng)有相同結(jié)點(diǎn)了,將其count++即可。
                    current->count ++;
                    splay(current);
                    return current;
                }
            }
            current = new node(value);
            connect(parent, current, type);
            splay(current);
            return current;
        }

        1.9 合并兩棵樹

        合并兩棵樹,我們設(shè)它們的根結(jié)點(diǎn)分別為xy
        要使兩棵樹能夠合并,x中的最大值要小于y中的最小值。

        合并過程:

        1. xy有一個(gè)樹是空的,返回不是空的那個(gè)。
        2. xy均不為空。
          1. splay x中的最大值。
          2. 此時(shí)x的根結(jié)點(diǎn)的右子樹為空,將y作為x的右子樹(因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(60, 112, 198);">右 > 中 > 左,x的最大值還在根結(jié)點(diǎn),沒有比最大值還有大的了,所有右側(cè)沒有結(jié)點(diǎn))

        合并操作需要在刪除中遇到,動畫與實(shí)現(xiàn)均在刪除中。

        1.10 刪除

        刪除過程:

        1. 我們首先將被刪除的結(jié)點(diǎn)進(jìn)行splay。
        2. 銷毀被刪除結(jié)點(diǎn),與左右子樹斷開聯(lián)系。
        3. 合并兩棵樹(右合并到左)
        // 求以current為根的樹的最大與最小值
        node* minMax(int type, node *current) // type == 0為min,type == 1為max
            while (current->child[type]) {
                current = current->child[type];
            }
            splay(current);
            return current;
        }

        void remove(node *current) {
            splay(current);
            if (current->count >= 2) {
                current->count --;
                return;
            }
            node *left = current->child[0];
            node *right = current->child[1];
            if (left) {
                left->parent = NULL;
            }
            if (right) {
                right->parent = NULL;
            }
            delete current;
            if (left && right) { // 兩個(gè)都有
                left = minMax(1, left); // 求最大值最小值時(shí)默認(rèn)進(jìn)行了splay
                right = minMax(0, right);
                connect(left, right, 1);
                root = left;
            } else {
                if (left) { // 只有左結(jié)點(diǎn)
                    root = left;
                } else { // 只有右結(jié)點(diǎn),或者兩個(gè)都沒有。兩個(gè)都沒有right為NULL,root就變成NULL了
                    root = right;
                }
            }
        }

        1.11 代碼

        LOJ上的普通平衡樹[2]

        我們上述的幾種操作個(gè)個(gè)對應(yīng)題目需要。但是當(dāng)我們仔細(xì)讀題:

        的前趨(前趨定義為小于 ,且最大的數(shù));
        的后繼(后繼定義為大于 ,且最小的數(shù))。

        本文中的前趨后繼為結(jié)點(diǎn)的前一個(gè)與后一個(gè),而題目中 可能不存在,怎么辦呢?
        簡單!將x插入到樹中,進(jìn)行查詢,隨后再刪除不就行了嗎?

        完整代碼如下,LOJ中不開優(yōu)化測評記錄[3]

        //
        // Created by Cat-shao on 2021/5/8.
        //

        #include <cstdio>
        #include <algorithm>
        #include <deque>

        using namespace std;

        class splayTree {
        public:
            class node {
            public:
                node *parent; // 父結(jié)點(diǎn)的指針
                node *child[2]; // child[0]為左子結(jié)點(diǎn)的指針,child[1]為右子結(jié)點(diǎn)的指針。
                int value, count, size; // 數(shù)據(jù),出現(xiàn)次數(shù),子樹大小

                node(int _value) {
                    value = _value;
                    parent = child[0] = child[1] = NULL;
                    count = size = 1;
                }
            };

            node *root;

            splayTree() {
                root = NULL;
            }

            ~splayTree() {
                destroy(root);
            }

            void destroy(node *current) {
                if (current) {
                    destroy(current->child[0]);
                    destroy(current->child[1]);
                    delete current;
                }
            }

            void maintain(node *current) // 更新size
                if (current == NULL) { // 可能傳入的是一個(gè)空指針
                    return;
                }
                current->size = current->count; // 先將自己加上
                if (current->child[0]) { // 防止沒兒子,NULL,報(bào)錯
                    current->size += current->child[0]->size; // 加上兒子
                }
                if (current->child[1]) {
                    current->size += current->child[1]->size;
                }
            }

            int get(node *current) // 獲得結(jié)點(diǎn)是左結(jié)點(diǎn)還是右結(jié)點(diǎn),0左1右
                if (current == NULL || current->parent == NULL) { // 傳入空指針;根結(jié)點(diǎn)沒有父親,特判一下
                    return -1;
                }
                return current->parent->child[1] == current; // 父親的右子結(jié)點(diǎn) 是不是 自己
            }

            void connect(node *parent, node *current, int type) // 父結(jié)點(diǎn)指針,當(dāng)前結(jié)點(diǎn)指針,類型(0左1右)
                if (parent) { // parent 可能為NULL
                    parent->child[type] = current;
                }
                if (current) { // current 也可能為NULL
                    current->parent = parent;
                }
            }

            void rotate(node *current) {
                if (current == root || current == NULL) { // 根結(jié)點(diǎn)沒有父結(jié)點(diǎn),旋轉(zhuǎn)不了;防止傳入空指針后報(bào)錯
                    return;
                }
                node *parent = current->parent; // 爹爹
                node *grandParent = parent->parent; // 爺爺,可能是NULL
                int type = get(current); // 被旋轉(zhuǎn)結(jié)點(diǎn)的類型,0為右旋,1為左旋
                int parentType = get(parent); // 爹爹的類型

                connect(parent, current->child[type ^ 1], type); // 將三角形的結(jié)點(diǎn)掛到父結(jié)點(diǎn)
                connect(current, parent, type ^ 1); // 兒子變父親
                if (parent == root) { // 如果父結(jié)點(diǎn)為根結(jié)點(diǎn),需要將根結(jié)點(diǎn)指針更新
                    root = current;
                }
                connect(grandParent, current, parentType); // 爺爺認(rèn)孫子為兒子

                maintain(parent); // 此時(shí)parent(父親)變?yōu)樽畹讓拥慕Y(jié)點(diǎn)了,對其先進(jìn)行更新
                maintain(current); // parent結(jié)點(diǎn)更新之后,位于parent父親位置的current進(jìn)行更新
                // 為啥不更新 grandParent 呢?是因?yàn)樾D(zhuǎn)是在 grandParent 以下的位置進(jìn)行的,子樹大小無變化。
            }

            void splay(node *current) {
                if (current == NULL || current->parent == NULL) { // 根結(jié)點(diǎn)沒有父結(jié)點(diǎn),會形成死循環(huán);為current傳入NULL以防萬一
                    return;
                }
                while (current->parent->parent) { // 父結(jié)點(diǎn)不是根結(jié)點(diǎn)(是根結(jié)點(diǎn)的話get(current->parent)無法正常執(zhí)行)
                    if (get(current) == get(current->parent)) { // 類型相同
                        rotate(current->parent);
                    } else {
                        rotate(current);
                    }
                }
                rotate(current); // 父親為根結(jié)點(diǎn),旋轉(zhuǎn)自己篡位
            }

            node* find(int value) {
                node *current = root; // 從根結(jié)點(diǎn)開始搜索
                while (current) {
                    if (current->value < value) {
                        current = current->child[1]; // 小了,往大了走
                    } else if (current->value > value) {
                        current = current->child[0]; // 大了,往小了走
                    } else { // 不大也不小不就是找到了嗎?
                        splay(current);
                        return current;
                    }
                }
                return NULL// 找不到結(jié)點(diǎn),退出。(找到得到結(jié)點(diǎn)的話會在while循環(huán)就退出)
            }

            int rankOfNode(int value) {
                // 這里傳入的value為被查詢排名的結(jié)點(diǎn)的value
                node *current = root; // 從根結(jié)點(diǎn)開始搜索
                int leftSize = 0// 左側(cè)的所有結(jié)點(diǎn)總和
                while (current) {
                    if (value < current->value) { // 目標(biāo)結(jié)點(diǎn)小于當(dāng)前結(jié)點(diǎn),向左走
                        current = current->child[0];
                    } else {
                        // 無論是目標(biāo)結(jié)點(diǎn)為當(dāng)前結(jié)點(diǎn),還是在右子樹,都跨過了左子樹
                        if (current->child[0]) { // 可能沒有左子樹
                            leftSize += current->child[0]->size;
                        }

                        if (value == current->value) {
                            leftSize += 1// 加上自己的。你就想最左邊結(jié)點(diǎn)leftSize = 0,可是排名是1
                            splay(current);
                            return leftSize;
                        }
                        // 找到結(jié)點(diǎn)的話會return,所以這里必然是目標(biāo)結(jié)點(diǎn)大,向右走
                        leftSize += current->count; // 把當(dāng)前結(jié)點(diǎn)越過,加上
                        current = current->child[1]; // 這個(gè)current變到右子結(jié)點(diǎn)是必須放在最后的,否則前面會亂
                    }
                }
                return -1// 找不到結(jié)點(diǎn),輸出-1
            }

            node* nodeOfRank(int rank) {
                node *current = root; // 從根結(jié)點(diǎn)查找
                while (current) {
                    if ((current->child[0] && rank > current->child[0]->size) || current->child[0] == NULL) {
                        // 1. 左子樹存在,rank > 且左子樹大小
                        // 2. 左子樹不存在,大小可看做0。因?yàn)閞ank > 0,所以無需判斷可知 rank > 左子樹大小。
                        if (current->child[0]) {
                            rank -= current->child[0]->size;
                        }
                        rank -= current->count;

                        if (rank <= 0) {
                            splay(current);
                            return current;
                        }
                        current = current->child[1]; // 不是在左子樹、當(dāng)前結(jié)點(diǎn),就是在右子樹唄
                    } else {
                        current = current->child[0]; // 目標(biāo)結(jié)點(diǎn)在左子樹
                    }
                }
                return NULL;
            }

            node* preNext(int type, node *current) // 0為前趨,1為后繼
                if (current == NULL) {
                    return NULL;
                }

                splay(current);
                current = current->child[type]; // 如果current沒有左結(jié)點(diǎn),那么current會變?yōu)镹ULL
                while (current && current->child[type ^ 1]) { // current->child[1]用來避免current為NULL
                    current = current->child[type ^ 1];
                }
                splay(current);
                return current;
            }

            node* insert(int value) {
                if (root == NULL) {
                    root = new node(value);
                    return root;
                }
                node *current = root;
                node *parent = current->parent;
                int type; // 類型
                while (current) { // 和查找一模一樣
                    if (current->value < value) {
                        parent = current;
                        current = current->child[1];
                        type = 1;
                    } else if (current->value > value) {
                        parent = current;
                        current = current->child[0];
                        type = 0;
                    } else { // 已經(jīng)有相同結(jié)點(diǎn)了,將其count++即可。
                        current->count ++;
                        splay(current);
                        return current;
                    }
                }
                current = new node(value);
                connect(parent, current, type);
                splay(current);
                return current;
            }

            node* minMax(int type, node *current) // type == 0為min,type == 1為max
                while (current->child[type]) {
                    current = current->child[type];
                }
                splay(current);
                return current;
            }

            void remove(node *current) {
                splay(current);
                if (current->count >= 2) {
                    current->count --;
                    return;
                }
                node *left = current->child[0];
                node *right = current->child[1];
                if (left) {
                    left->parent = NULL;
                }
                if (right) {
                    right->parent = NULL;
                }
                delete current;
                if (left && right) { // 兩個(gè)都有
                    left = minMax(1, left); // 求最大值最小值時(shí)默認(rèn)進(jìn)行了splay
                    right = minMax(0, right);
                    connect(left, right, 1);
                    root = left;
                } else {
                    if (left) { // 只有左節(jié)點(diǎn)
                        root = left;
                    } else { // 只有右節(jié)點(diǎn),或者兩個(gè)都沒有。兩個(gè)都沒有right為NULL,root就變成NULL了
                        root = right;
                    }
                }
            }
        };

        void LOJ104()
        {
            int n, opt, x;
            splayTree tree = splayTree();

            scanf("%d", &n);
            for (int i = 1; i <= n; i++) {
                scanf("%d%d", &opt, &x);
                switch (opt) {
                    case 1: tree.insert(x); break;
                    case 2: tree.remove(tree.find(x)); break;
                    case 3printf("%d\n", tree.rankOfNode(x)); break;
                    case 4printf("%d\n", tree.nodeOfRank(x)->value); break;
                    case 5:
                        tree.insert(x);
                        printf("%d\n", tree.preNext(0, tree.find(x))->value);
                        tree.remove(tree.find(x));
                        break;
                    case 6:
                        tree.insert(x);
                        printf("%d\n", tree.preNext(1, tree.find(x))->value);
                        tree.remove(tree.find(x));
                        break;
                }
            }
        }

        int main()
        {
            LOJ104();
        }

        1.12 時(shí)間復(fù)雜度

        伸展樹可視化[4](需要將網(wǎng)址復(fù)制到瀏覽器中,網(wǎng)址見頁腳)

        在學(xué)完了伸展樹的基礎(chǔ)操作之后,我們發(fā)現(xiàn)主要是splay來維護(hù)整個(gè)二叉樹平衡,然而splay后的樹并不平衡。

        伸展樹的時(shí)間復(fù)雜度是均攤,Tarjan是怎么將這個(gè)復(fù)雜度算出來的呢?

        你百度一下,就會看見很多人使用了勢函數(shù)來證明。

        身為一個(gè)蒟蒻,我無法證明??墒?,經(jīng)過測試,伸展樹與其他平衡樹的速度大同小異。

        1.13 與其他平衡樹比較

        紅黑樹AVLfhq treapsplay(伸展樹)
        速度最快最平衡,查找最快代碼最好打?

        這么看伸展樹就一打醬油的,那這個(gè)東西到底有什么意義呢?

        伸展樹的優(yōu)勢在于操作多

        欲知后事如何,且聽下回分解!


        參考與鳴謝

        1. OI Wiki[5]:OIer的算法wiki,知識點(diǎn)全面但是小白看不懂,大家可以收藏。
        2. KHIN[6]:問了KH很多問題,受益頗多。
        3. manim幼兒園[7]:本文所有的動畫皆為manim所做,manim幼兒園的視頻教程十分詳細(xì)。

        伸展樹算法偏難,你若有什么問題,歡迎回復(fù),或者在LOJ的討論[8]中發(fā)出你的觀點(diǎn)。

        討論中可能會跟進(jìn)一些內(nèi)容(如前趨后繼的更好實(shí)現(xiàn)、勘誤)。

        參考資料

        [1]

        splay tree demo: https://www.link.cs.cmu.edu/splay/

        [2]

        LOJ上的普通平衡樹: https://loj.ac/p/104

        [3]

        LOJ中不開優(yōu)化測評記錄: https://loj.ac/s/1141137

        [4]

        伸展樹可視化: https://www.cs.usfca.edu/~galles/visualization/SplayTree.html

        [5]

        OI Wiki: https://oi-wiki.org/ds/splay/#_11

        [6]

        KHIN: https://www.luogu.com.cn/user/236807

        [7]

        manim幼兒園: https://manim.wiki/

        [8]

        LOJ的討論: https://loj.ac/d/3181



        瀏覽 82
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            一区二区三区日 | 午夜爽爽爽羞羞视频影院 | 欧美一级黄 | 男生被女生操 | 日韩AV成人在线 | 欧美日韩免费观看一区=区三区 | 日韩精品免费一区二区三区 | 操中国美女 | www.色五月.com | 97视频 |