平衡樹不好打?試試Trie字典樹!


Woc,考場(面試)忘記打平衡樹怎么辦,Trie救你命

文本只發布於博客園,其他地方出現本文均是未經許可的盜版。

算法導入

眾所周知平衡樹很難打(大佬除外),還老是調錯。萬一這種事情發生在關鍵時刻你就GG了。那我們怎么辦呢?

從本質上介紹,平衡樹作用就是維護一個有序的序列(關系)。很多操作我們用vector(數組)+lower_bound(二分查找),都可以實現。但是vector插入不便,導致序列維護不便。如果用list(鏈表)代替vector,那么查找又很慢。怎么辦呢?

考慮到我們一般都是維護一個整數的排序,那么我能不能找到一種支持動態排序的數據結構。堆可以進行動態處理,但是只有第一個是有序的,他的子節點只能滿足比他小(大)。

難道除了平衡樹我們就沒有別的方法了嗎?答案是有的。看看下面的樹。

graph TD Root-->A["0"] Root-->B["1"] A["0"]-->C["0"] A["0"]-->D["1"] B["1"]-->E["0"] B["1"]-->F["1"] C["0"].->G["0"] D["1"].->H["1"] E["0"].->I["2"] F["1"].->K["3"]

這不是01Trie(字典樹)嗎?這能當平衡樹????

別急,仔細看看。

這個Trie是不是左子樹小於右子樹?

好像是啊

再想想為什么?

應該是二進制關系,左邊的節點當前這一位是0,后面更小的位再大,也不可能比當前這一位是1的大了。

沒錯

你再想想,Trie字典樹是不是可以動態插入?

好像是

所以我們可以利用這種關系實現平衡了。甚至我們還能很簡單的實現可持久化

但是不難看出有幾個缺點。

  1. 空間要比一般平衡樹大,一個int32最壞情況需要32個int32去記錄他。當然實際情況也不可能都是這樣。實際運用中有許多的重復節點,可以重復利用的。知道數字范圍的時候也能調整樹高解決。在一道平衡樹的模板題,內存是FHQ_Treap的1.7倍,速度更快,內存是Splay的2/3,速度差不多。是紅黑樹的4/5,速度是1/2。

  2. 不能直接輸入負數(除非你建樹的時候把符號位也建立進去,不過這樣你也不好解決大小查找,畢竟負數是補碼,情況和正數相反)。你需要加一個比較大的數值使得負數變為正數。

  3. 不能進行序列操作(不過支持這個操作的平衡樹也不多)。

當然直接用Trie也不行,你還需要維護幾個信息,否則你沒法實現一些功能。
你需要維護

  1. 每個節點對應子樹大小
  2. 每個葉子節點的值的數量(為了支持重復值插入)

代碼實現

提前定義

這里我定義樹高為25層

const int MAXLOG=25;

節點信息

需要保存兩個兒子,節點子樹大小,葉子節點計數

代碼如下

struct node
{
    int ch[2];
    int sze, cnt;
} Tree[MAXLOG];
int p; //模擬一個內存池,這是對應的頂。

插入&刪除 操作

插入操作其實和一般樹沒什么區別,唯一不同的地方就是你需要維護節點的sze和cnt信息。

刪除操作和插入操作其實一樣,區別就是一個做加法,一個做減法。

代碼如下

void updata(int x, int offset) // 插入/刪除 offset個x,插入正,刪除負
{
    int now = 0; //0是根
    for (int i = MAXLOG - 1; i >= 0; i--)
    {
        bool now_bit = (x & (1 << i) != 0); //第i位的數值
        if (!Tree[now].ch[now_bit] == 0)    //指向空,需要分配子節點
            Tree[now].ch[now_bit] = ++p;
        now = Tree[now].ch[now_bit];
        Tree[now].sze += offset; //這條子樹鏈上的節點都需要
    }
    Tree[now].cnt += offset; //跑到葉子節點了
}

數取排名

代碼也是比較簡單,就是注意當節點為NULL的時候就可以退出了。

int get_rank(int x) //獲取比x小的數的數量,不包括本身
{
    int now = 0; //0是根
    int ans = 0;
    for (int i = MAXLOG - 1; i >= 0; i--)
    {
        bool now_bit = ((x & (1 << i)) != 0); //第i位的數值
        if (now_bit == 1)                     //處於大的那一邊
            ans += Tree[Tree[now].ch[0]].sze; //把比自己小的那些加上
        now = Tree[now].ch[now_bit];
        if (now == 0) //別忘了,這里的0是空的意思
            break;
    }
    return ans;
}

排名取數

int get_kth(int k) //通過排名大小
{
    int now = 0; //0是根
    int ans = 0;
    for (int i = MAXLOG - 1; i >= 0; i--)
    {
        int temp = Tree[Tree[now].ch[0]].sze; //看看自己前面有多少
        if (k <= temp)                        //還在更前面
            now = Tree[now].ch[0];
        else //在自己后面
        {
            k -= temp; //更新相對在右子樹的位置
            now = Tree[now].ch[1];
            ans |= (1 << i); //可以確定這一位是1
        }
        if (now == 0) //別忘了,這里的0是空的意思
            break;
    }
    return ans;
}

求前驅&后繼

這個也是草雞簡單

int pre(int x)
{
    return get_kth(get_rank(x));
}
int nxt(int x)
{
    return get_kth(get_rank(x + 1) + 1);
}

更多

由於這個樹是基於Trie的,Trie能可持久化,這個也能。不過可持久化我們以后再講。


完整代碼下載地址:https://files.cnblogs.com/files/blogs/694685/TrieBST.7z


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM