Trie樹

Trie樹簡介

Trie 樹庐镐,也叫字典樹或者叫前綴樹。顧名思義,它是一個樹形結(jié)構(gòu)贝搁。它是一種專門處理字符串匹配的樹狀結(jié)構(gòu)吗氏,用來解決在一組字符串集合中快速查找某個字符串的問題。Trie 樹的本質(zhì)雷逆,就是利用字符串之間的公共前綴弦讽,將重復(fù)的前綴合并在一起。

下面展示了一個由“hello”关面、“her”坦袍、“hi”、“how”等太、“see”以及“so”組成的Trie樹捂齐。

Trie-Tres.jpg

Trie特點

由上圖中的Trie中可知

  • Trie的每個節(jié)點存儲一個字符,根節(jié)點不保存字符
  • 從根節(jié)點到某一個節(jié)點缩抡,路徑上經(jīng)過的字符連接起來奠宜,為該節(jié)點對應(yīng)的字符串
  • 每個節(jié)點的所有子節(jié)點包含的字符都不相同

實現(xiàn)Trie樹

Trie樹節(jié)點

由于每個節(jié)點只存儲一個字符,以及指向它的子節(jié)點瞻想,那么對于每個節(jié)點有如下的數(shù)據(jù)結(jié)構(gòu)

function TrieNode(key) {
    this.key = key;
    this.children = [];
}

其中key保存了這個節(jié)點的值压真,children中保存了該節(jié)點的所有子節(jié)點(對于小寫英文字母而言,這個數(shù)組的長度為26)

Trie樹的插入

構(gòu)建一棵Trie樹就是將字符串逐個插入的過程蘑险。由于Trie的特點滴肿,插入操作的過程中。要先判斷Trie是否已經(jīng)存在了與待插入字符串相同的前綴佃迄,找到所有的公共前綴后泼差,將不同的字符插入Trie中。對于插入的過程呵俏,大致的代碼如下:

// 采用遞歸插入字符串
function insertData(data, node){
    if (data === '') {
        return;
    }

    let children = node.children;
    let haveData = null;
    
   // 判斷存儲子節(jié)點的數(shù)組中堆缘,是否有與插入字符串的第一個值匹配的節(jié)點
    for (let i in children) {
        if (children[i].key == data[0]) {
            haveData = children[i];
        }
    }
    
    if(haveData) {
        // 待插入的第一個字符已經(jīng)存在children數(shù)組中,則繼續(xù)判斷下一個字符
        this.insertData(data.substring(1), haveData);
    }else{
      // 未找到相應(yīng)的子節(jié)點普碎,分為兩種情況
      // 已經(jīng)是現(xiàn)有Trie的葉子節(jié)點
        if(children.length === 0) {
            let insertNode = new TrieNode(data[0]);
            children.push(insertNode);
            this.insertData(data.substring(1), insertNode); 
        }else{
          // 要在當(dāng)前的children中找到合適的位置吼肥,插入字符
            let validPosition = 0;
            for (let j in children) {
                if (children[j].key < data[0]) {
                    validPosition++;
                }
            }
            let insertNode = new TrieNode(data[0]);
            children.splice(validPosition, 0, insertNode);
            this.insertData(data.substring(1), insertNode); 
        }
    }
}

在插入Trie樹的過程中,需要遍歷所有的字符串麻车,時間復(fù)雜度是O(n)缀皱,n表示所有字符串的長度和。

Trie樹的查找

查找的過程與插入的過程類似动猬,需要逐層遍歷唆鸡,尋找與待匹配字符串相同的字符。其大概代碼如下:

function search (data) {
    if (data === '' || this.root.children.length === 0) {
        return false;
    }
  // 遍歷children
    for (let i in this.root.children) {
        if (this.searchNext(this.root.children[i], data)) {
            return true;
        }
    }
    return false;
}
// 遞歸查找
function searchNext(node, data) {
    if(data[0] !== node.key) return false;
    let children = node.children;
  // 該節(jié)點已經(jīng)是葉子節(jié)點枣察,并且待查找字符串已查找完畢
    if(children.length === 0 && data.length === 1) {
        return true;
    } else if(children.length > 0 && data.length > 1) {
        for(let i in children) {
            // 繼續(xù)判斷下一個字符
            if(children[i].key === data[1]) {
                return searchNext(children[i], data.substring(1));
            }
        }
    } else {
        return false;
    }
}

在Trie樹已經(jīng)構(gòu)建完成的情況下争占,查找一個字符串是否在Trie中效率是非常高的燃逻,其事件復(fù)雜度為O(K),k表示待查找字符串的長度臂痕。

Trie樹的刪除

先遞歸查找到字符串的葉子節(jié)點伯襟,然后從字符串的葉子節(jié)點逐級向根節(jié)點遞歸刪除葉子節(jié)點,直到刪除字符串握童。其大概的代碼如下:

function deleteNode(data) {
    if (this.search(data)) { // 判斷是否存在該單詞(字符串)
        for (let i in this.root.children) {
            if (this.deleteNext(this.root, i, data, data)) {
                return;
            }
        }
    }
    return this;
}

/**
* @param parent 父節(jié)點
* @param index 子節(jié)點在父節(jié)點children數(shù)組中的索引位置
* @param stringData 遞歸遍歷中的字符串
* @param delStr 調(diào)用delete方法時的原始字符串
*/
function deleteNext(parent, index, stringData, delStr) {
    let node = parent.children[index];
    // 若字符與節(jié)點key不相等姆怪,則不匹配
    if (stringData[0] != node.key) {
        return false;
    } else { // 若與key相等,繼續(xù)判斷
        let children = node.children;
        if (children.length == 0 && stringData.length == 1) { // 葉子節(jié)點澡绩,最后一個字符稽揭,則完全匹配
            // 刪除葉子節(jié)點,利用父節(jié)點刪除子節(jié)點原理
            parent.children.splice(index, 1);
            // 字符串從尾部移除一個字符后肥卡,繼續(xù)遍歷刪除方法
            this.deleteNode(delStr.substring(0, delStr.length - 1));
        } else if (children.length > 0 && stringData.length > 1) { // 既不是葉子節(jié)點溪掀,也不是最后一個字符,則繼續(xù)遞歸查找
            for (let i in children) {
                if (children[i].key == stringData[1]) {
                    return this.deleteNext(node, i, stringData.substring(1), delStr); // 記得return 遞歸函數(shù)步鉴,否則獲取的返回值為undefined
                }
            }
        }
    }
}

Trie改進

Trie樹是非常消耗內(nèi)存的數(shù)據(jù)結(jié)構(gòu)揪胃,用的是一種空間換時間的思路。Trie樹的問題就在于存儲子節(jié)點的children數(shù)組中氛琢。如果字符串中只包含從a到z的26個字符喊递,那么children的長度就為26。注意阳似,這里說的是每一個Trie樹的節(jié)點骚勘,都需要申請一個長度為26的數(shù)組,即使這個節(jié)點只有一個子節(jié)點撮奏!

那么如果字符串包含了大小寫俏讹,或者是數(shù)字特殊字符。Trie所需的空間就更大了挽荡。尤其是在重復(fù)的前綴不多的情況下藐石,Trie樹不但不能節(jié)省內(nèi)存即供,而且還有可能浪費更多的內(nèi)存空間定拟。可以想到的方法就是逗嫡,將children修改為其他的數(shù)據(jù)結(jié)構(gòu)青自,比如有序數(shù)組、跳表等驱证。

對只有一個子節(jié)點的節(jié)點延窜,而且此節(jié)點不是一個串的結(jié)束節(jié)點可以將此節(jié)點與子節(jié)點合并,這就是縮點優(yōu)化抹锄。

Trie-up.jpg

Trie樹完整代碼

function TrieNode(key) {
    this.key = key;
    this.children = [];
}

function Trie() {
    this.root = new TrieNode('/'); // 添加根節(jié)點
    this.insert = insert; // 插入
    this.insertData = insertData;
    this.search = search; // 查找
    this.searchNext = searchNext;
    this.deleteNode = deleteNode; // 刪除
    this.deleteNext = deleteNext;

    this.nodeNumber = 0; // trie所有節(jié)點個數(shù)
    this.print = print; // 打印Trie樹
    this.printHelper = printHelper;
}

function insert(data) {
    this.insertData(data, this.root);
}

function insertData(data, node){
    if (data === '') {
        return;
    }

    let children = node.children;
    let haveData = null;
    
    for (let i in children) {
        if (children[i].key == data[0]) {
            haveData = children[i];
        }
    }
    
    if(haveData) {
        this.insertData(data.substring(1), haveData);
    }else{
        if(children.length === 0) {
            let insertNode = new TrieNode(data[0]);
            children.push(insertNode);
            this.insertData(data.substring(1), insertNode); 
        }else{
            let validPosition = 0;
            for (let j in children) {
                if (children[j].key < data[0]) {
                    validPosition++;
                }
            }
            let insertNode = new TrieNode(data[0]);
            children.splice(validPosition, 0, insertNode);
            this.insertData(data.substring(1), insertNode); 
        }
    }
}

function search (data) {
    if (data === '' || this.root.children.length === 0) {
        return false;
    }
    for (let i in this.root.children) {
        if (this.searchNext(this.root.children[i], data)) {
            return true;
        }
    }
    return false;
}

function searchNext(node, data) {
    if(data[0] !== node.key) return false;
    let children = node.children;
    if(children.length === 0 && data.length === 1) {
        return true;
    } else if(children.length > 0 && data.length > 1) {
        for(let i in children) {
            if(children[i].key === data[1]) {
                return searchNext(children[i], data.substring(1));
            }
        }
    } else {
        return false;
    }
}

function deleteNode(data) {
    if (this.search(data)) { 
        for (let i in this.root.children) {
            if (this.deleteNext(this.root, i, data, data)) {
                return;
            }
        }
    }
    return this;
}

function deleteNext(parent, index, stringData, delStr) {
    let node = parent.children[index];
    if (stringData[0] != node.key) {
        return false;
    } else {
        let children = node.children;
        if (children.length == 0 && stringData.length == 1) { 
            parent.children.splice(index, 1);
            this.deleteNode(delStr.substring(0, delStr.length - 1));
        } else if (children.length > 0 && stringData.length > 1) {
            for (let i in children) {
                if (children[i].key == stringData[1]) {
                    return this.deleteNext(node, i, stringData.substring(1), delStr);
                }
            }
        }
    }
}

function print () {
    for (let i in this.root.children) {
        this.nodeNumber++;
        this.printHelper(this.root.children[i], [this.root.children[i].key]);
    }
}

function printHelper (node, data) {
    if (node.children.length === 0) {
        console.log('>', data.join(''));
        return;
    }
    for (let i in node.children) {
        this.nodeNumber++;
        data.push(node.children[i].key);
        this.printHelper(node.children[i], data);
        data.pop();
    }
}

let trie = new Trie();

trie.insert('apple');
trie.insert('appcd');
trie.insert('banana');

trie.print();
console.log(trie.search("app"));
trie.deleteNode('appcd')
trie.print();

console.log(trie.nodeNumber);

// 輸出結(jié)果
> appcd
> apple
> banana
false
> apple
> banana
11

學(xué)習(xí)心得

第一次看Trie樹的概念逆瑞,完全沒明白到底是什么樣的數(shù)據(jù)結(jié)構(gòu)荠藤。直到看了Trie樹的圖示例,一下就明白了获高,Trie樹的概念非常好理解哈肖,就是把相同的前綴放到一起構(gòu)成了一個數(shù)據(jù)結(jié)構(gòu)。在對比字符串的時候念秧,就像查字典一層層的比較淤井。

對于每個節(jié)點的存儲結(jié)構(gòu)確實比較繞,每次遍歷children數(shù)組就相當(dāng)于去往下一層比較了摊趾,反正每次涉及到遞歸函數(shù)總是不好理解币狠。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砾层,一起剝皮案震驚了整個濱河市漩绵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梢为,老刑警劉巖渐行,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異铸董,居然都是意外死亡祟印,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門粟害,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕴忆,“玉大人,你說我怎么就攤上這事悲幅√锥欤” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵汰具,是天一觀的道長卓鹿。 經(jīng)常有香客問我,道長留荔,這世上最難降的妖魔是什么吟孙? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮聚蝶,結(jié)果婚禮上杰妓,老公的妹妹穿的比我還像新娘碘勉。我一直安慰自己巷挥,他們只是感情好验靡,可當(dāng)我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布雏节。 她就那樣靜靜地躺著,像睡著了一般高职。 火紅的嫁衣襯著肌膚如雪矾屯。 梳的紋絲不亂的頭發(fā)上初厚,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機與錄音产禾,去河邊找鬼排作。 笑死,一個胖子當(dāng)著我的面吹牛亚情,可吹牛的內(nèi)容都是我干的妄痪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼衫生,長吁一口氣:“原來是場噩夢啊……” “哼土浸!你這毒婦竟也來了罪针?” 一聲冷哼從身側(cè)響起黄伊,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎墓阀,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斯撮,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡扶叉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辜梳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泳叠。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡危纫,死狀恐怖乌庶,靈堂內(nèi)的尸體忽然破棺而出契耿,到底是詐尸還是另有隱情,我是刑警寧澤搪桂,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站踢械,受9級特大地震影響酗电,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜内列,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望话瞧。 院中可真熱鬧,春花似錦划滋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽稻薇。三九已至胶征,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間睛低,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工骂铁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拉庵。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓套蒂,卻偏偏與公主長得像茫蛹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婴洼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,647評論 2 354

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

  • 什么是“Trie樹”? Trie樹撼嗓,又稱前綴樹或字典樹,是一種有序樹且警,用于保存關(guān)聯(lián)數(shù)組,其中的鍵通常是字符串振湾。與二...
    尼桑麻閱讀 849評論 0 2
  • 數(shù)據(jù)結(jié)構(gòu)在各大開發(fā)語言中應(yīng)用廣泛押搪,尤其是后端開發(fā)對數(shù)據(jù)的處理树酪,而大部分前端開發(fā)很少應(yīng)用到大州,或是應(yīng)用場景不允許等因素...
    煙傷肺閱讀 3,532評論 1 3
  • 字典樹(Trie)筆記 特別聲明 本文只是一篇筆記類的文章,所以不存在什么抄襲之類的疮茄。 以下為我研究時參考過的鏈接...
    Harlan1994閱讀 20,232評論 2 21
  • 1根暑、 概述 Trie樹力试,又稱字典樹排嫌,單詞查找樹或者前綴樹,是一種用于快速檢索的多叉樹結(jié)構(gòu)淳地,如英文字母的字典樹是一個...
    Shadowsocks2閱讀 703評論 1 2
  • 聲明:摘自github:https://github.com/ZtesoftCS/go-ethereum-code...
    藍Renly閱讀 772評論 0 0