<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 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),原來的速度 會(huì)掉到 ,與鏈表一個(gè)速度,失去了價(jià)值。


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

          伸展樹的核心,是通過不斷的將結(jié)點(diǎn)旋轉(zhuǎn)到根結(jié)點(diǎn)(即為splay操作),來保證整棵樹不會(huì)跟鏈表一樣。它由 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),否則會(huì)內(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)錯(cuò)
                      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;
                  }
              }
          };

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

          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)

          上面的動(dòng)畫使用文字?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)定義不同,但是只看移動(dòng)這分明就是一種嘛。

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

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

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

          這是因?yàn)樽笮陀倚龝?huì)保證旋轉(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è)動(dòng)畫對應(yīng)左旋:

          由于旋轉(zhuǎn)改變了父子關(guān)系,所以當(dāng)前結(jié)點(diǎn)與父結(jié)點(diǎn)的size會(huì)發(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)錯(cuò)
                  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ì)形成死循環(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)小)
          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)的話會(huì)在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)的話會(huì)return,所以這里必然是目標(biāo)結(jié)點(diǎn)大,向右走
                      leftSize += current->count; // 把當(dāng)前結(jié)點(diǎn)越過,加上
                      current = current->child[1]; // 這個(gè)current變到右子結(jié)點(diǎn)是必須放在最后的,否則前面會(huì)亂
                  }
              }
              return -1// 找不到結(jié)點(diǎn),輸出-1
          }

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

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

          勘誤:動(dòng)畫中判斷應(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就會(huì)出現(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不動(dòng)。

          因?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在做完減法后就會(huì)判斷是否為小于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會(huì)變?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))

          合并操作需要在刪除中遇到,動(dòng)畫與實(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)錯(cuò)
                      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)錯(cuò)
                      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ì)形成死循環(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)的話會(huì)在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)的話會(huì)return,所以這里必然是目標(biāo)結(jié)點(diǎn)大,向右走
                          leftSize += current->count; // 把當(dāng)前結(jié)點(diǎn)越過,加上
                          current = current->child[1]; // 這個(gè)current變到右子結(jié)點(diǎn)是必須放在最后的,否則前面會(huì)亂
                      }
                  }
                  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會(huì)變?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ù)雜度算出來的呢?

          你百度一下,就會(huì)看見很多人使用了勢函數(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]:本文所有的動(dòng)畫皆為manim所做,manim幼兒園的視頻教程十分詳細(xì)。

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

          討論中可能會(huì)跟進(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)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲综合777777 | 久久久久久米奇影视 | 国产黄色片片片 | 睛唱久久久久久久 | 91人妻人人澡人人精品 |