一種字典樹(shù)結(jié)構(gòu)的高效實(shí)現(xiàn)

前段時(shí)間逛論壇图甜,發(fā)現(xiàn)了一篇高效的字典樹(shù)實(shí)現(xiàn)論文,很有意思叮称。

簡(jiǎn)書(shū)專用圖

常見(jiàn)的字典樹(shù)實(shí)現(xiàn)方法

class Node{                            
  uint node ;
  uint[] next;  
};```                      
或者類似如下結(jié)構(gòu)

class Node{
uint node;
map<> next;
}

第一種保證了查找效率,但是對(duì)于字典樹(shù)這種稀疏數(shù)組,空間利用率比較低笼痹,特別是遇到中文日文這種,會(huì)造成空間極大浪費(fèi)酪穿。因此凳干,部分選擇了第二種實(shí)現(xiàn),當(dāng)然第二種可以保證空間被济,但是卻是在犧牲了效率的基礎(chǔ)上的改進(jìn)救赐。map的查找速度```O(logn)```肯定不如數(shù)組```O(1)```快。
####另一種高效實(shí)現(xiàn)
下面介紹另一種實(shí)現(xiàn)只磷,它將字典樹(shù)用base, check數(shù)組存儲(chǔ)起來(lái)经磅,不僅壓縮了數(shù)組,而且不降低查找效率钮追。這就是雙數(shù)組字典樹(shù)(Double array trie) 预厌。這種字典樹(shù)原理很簡(jiǎn)單,即下面的等式

if (check[base[pre] + string.charAt(i) ] == pre)
pre = base[pre] + string.charAt(i)

對(duì)于base和check數(shù)組雖然```雙數(shù)組字典樹(shù)```論文作者并沒(méi)有特意描述它的含義元媚,簡(jiǎn)單來(lái)說(shuō)base數(shù)組和check數(shù)組更像一種父子關(guān)系轧叽。check數(shù)組存儲(chǔ)當(dāng)前節(jié)點(diǎn)的父親節(jié)點(diǎn)苗沧。

而我那天看到的論文是在此基礎(chǔ)上的改進(jìn)。論文中配圖下的小標(biāo)題犹芹,作者取名為```ReducedTrie```
它和原生的雙數(shù)組字典樹(shù)的最明顯的區(qū)別在于崎页,它多了一個(gè)tail數(shù)組,這個(gè)數(shù)組的含義就像它的英文含義一樣腰埂。reducedTrie的base, check數(shù)組僅存儲(chǔ)前綴部分飒焦,而非前綴部分全部放到tail數(shù)組中。

那么如何定位tail數(shù)組的位置呢屿笼?在base數(shù)組之中牺荠,每個(gè)字符串結(jié)尾的字符的base值為其后綴在tail的下標(biāo)的負(fù)值。舉例說(shuō)```base[10] = -12 ```不那么說(shuō)明```tail[12]```數(shù)組之后直到結(jié)束字符都是后綴驴一。

好處很明顯肿孵,節(jié)約了base, check的空間減少了非前綴部分的計(jì)算璃弄,同樣也有缺點(diǎn)喳坠,最大的缺點(diǎn)個(gè)人覺(jué)得是在存儲(chǔ)為文件上丈钙,由于之前只需要base, check數(shù)組,并且是兩個(gè)數(shù)組成對(duì)出現(xiàn)胸懈,因此很容易將字典樹(shù)壓縮離線存儲(chǔ)為一個(gè)文件担扑,而tail數(shù)組和base, check數(shù)組并無(wú)關(guān)系,因此需要兩個(gè)文件存儲(chǔ)趣钱。另一點(diǎn)就是tail數(shù)組的碎片太多涌献,這個(gè)后面說(shuō)插入會(huì)講到。
####具體實(shí)現(xiàn)
其實(shí)講了tail數(shù)組的作用之后首有,再結(jié)合雙數(shù)組字典樹(shù)燕垃,就能夠很快理解這個(gè)結(jié)構(gòu)了。
##### insert
插入是比較復(fù)雜的部分井联。分為四種情況處理卜壕。論文原文如下
```
1. Insertion of the new word when the double-array is empty. 
2. Insertion of the new word without any collisions. 
3. Insertion of the new word with a collision; in this case, additional characters must be added to the BASE and characters must be removed from the TAIL array to resolve the collision, but nothing already in the BASE array must be removed. 
4. When insertion of the new word with a collision as in case 3 occurs, values in the BASE array must be moved.
```
拙略地翻一下,大致是
```
1.當(dāng)數(shù)組為空則直接插入新節(jié)點(diǎn).(體現(xiàn)節(jié)點(diǎn)為空檢查check數(shù)組為0即可)
2.沒(méi)有任何沖突的插入.
3.插入新節(jié)點(diǎn)時(shí)發(fā)生了一種不需要修改base數(shù)組烙常,但是必須修改tail和多添加base字符用以解決沖突的沖突印叁。
4.插入新節(jié)點(diǎn)發(fā)生了類似3的情況,但是base數(shù)組的值必須被清空
```
如果你實(shí)現(xiàn)過(guò)雙數(shù)組字典樹(shù)军掂,那么你很快就明白了作者所說(shuō)的沖突原意。如果沒(méi)有實(shí)現(xiàn)過(guò)昨悼,那么用白話說(shuō)下來(lái)蝗锥,就是以下兩種。

- 當(dāng)base為負(fù)值(base[i] <0 說(shuō)明此節(jié)點(diǎn)為終結(jié)點(diǎn))而需要新插入節(jié)點(diǎn)率触,那么這就算一種沖突终议,解決這種沖突,必須要對(duì)比tail數(shù)組。舉個(gè)論文中的例子穴张,bachelor與badge细燎,當(dāng)插入bachelor時(shí),b節(jié)點(diǎn)在樹(shù)中皂甘,achelor都在tail數(shù)組中玻驻,所以插入badge時(shí)候,b節(jié)點(diǎn)比對(duì)完了偿枕,base就為負(fù)值了璧瞬,這時(shí)候就需要比對(duì)badge的a和tail中的a。由于``` a == a ```因此直接插入一個(gè)新節(jié)點(diǎn)就可以繼續(xù)走下去了渐夸。

  那么如果遇到節(jié)點(diǎn)不同呢嗤锉?還是上面的例子,走完a就來(lái)到了(c墓塌,d)顯然不相等瘟忱,那么需要新建兩個(gè)節(jié)點(diǎn)。原理和上面所說(shuō)的相同苫幢。

- 當(dāng)base[pre]>0時(shí)访诱, 并且check[base[pre] + char ] != pre那么說(shuō)明發(fā)生了需要更改base值的沖突。這種沖突的本質(zhì)是由于兩個(gè)節(jié)點(diǎn)的base值相同态坦,因此必須更改其中一個(gè)值來(lái)解決沖突盐数。需要更改的base的節(jié)點(diǎn)就是pre和check[ base[pre] + char] 其中的一個(gè)。

 那么更改哪一個(gè)伞梯?選擇從兩個(gè)節(jié)點(diǎn)中選擇孩子節(jié)點(diǎn)少的玫氢。因?yàn)閎ase值改變了意味著他們的孩子節(jié)點(diǎn)的base值也要隨著改變,如果節(jié)點(diǎn)的孩子也有孩子```暫稱為孫子吧```谜诫,那么```孫子```節(jié)點(diǎn)的父親也是要更改的漾峡。孩子節(jié)點(diǎn)有了新值,那么舊值就可以清0了喻旷。新節(jié)點(diǎn)就可以插入了

 但是在這其中有個(gè)巨坑的點(diǎn)生逸,就是如果沖突的兩個(gè)節(jié)點(diǎn)剛好是父子關(guān)系,那么一定要更新好父親的下標(biāo)且预。

可能上面說(shuō)的還是不能解惑槽袄,那我下面放出具體解決上面兩種沖突的實(shí)現(xiàn)代碼
```
其中xCheck(List list)是從1查找一個(gè)的值q,能夠讓list中的任何一個(gè)變量x都滿足check[q+x]=0
moveTail(int x)即在x位置開(kāi)始直到讀到結(jié)束字符這段區(qū)間內(nèi)锋谐,tail向左移動(dòng)一位
writeTail(int[] value, int x)從value數(shù)組的x位置開(kāi)始向tail數(shù)組寫(xiě)入遍尺。
put(int key, value) 緩存pre節(jié)點(diǎn)的孩子
初始化時(shí)候base[1] = 1 check[1] = 1
```
第一種沖突
```java
if (base[pre] < 0) {
    //if current node is an end-point ,then separate or create a new node
    int oldBase = base[pre];
    if (tail[-oldBase] == keyValue[i]) {
        //create a new node
        base[pre] = xCheck(keyValue[i]);
        base[ base[pre]+keyValue[i] ] = oldBase;
        check[ base[pre]+keyValue[i] ] = pre;
        put(pre, keyValue[i]);
        moveTail(-oldBase);
        pre = base[pre] + keyValue[i];
        continue;
    } else {
        //separate
        List<Integer> list = new ArrayList<>();
        list.add(tail[-oldBase]); list.add(keyValue[i]);
        base[pre] = xCheck(list);
        base[ base[pre]+tail[-oldBase] ] = oldBase;
        base[ base[pre]+keyValue[i] ] = -position;
        check[ base[pre]+tail[-oldBase] ] = check[ base[pre]+keyValue[i] ] = pre;
        writeTail(keyValue, i+1);
        put(pre, tail[-oldBase]);
        put(pre, keyValue[i]);
        moveTail(-oldBase);
        break;// 2 new nodes
    }
} 
```
第二種沖突
- @param node1為當(dāng)前節(jié)點(diǎn)位置
- @param node2為另一個(gè)已存在的與node1發(fā)生沖突的節(jié)點(diǎn)
- @param newNodeValue為需要插入的節(jié)點(diǎn)值

請(qǐng)注意巨坑點(diǎn) :))))
```java
public int processConflict(int node1, int node2, int newNodeValue) {
    int node = (lists[node1].size()+1) < lists[node2].size() ? node1 : node2;
    int oldNodeBase = base[node];
    if (node == node1) {
        base[node] = xCheck(lists[node], newNodeValue);
    } else {
        base[node] = xCheck(lists[node]);
    }
    for (int i = 0; i < lists[node].size(); i++) {
        int oldNext = oldNodeBase + lists[node].get(i);
        int newNext = base[node] + lists[node].get(i);
        if (oldNext == node1) node1 = newNext;//巨坑點(diǎn)
        base[newNext] = base[oldNext];
        check[newNext] = node;
        if (base[oldNext] > 0) {
            for (int j = 0; j < lists[oldNext].size(); j++) {
                check[ base[oldNext] + lists[oldNext].get(j) ] = newNext;
                put(newNext, lists[oldNext].get(j));
            }
            lists[oldNext] = null;
        }
        base[oldNext] = 0; check[oldNext] = 0;
    }
    base[ base[node1] + newNodeValue ] = -position;
    check[ base[node1] + newNodeValue ] = node1;
    put(node1, newNodeValue);
    return node;
}
```
如果你還是有所疑惑,建議閱讀An Efficient Implementation of Trie Structures這篇原文或者翻譯版涮拗。
##### search
搜索就很簡(jiǎn)單了乾戏,按照文章最開(kāi)始的
```
public boolean search(int[] key) {
    int pre = 1;
    for (int i = 0; i < key.length; i++) {
        if (base[pre] < 0) {
            return compareTail(-base[pre], i, key);
        } else if (base[pre] > 0) {
            if (check[ base[pre] + key[i] ] == pre) {
                pre = base[pre] + key[i];
            } else {
                return false;
            }
        } else return false;
    }
    return true;
}
```
##### delete
對(duì)于刪除操作迂苛,只需要找到每個(gè)單詞最后一個(gè)節(jié)點(diǎn),釋放它即可鼓择。
```$java
public boolean delete(String key) {
    int []keyValue = string2IntArray(key);
    int pre = 1;
    int index = -1;
    int tempVal;
    int next;
    do {
        index++;
        tempVal = keyValue[index];
        next = base[pre] + tempVal;
        if (check[next] != pre)  {
            return false;
        }
        if (base[next] < 0) break;
        pre = next;
    } while (true);
    if (tempVal == END_FLAG || compareTail(-base[next], index+1, keyValue)) {
        for (int i = 0; i < lists[pre].size(); i++) {
            if (lists[pre].get(i) == tempVal) {
                lists[pre].remove(i);break;
            }
        }
        base[next] = 0; check[next] = 0;
        //info(String.format("%s next[%d] turn to 0",key, next));
        return true;
    }
    return false;
}
```
##### usage
字典樹(shù)最常見(jiàn)的用途就是前綴匹配和前綴提示三幻,trie樹(shù)建立成功之后就可以根據(jù)輸入的前綴去尋找從這個(gè)節(jié)點(diǎn)出發(fā)所有可能的字符串。這里采用深度優(yōu)先遍歷呐能。
```java
private void find( int pre, StringBuilder builder, List<String> list) {
    int next;
    if (base[pre] < 0) {
        builder.append(readTail(-base[pre]));
        list.add(builder.toString());
        return;
    }
    for (int i = 0; i < lists[pre].size(); i++) {
        next = base[pre] + lists[pre].get(i);
        StringBuilder reserved = new StringBuilder(builder.toString());
        if (check[next] == pre) {
            if (lists[pre].get(i) == END_FLAG) {
                find(next, builder, list);
            } else {
                find(next, builder.append((char) lists[pre].get(i).intValue()), list);
            }
        }
        builder = reserved;
    }
}
```

#### 總結(jié)
好了念搬,還記得之前我所說(shuō)過(guò)這種結(jié)構(gòu)的缺點(diǎn)中的一點(diǎn)是建立過(guò)程tail數(shù)組的碎片化嚴(yán)重嗎?為什么這么說(shuō)呢催跪,因?yàn)樵谔幚淼谝环N沖突的時(shí)候锁蠕,會(huì)不斷地移動(dòng)的tail數(shù)組,比如bachelor和badge懊蒸,原本tail數(shù)組中是achelor#由于對(duì)比```a==a```成功荣倾,所以向左移動(dòng)一位,chelor?#而這個(gè)'?'的部分骑丸,就無(wú)法被后續(xù)的節(jié)點(diǎn)插入使用舌仍,當(dāng)然也是有解決辦法的,需要在樹(shù)建立成功之后整塊移動(dòng)通危。

我在論壇上看到一種看法說(shuō)這種寫(xiě)法在插入時(shí)發(fā)生沖突概率這么大铸豁,有什么實(shí)用性呢?其實(shí)這種結(jié)構(gòu)在插入過(guò)程慢一點(diǎn)是無(wú)所謂的菊碟,字典樹(shù)用途主要看它的查找效率节芥。我們完全可以用長(zhǎng)時(shí)間建立然后存儲(chǔ)每個(gè)節(jié)點(diǎn)的值,第二次直接讀進(jìn)內(nèi)存逆害,就可以直接使用头镊,建立過(guò)程只需要一次即可。

//第一次碼這么多字魄幕,好累啊

//不過(guò)雙數(shù)組字典樹(shù)性能真的很強(qiáng)

測(cè)試源代碼:[ReducedTrie](https://github.com/saltyx/ReducedTrie)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末相艇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子纯陨,更是在濱河造成了極大的恐慌坛芽,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翼抠,死亡現(xiàn)場(chǎng)離奇詭異咙轩,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)阴颖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門活喊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人膘盖,你說(shuō)我怎么就攤上這事胧弛。” “怎么了侠畔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵结缚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我软棺,道長(zhǎng)红竭,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任喘落,我火速辦了婚禮茵宪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瘦棋。我一直安慰自己稀火,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布赌朋。 她就那樣靜靜地躺著凰狞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沛慢。 梳的紋絲不亂的頭發(fā)上赡若,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音团甲,去河邊找鬼逾冬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛躺苦,可吹牛的內(nèi)容都是我干的身腻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼圾另,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼霸株!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起集乔,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤去件,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后扰路,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體尤溜,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年汗唱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宫莱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哩罪,死狀恐怖授霸,靈堂內(nèi)的尸體忽然破棺而出巡验,到底是詐尸還是另有隱情,我是刑警寧澤碘耳,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布显设,位于F島的核電站,受9級(jí)特大地震影響辛辨,放射性物質(zhì)發(fā)生泄漏捕捂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一斗搞、第九天 我趴在偏房一處隱蔽的房頂上張望指攒。 院中可真熱鬧,春花似錦僻焚、人聲如沸允悦。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)澡屡。三九已至,卻和暖如春咐旧,著一層夾襖步出監(jiān)牢的瞬間驶鹉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工铣墨, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留室埋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓伊约,卻偏偏與公主長(zhǎng)得像姚淆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子屡律,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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