1. 程式人生 > >從Trie樹(字典樹)談到字尾樹

從Trie樹(字典樹)談到字尾樹

引言

常關注本blog的讀者朋友想必看過此篇文章:從 B樹、B+樹、B*樹談到R 樹 ,這次,咱們來講另外兩種樹:Tire樹與字尾樹。不過,在此之前,先來看兩個問題。

第一個問題: 一個文字檔案,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。

之前在此文: 海量資料處理面試題集錦與Bit-map 詳解中給出的參考答案:用trie樹統計每個詞出現的次數,時間複雜度是O(n*le)(le表示單詞的平均長度),然後是找出出現最頻繁的前10個詞。也可以用堆來實現(具體的操作可參考第三章、尋找最小的k個數),時間複雜度是O(n*lg10)。所以總的時間複雜度,是O(n*le)與O(n*lg10)中較大的哪一個。

第二個問題: 找出給定字串裡的最長迴文。例子:輸入XMADAMYX。則輸出MADAM。這道題的流行解法是用字尾樹(Suffix Tree),但其用途遠不止如此,它能高效解決一大票複雜的字串程式設計問題(當然,它有它的弱點,如演算法實現複雜以及空間開銷大),概括如下:

  • 查詢字串S是否包含子串S1。主要思想是:如果S包含S1,那麼S1必定是S的某個字尾的字首;又因為S的字尾樹包含了所有的字尾,所以只需對S的字尾樹使用和Trie相同的查詢方法查詢S1即可(使用字尾樹實現的複雜度同流行的KMP演算法的複雜度相當)。
  • 找出字串S的最長重複子串S1。比如abcdabcefda裡abc同da都重複出現,而最長重複子串是abc。
  • 找出字串S1同S2的最長公共子串。注意最長公共子串(Longest CommonSubstring)和最長公共子序列(LongestCommon Subsequence, LCS)的區別:子串(Substring)是串的一個連續的部分,子序列(Subsequence)則是從不改變序列的順序,而從序列中去掉任意的元素而獲得的新序列;更簡略地說,前者(子串)的字元的位置必須連續,後者(子序列LCS)則不必。比如字串acdfg同akdfc的最長公共子串為df,而他們的最長公共子序列是adf。LCS可以使用動態規劃法解決。
  • Ziv-Lampel無失真壓縮演算法。 LZW演算法的基本原理是利用編碼資料本身存在字串重複特性來實現資料壓縮,所以一個很好的選擇是使用字尾樹的形式來組織儲存字串及其對應壓縮碼值的字典。
  • 找出字串S的最長迴文子串S1。例如:XMADAMYX的最長迴文子串是MADAM(此即為上面所說的第二個問題:最長迴文問題,本文第二部分將詳細闡述此問題)。
  • 多模式串的模式匹配問題(suffix_array + 二分)。

本文 第一部分 ,咱們就來了解這個 Trie樹 ,然後自然而然過渡到 第二部分、字尾樹 權作此番闡述,以備不時之需,在需要的時候便可手到擒來。ok,有任何問題,歡迎不吝指正或賜教。謝謝。

第一部分、Trie樹

1.1、什麼是Trie樹

Trie樹,即字典樹,又稱單詞查詢樹或鍵樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。

Trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。

它有3個基本性質:

  1. 根節點不包含字元,除根節點外每一個節點都只包含一個字元。
  2. 從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
  3. 每個節點的所有子節點包含的字元都不相同。

1.2、樹的構建

舉個在網上流傳頗廣的例子,如下:

題目:給你100000個長度不超過10的單詞。對於每一個單詞,我們要判斷他出沒出現過,如果出現了,求第一次出現在第幾個位置。

分析:這題當然可以用hash來解決,但是本文重點介紹的是trie樹,因為在某些方面它的用途更大。比如說對於某一個單詞,我們要詢問它的字首是否出現過。這樣hash就不好搞了,而用trie還是很簡單。

現在回到例子中,如果我們用最傻的方法,對於每一個單詞,我們都要去查詢它前面的單詞中是否有它。那麼這個演算法的複雜度就是O(n^2)。顯然對於100000的範圍難以接受。現在我們換個思路想。假設我要查詢的單詞是abcd,那麼在他前面的單詞中,以b,c,d,f之類開頭的我顯然不必考慮。而只要找以a開頭的中是否存在abcd就可以了。同樣的,在以a開頭中的單詞中,我們只要考慮以b作為第二個字母的,一次次縮小範圍和提高針對性,這樣一個樹的模型就漸漸清晰了。

好比假設有b,abc,abd,bcd,abcd,efg,hii 這6個單詞,我們構建的樹就是如下圖這樣的:

當時第一次看到這幅圖的時候,便立馬感到此樹之不凡構造了。單單從上幅圖便可窺知一二,好比大海搜人,立馬就能確定東南西北中的到底哪個方位,如此迅速縮小查詢的範圍和提高查詢的針對性,不失為一創舉。

ok,如上圖所示,對於每一個節點,從根遍歷到他的過程就是一個單詞,如果這個節點被標記為紅色,就表示這個單詞存在,否則不存在。

那麼,對於一個單詞,我只要順著他從根走到對應的節點,再看這個節點是否被標記為紅色就可以知道它是否出現過了。把這個節點標記為紅色,就相當於插入了這個單詞。

這樣一來我們查詢和插入可以一起完成(重點體會這個查詢和插入是如何一起完成的,稍後,下文具體解釋),所用時間僅僅為單詞長度,在這一個樣例,便是10。

我們可以看到,trie樹每一層的節點數是26^i級別的。所以為了節省空間。我們用動態連結串列,或者用陣列來模擬動態。空間的花費,不會超過單詞數×單詞長度。

1.3、字首查詢

上文中提到”比如說對於某一個單詞,我們要詢問它的字首是否出現過。這樣hash就不好搞了,而用trie還是很簡單“。下面,咱們來看看這個字首查詢問題:

已知n個由小寫字母構成的平均長度為10的單詞,判斷其中 是否存在某個串為另一個串的字首子串 。下面對比3種方法:

  1. 最容易想到的:即從字串集中從頭往後搜,看每個字串是否為字串集中某個字串的字首,複雜度為O(n^2)。
  2. 使用hash:我們用hash存下所有字串的所有的字首子串,建立存有子串hash的複雜度為O(n*len),而查詢的複雜度為O(n)* O(1)= O(n)。
  3. 使用trie:因為當查詢如字串abc是否為某個字串的字首時,顯然以b,c,d....等不是以a開頭的字串就不用查找了。所以建立trie的複雜度為O(n*len),而 建立+查詢在trie中是可以同時執行的 ,建立的過程也就可以成為查詢的過程,hash就不能實現這個功能。所以總的複雜度為O(n*len),實際查詢的複雜度也只是O(len)。(說白了,就是 Trie樹的平均高度h為len,所以Trie樹的查詢複雜度為O(h)=O(len) 。好比一棵二叉平衡樹的高度為logN,則其查詢,插入的平均時間複雜度亦為O(logN))。

下面解釋下上述方法3中所說的為什麼hash不能將建立與查詢同時執行,而Trie樹卻可以:

  • 在hash中,例如現在要輸入兩個串911,911456,如果要同時查詢這兩個串,且查詢串的同時若hash中沒有則存入。那麼,這個查詢與建立的過程就是先查詢其中一個串911,沒有,然後存入9、91、911;而後查詢第二個串911456,沒有然後存入9、91、911、9114、91145、911456。因為程式沒有記憶功能,所以並不知道911在輸入資料中出現過,只是照常以例行事,存入9、91、911、9114、911...。也就是說用hash必須先存入所有子串,然後for迴圈查詢。
  • 而trie樹中,存入911後,已經記錄911為出現的字串,在存入911456的過程中就能發現而輸出答案;倒過來亦可以,先存入911456,在存入911時,當指標指向最後一個1時,程式會發現這個1已經存在,說明911必定是某個字串的字首。

讀者反饋 @悠悠長風:關於這點,我有不同的看法。hash也是可以實現邊建立邊查詢的啊。當插入911時,需要一個額外的標誌位,表示它是一個完整的單詞。在處理911456時,也是按照前面的查詢9,91,911,當查詢911時,是可以找到前面插入的911,且通過標誌位知道911為一個完整單詞。那麼就可以判斷出911為911456的字首啊。雖然trie樹更適合這個問題,但是我認為hash也是可以實現邊建立,邊查詢。

至於,有關Trie樹的查詢,插入等操作的實現程式碼,網上遍地開花且千篇一律,諸君儘可參考,想必不用我再做多餘費神。

1.4、查詢

Trie樹是簡單但實用的資料結構,通常用於實現字典查詢。我們做即時響應使用者輸入的AJAX搜尋框時,就是Trie開始。本質上,Trie是一顆儲存多個字串的樹。相鄰節點間的邊代表一個字元,這樣樹的每條分支代表一則子串,而樹的葉節點則代表完整的字串。和普通樹不同的地方是,相同的字串字首共享同一條分支。下面,再舉一個例子。給出一組單詞,inn, int, at, age, adv, ant, 我們可以得到下面的Trie:

可以看出:

  • 每條邊對應一個字母。
  • 每個節點對應一項字首。葉節點對應最長字首,即單詞本身。
  • 單詞inn與單詞int有共同的字首“in”, 因此他們共享左邊的一條分支,root->i->in。同理,ate, age, adv, 和ant共享字首"a",所以他們共享從根節點到節點"a"的邊。

查詢操縱非常簡單。比如要查詢int,順著路徑i -> in -> int就找到了。

搭建Trie的基本演算法也很簡單,無非是逐一把每則單詞的每個字母插入Trie。插入前先看字首是否存在。如果存在,就共享,否則建立對應的節點和邊。比如要插入單詞add,就有下面幾步:

  1. 考察字首"a",發現邊a已經存在。於是順著邊a走到節點a。
  2. 考察剩下的字串"dd"的字首"d",發現從節點a出發,已經有邊d存在。於是順著邊d走到節點ad
  3. 考察最後一個字元"d",這下從節點ad出發沒有邊d了,於是建立節點ad的子節點add,並把邊ad->add標記為d。

1.5、Trie樹的應用

除了本文引言處所述的問題能應用Trie樹解決之外,Trie樹還能解決下述問題(節選自此文: 海量資料處理面試題集錦與Bit-map ):

  • 3. 有一個1G大小的一個檔案,裡面每一行是一個詞,詞的大小不超過16位元組,記憶體限制大小是1M。返回頻數最高的100個詞。
  • 9. 1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字串。請怎麼設計和實現?
  • 10. 一個文字檔案,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。
  • 13.尋找熱門查詢: 
    搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255位元組。假設目前有一千萬個記錄,這些查詢串的重複讀比較高,雖然總數是1千萬,但是如果去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的使用者越多,也就越熱門。請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1G。 
    (1) 請描述你解決這個問題的思路; 
    (2) 請給出主要的處理流程,演算法,以及演算法的複雜度。

有了Trie,字尾樹就容易理解了。本文接下來的第二部分,介紹字尾樹。

下面是相關的實現程式碼

package algorithm;

import java.util.*;

/**
 * Created by jdan on 2017/5/11.
 */
public class Trie {
    class Node implements Comparable<Node>{
        private Node[] next;
        private String word;
        private int cnt;

        public Node() {
            next = new Node[26];
            word = "";
            cnt = 0;
        }

        @Override
        public int compareTo(Node node) {
            return cnt - node.cnt;
        }
    }
    Node root;

    public Trie() {
        root = new Node();
    }

    /*
    * insert the String s
    * */
    public void insert(String s) {
        int n = s.length();
        Node curNode = root;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (curNode.next[c-'a'] == null) curNode.next[c-'a'] = new Node();
            curNode = curNode.next[c-'a'];
        }
        curNode.word = s;
        curNode.cnt++;
    }

    /*
    * delete the String s
    * */
    public void delete(String s) {
        int n = s.length();
        Node curNode = root;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (curNode.next[c-'a'] == null) return;
            curNode = curNode.next[c-'a'];
        }
        if (curNode.cnt > 0) curNode.cnt--;
    }

    /*
    * search for the string s in the trie
    * */
    public boolean find(String s) {
        int n = s.length();
        Node curNode = root;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (curNode.next[c-'a'] == null) return false;
            curNode = curNode.next[c-'a'];
        }
        return curNode.cnt > 0;
    }

    public void findKth(Node node, int k, PriorityQueue<Node> queue) {
        if (node == null) return;
        int cnt = node.cnt;
        if (cnt > 0) {
            if (queue.size() < k) queue.add(node);
            else {
                int min = queue.peek().cnt;
                if (cnt > min) {
                    queue.poll();
                    queue.add(node);
                }
            }
        }
        for (int i = 0; i < 26; i++) {
            if (node.next[i] != null) findKth(node.next[i], k, queue);
        }
    }

    /*
    * 查找出現次數最多的k個單詞
    * find the most frequent k words
    * */
    public List<String> findKthLargest(int k) {
        PriorityQueue<Node> queue = new PriorityQueue<>();
        findKth(root, k, queue);
        List<String> res = new ArrayList<>();
        while (!queue.isEmpty()) {
            Node node = queue.poll();
            res.add(node.word);
        }
        return res;
    }


    public void sortTrieMethod(Node node, List<String> res) {
        if (node == null) return;
        if (node.cnt > 0) res.add(node.word);
        for (int i = 0; i < 26; i++) {
            if (node.next[i] != null) sortTrieMethod(node.next[i], res);
        }
    }

    /*
    * sort the words in trie using  lexicographical order
    * */
    public List<String> sortTrie() {
        List<String> res = new ArrayList<>();
        sortTrieMethod(root, res);
        return res;
    }

    public static void main(String[] args) {
        Trie tree = new Trie();
        tree.insert("hello");
        tree.insert("hello");
        tree.insert("hi");
        tree.insert("hi");
        tree.insert("hi");
        System.out.println(tree.sortTrie());
        System.out.println(tree.findKthLargest(1));
    }

}


第二部分、字尾樹

2.1、字尾樹的定義

字尾樹(Suffix tree)是一種資料結構,能快速解決很多關於字串的問題。字尾樹的概念最早由Weiner 於1973年提出,既而由McCreight 在1976年和Ukkonen在1992年和1995年加以改進完善。

字尾,顧名思義,甚至通俗點來說,就是所謂字尾就是後面尾巴的意思。比如說給定一長度為n的字串S=S1S2..Si..Sn,和整數i,1 <= i <= n,子串SiSi+1...Sn便都是字串S的字尾。

以字串S=XMADAMYX為例,它的長度為8,所以S[1..8], S[2..8], ... , S[8..8]都算S的字尾,我們一般還把空字串也算成字尾。這樣,我們一共有如下字尾。對於字尾S[i..n],我們說這項字尾起始於i。

S[1..8], XMADAMYX, 也就是字串本身,起始位置為1 
S[2..8], MADAMYX,起始位置為2 
S[3..8], ADAMYX,起始位置為3 
S[4..8], DAMYX,起始位置為4 
S[5..8], AMYX,起始位置為5 
S[6..8], MYX,起始位置為6 
S[7..8], YX,起始位置為7 
S[8..8], X,起始位置為8 
空字串,記為$。

而後綴樹,就是包含一則字串所有後綴的壓縮Trie。把上面的字尾加入Trie後,我們得到下面的結構:

仔細觀察上圖,我們可以看到不少值得壓縮的地方。比如藍框標註的分支都是獨苗,沒有必要用單獨的節點同邊表示。如果我們允許任意一條邊裡包含多個字 母,就可以把這種沒有分叉的路徑壓縮到一條邊。另外每條邊已經包含了足夠的字尾資訊,我們就不用再給節點標註字串資訊了。我們只需要 在葉節點上標註上每項字尾的起始位置 。於是我們得到下圖:

這樣的結構丟失了某些字尾。比如後 綴X在上圖中消失了 ,因為它正好是字串XMADAMYX的字首。為了避免這種情況,我們也規定 每項字尾不能是其它字尾的字首 。要解決這個問題其實挺簡單,在 待處理的子串後加一個空字串 就行了。例如我們處理XMADAMYX前,先把XMADAMYX變為 XMADAMYX$,於是就得到suffix tree--字尾樹了,如下圖所示:

2.2、字尾樹與迴文問題的關聯

那字尾樹同最長迴文有什麼關係呢?我們得先知道兩個簡單概念:

  • 最低共有祖先, LCA (Lowest Common Ancestor),也就是任意兩節點(多個也行)最長的共有字首。比如下圖中,節點7同節點1的共同祖先是節點5與節點10,但最低共同祖先是5。 查詢LCA的演算法是O(1)的複雜度,當然,代價是需要對字尾樹做複雜度為O(n)的預處理。

  • 廣義字尾樹(Generalized Suffix Tree)。傳統的字尾樹處理一坨單詞的所有後綴。廣義字尾樹儲存任意多個單詞的所有後綴。例如下圖是單詞 XMADAMYX與XYMADAMX的廣義字尾 樹 。注意我們需要區分不同單詞的字尾,所以葉節點用不同的特殊符號與字尾位置配對。

2.3、最長迴文問題的解決

有了上面的概念,本文引言中提出的查詢最長迴文問題就相對簡單了。咱們來回顧下引言中提出的迴文問題的具體描述:找出給定字串裡的最長迴文。例如輸入XMADAMYX,則輸出MADAM。

思維的突破點在於考察迴文的半徑,而不是迴文本身。所謂半徑,就是迴文對摺後的字串。比如迴文MADAM 的半徑為MAD,半徑長度為3,半徑的中心是字母D。顯然,最長迴文必有最長半徑,且兩條半徑相等。還是以MADAM為例,以D為中心往左,我們得到半徑 DAM;以D為中心向右,我們得到半徑DAM。二者肯定相等。因為MADAM已經是單詞XMADAMYX裡的最長迴文,我們可以肯定從 D往左數的字串 DAMX與從D往右數的子串DAMYX共享最長字首DAM 。而這,正是解決迴文問題的關鍵。現在我們有後綴樹,怎麼把從D向左數的字串DAMX變成字尾 呢?

到這個地步,答案應該明顯: 把單詞XMADAMYX翻轉(XMADAMYX=>XYMADAMX , DAMX 就變成字尾了 ) 就行了。於是我們把尋找回文的問題轉換成了尋找兩坨字尾的 LCA 的問題。當然,我們還需要知道 到底查詢那些字尾間的LCA。很簡單,給定字串S,如果最長迴文的中心在i,那從位置i向右數的字尾剛好是S(i),而向左數的字串剛好是翻轉S後得到的字串S‘的字尾S'(n-i+1)。這裡的n是字串S的長度。

可能上面的闡述還不夠直觀,我再細細說明下:

1. 首先,還記得本第二部分開頭關於字尾樹的定義麼: “先說說字尾的定義,顧名思義,甚至通俗點來說,就是所謂字尾就是後面尾巴的意思。比如說給定一長度為n的字串S=S1S2..Si..Sn,和整數i, 1 <= i <= n,子串SiSi+1...Sn便都是字串S的字尾 。”

以字串S=XMADAMYX為例,它的長度為8,所以S[1..8], S[2..8], ... , S[8..8]都算S的字尾,我們一般還把空字串也算成字尾。這樣,我們一共有如下字尾。對於字尾S[i..n],我們說這項字尾起始於i。

S[1..8], XMADAMYX, 也就是字串本身,起始位置為1 
S[2..8], MADAMYX,起始位置為2 
S[3..8], ADAMYX,起始位置為3 
S[4..8], DAMYX,起始位置為4 
S[5..8], AMYX,起始位置為5 
S[6..8], MYX,起始位置為6 
S[7..8], YX,起始位置為7 
S[8..8], X,起始位置為8 
空字串,記為$。

2. 對單詞XMADAMYX而言,迴文中心為D,那麼D向右的字尾 DAMYX 假設是S(i)(當N=8,i從1開始計數,i=4時,便是S(4..8));而對於翻轉後的單詞XYMADAMX而言,迴文中心D向右對應的字尾為 DAMX ,也就是S'(N-i+1)((N=8,i=4,便是S‘(5..8)) 。此刻已經可以得出,它們共享最長字首,即 LCA(DAMYX,DAMX)=DAM 。有了這套直觀解釋,演算法自然呼之欲出:

  1. 預處理字尾樹,使得查詢LCA的複雜度為O(1)。這步的開銷是O(N),N是單詞S的長度 ;

  2. 對單詞的每一位置i(也就是從0到N-1),獲取LCA(S(i), S‘(N-i+1)) 以及LCA(S(i+1), S’(n-i+1))。查詢兩次的原因是我們需要考慮奇數迴文和偶數迴文的情況。這步要考察每坨i,所以複雜度是O(N) ;

  3. 找到最大的LCA,我們也就得到了迴文的中心i以及迴文的半徑長度,自然也就得到了最長迴文。總的複雜度O(n)。

用上圖做例子,i為4時,LCA(4$, 5#)為DAM,正好是最長半徑。當然,這只是直觀的敘述。

上面大致描述了字尾樹的基本思路。要想寫出實用程式碼,至少還得知道下面的知識:

  • 建立字尾樹的O(n)演算法。此演算法有很多種,無論Peter Weiner的73年年度最佳演算法,還是Edward McCreight1976的改進演算法,還是1995年E. Ukkonen大幅簡化的演算法(本文第4部分將重點闡述這種方法),還是Juha Kärkkäinen 和 Peter Sanders2003年進一步簡化的線性演算法,都是O(n)的時間複雜度。至於實際中具體選擇哪一種演算法,可依實際情況而定。
  • 實現字尾樹用的資料結構。比如常用的子結點加兄弟節點列表,Directed 優化字尾樹空間的辦法。比如不儲存子串,而儲存讀取子串必需的位置。以及Directed Acyclic Word Graph,常縮寫為黑哥哥們掛在嘴邊的DAWG。

2.4、字尾樹的應用

字尾樹的用途,總結起來大概有如下幾種:

  1. 查詢字串o是否在字串S中。 
    方案:用S構造字尾樹,按在trie中搜索字串的方法搜尋o即可。 
    原理:若o在S中,則o必然是S的某個字尾的字首。 
    例如S: leconte,查詢o: con是否在S中,則o(con)必然是S(leconte)的字尾之一conte的字首.有了這個前提,採用trie搜尋的方法就不難理解了。
  2. 指定字串T在字串S中的重複次數。 
    方案:用S+’$'構造字尾樹,搜尋T節點下的葉節點數目即為重複次數 
    原理:如果T在S中重複了兩次,則S應有兩個字尾以T為字首,重複次數就自然統計出來了。
  3. 字串S中的最長重複子串 
    方案:原理同2,具體做法就是找到最深的非葉節點。 這個深是指從root所經歷過的字元個數,最深非葉節點所經歷的字串起來就是最長重複子串。 
    為什麼要非葉節點呢?因為既然是要重複,當然葉節點個數要>=2。
  4. 兩個字串S1,S2的最長公共部分 
    方案:將S1#S2$作為字串壓入字尾樹,找到最深的非葉節點,且該節點的葉節點既有#也有$(無#)。

相關推薦

簡單剖析BB-Tree與B+

注意:首先需要說明的一點是:B-樹就是B樹,沒有所謂的B減樹 引言   我們都知道二叉查詢樹的查詢的時間複雜度是O(log N),其查詢效率已經足夠高了,那為什麼還有B樹和B+樹的出現呢?難道它兩的時間複雜度比二叉查詢樹還小嗎?   答案當然不是,B樹和B

BB-Tree與B+詳解

注意:首先需要說明的一點是:B-樹就是B樹,沒有所謂的B減樹 引言   我們都知道二叉查詢樹的查詢的時間複雜度是O(log N),其查詢效率已經足夠高了,那為什麼還有B樹和B+樹的出現呢?難道它兩的時間複雜度比二叉查詢樹還小嗎?    答案當然不是,B樹和B+樹的出現是因

Trie字典字尾

引言 常關注本blog的讀者朋友想必看過此篇文章:從 B樹、B+樹、B*樹談到R 樹 ,這次,咱們來講另外兩種樹:Tire樹與字尾樹。不過,在此之前,先來看兩個問題。 第一個問題: 一個文字檔案,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請

Trie字典字尾10.28修訂

            從Trie樹(字典樹)談到字尾樹說明:本文基本上是“整理”性質,致謝文末的參考文獻。 引言    常關注本blog的讀者朋友想必看過此篇文章:這次,咱們來講另外兩種樹:Tire樹與字尾樹。不過,在此之前,先來看兩個問題。    第一個問題: 一個文字檔

Trie字典1

stdio.h public ctu 哈希 pac 索引 cas proc ren   Trie樹。又稱字典樹,單詞查找樹或者前綴樹,是一種用於高速檢索的多叉樹結構。   Trie樹與二叉搜索樹不同,鍵不是直接保存在節點中,而是由節點在樹中的位置決定。

trie字典

arc delete png 技術分享 我只 blog 存在 紅色 style 核心思想: 利用字符串的公共前綴來降低查詢時間的開銷以達到提高效率的目的 舉個例子 上圖是由 am as tea too tooth two 構成的字典樹。每個節

Trie tree字典

pri table main radix gcc編譯器 out 字典 name dia   Trie tree有時被稱為(digital tree)或(radix tree or prefix tree)。   可能是編譯器問題,我的實現方法用gcc編譯器,debug沒問

hiho 第2周 Trie字典

oid syn one ++ tac col splay str gif 裸字典樹。AC自動機前綴技能 1 #include <set> 2 #include <map> 3 #include <queue> 4

【模板】Trie字典,單詞查詢

int n; // 0為根節點 char a[MAX_N]; // a[0] = 0; int p[MAX_N][26]; void Update(string s) { int now = 0, len = s.size(); for(register int i = 0; i < l

Trie字典:應用於統計和排序

轉載這篇關於字典樹的原因是看到騰訊面試相關的題:就是在海量資料中找出某一個數,比如2億QQ號中查找出某一個特定的QQ號。。 有人提到字典樹,我就順便了解下字典樹。 [轉自:http://blog.csdn.net/oncealong/article/details

Trie 字典

字典樹(Trie)可以儲存一些 字串->值 的對應關係。 基本上,它跟 Java 的 HashMap 功能相同,都是 key-value 對映,只不過 Trie 的 key 只能是字串。 它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。

Trie字典,字首,鍵分析詳解

Trie樹概述    Trie樹,又稱字典樹、字首樹、單詞查詢樹、鍵樹,是一種多叉樹形結構,是一種雜湊樹的變種。Trie這個術語來自於retrieval,發音為/tri:/ “tree”,也有人讀為/traɪ/ “try”。Trie樹典型應用是用於快速檢索(最

Trie字典的C++實現

問題描述: Trie樹 又稱單詞查詢樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計,排序和儲存大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。 舉個例子:os,oh,old,char,chat這些關鍵詞構成的trie樹:          

Trie字典、字首面向物件思想C++實現

Trie樹的功能、思想、實現都寫在程式碼註釋中了 使用: Trie trie = new Trie([TypeCase]); trie. TypeCase= Bit //二進位制 Number // 0-9數字 LowerCase //小寫字母

01字典待更新

names sin %d string iostream urn stream acm 數字 01字典樹典型的題就是找出異或值最大的兩個數,其實跟字典樹差不多的,就是從原來的26位字母變成了0和1,插入操作也跟字典樹差不多,查詢的時候有貪心思想,盡量找同位不相同的。 模板

Trie動態規劃

字首查詢的典型應用: http://acm.hdu.edu.cn/showproblem.php?pid=1251 hohicoder1014是相同的模板題 #include<iostream> #include<cstring> using name

資料結構實現 8.1:字典C++版

資料結構實現 8.1:字典樹(C++版) 1. 概念及基本框架 2. 基本操作程式實現 2.1 增加操作 2.2 查詢操作 2.3 其他操作 3. 演算法複雜度分析 3.1 增加操作 3.2 查

Tire字典資料結構詳解圖解及模板

先在這裡放模板,具體圖解回去再發 #include <map> #include <queue> #include <cstdlib> #include <cm

線段by Shine_hale

一. 線段樹是什麼? 線段樹,顧名思義,就是將區間變成線段進行處理如圖可以看出,將1-10這個線段不斷拆分,進而得到子節點; 摘自網際網路 二、為什麼要用線段樹 線段樹修改簡單,方便快捷,同時;在查詢上可以使時間複雜度到達O(1),這很厲害了同時不同於RMQ問題,可以線上進行修改,不用花時間進行重構

zkw線段by Shine_hale

說我 sca can 心理 www. node -- mes 處理 線段樹嘛,很好用的數據結構處理方法但是有個缺點 代碼長,不好理解,但是很強大 其建樹方法是遞歸建樹,調用棧來運行,從上至下,有人說,這類似一個回溯的過程 其實也不然,標記下放後,標記仍需上浮,一上一下,自然