咱們先來看一道面試題:乙個文字檔案,大約有一萬行,每行乙個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。
之前在此文:海量資料處理面試題集錦與bit-map詳解中給出的參***:用trie樹統計每個詞出現的次數,時間複雜度是o(n*le)(le表示單詞的平均長度),然後是找出出現最頻繁的前10個詞。也可以用堆來實現(具體的操作可參考第三章、尋找最小的k個數),時間複雜度是o(n*lg10)。所以總的時間複雜度,是o(n*le)與o(n*lg10)中較大的哪乙個。
本文第一部分,咱們就來了解了解這個trie樹,然後自然而然過渡到第二部分、字尾樹,在此對這兩種樹權作此番闡述,以備不時之需,在需要的時候能手到擒來即可。ok,有任何問題,歡迎不吝指正或賜教。謝謝。
第一部分、trie樹
什麼是trie樹
trie樹,又稱單詞查詢樹或鍵樹,是一種樹形結構,是一種雜湊樹的變種。典型應用是用於統計和排序大量的字串(但不僅限於字串),所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:最大限度地減少無謂的字串比較,查詢效率比雜湊表高。
trie的核心思想是空間換時間。利用字串的公共字首來降低查詢時間的開銷以達到提高效率的目的。
它有3個基本性質:
根節點不包含字元,除根節點外每乙個節點都只包含乙個字元。
從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
每個節點的所有子節點包含的字元都不相同。 舉例
舉個在網上流傳頗廣的例子,如下:
題目:給你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個單詞,我們構建的樹就是如下圖這樣的:
(圖義:當時第一次看到這幅圖的時候,便立馬感到此樹之不凡構造了。單單從上幅圖便可窺知一二,好比大海搜人,立馬就能確定東南西北中的到底哪個方位,如此迅速縮小查詢的範圍和提高查詢的針對性,不失為一創舉)
對於每乙個節點,從根遍歷到他的過程就是乙個單詞,如果這個節點被標記為紅色,就表示這個單詞存在,否則不存在。
那麼,對於乙個單詞,我只要順著他從跟走到對應的節點,再看這個節點是否被標記為紅色就可以知道它是否出現過了。把這個節點標記為紅色,就相當於插入了這個單詞。
這樣一來我們查詢和插入可以一起完成(重點體會這個查詢和插入是如何一起完成的,稍後,下文具體解釋),所用時間僅僅為單詞長度,在這乙個樣例,便是10。
我們可以看到,trie樹每一層的節點數是26^i級別的。所以為了節省空間。我們用動態鍊錶,或者用陣列來模擬動態。空間的花費,不會超過單詞數×單詞長度。
小結ok,下面,咱們再總結一下上述問題:
已知n個由小寫字母構成的平均長度為10的單詞,判斷其中是否存在某個串為另乙個串的字首子串。下面對比3種方法:
最容易想到的:即從字串集中從頭往後搜,看每個字串是否為字串集中某個字串的字首,複雜度為o(n^2)。
使用hash:我們用hash存下所有字串的所有的字首子串,建立存有子串hash的複雜度為o(n*len),而查詢的複雜度為o(n)* o(1)= o(n)。
使用trie:因為當查詢如字串abc是否為某個字串的字首時,顯然以b,c,d....等不是以a開頭的字串就不用查詢了。所以建立trie的複雜度為o(n*len),而建立+查詢在trie中是可以同時執行的,建立的過程也就可以成為查詢的過程,hash就不能實現這個功能。所以總的複雜度為o(n*len),實際查詢的複雜度也只是o(len)。
解釋下上述方法3中所說的為什麼hash不能將建立與查詢同時執行,而trie樹卻可以:
至於,有關trie樹的查詢,插入等操作的實現**,網上遍地開花且千篇一律,諸君盡可參考,想必不用我再做多餘費神。
查詢trie樹是簡單但實用的資料結構,通常用於實現字典查詢。我們做即時響應使用者輸入的ajax搜尋框時,就是trie開始。本質上,trie是一顆儲存多個字串的樹。相鄰節點間的邊代表乙個字元,這樣樹的每條分支代表一則子串,而樹的葉節點則代表完整的字串。和普通樹不同的地方是,相同的字串字首共享同一條分支。下面,再舉乙個例子。給出一組單詞,inn, int, at, age, adv, ant, 我們可以得到下面的trie:
可以看出:
查詢操縱非常簡單。比如要查詢int,順著路徑i -> in -> int就找到了。
搭建trie的基本演算法也很簡單,無非是逐一把每則單詞的每個字母插入trie。插入前先看字首是否存在。如果存在,就共享,否則建立對應的節點和邊。比如要插入單詞add,就有下面幾步:
考察字首"a",發現邊a已經存在。於是順著邊a走到節點a。
考察剩下的字串"dd"的字首"d",發現從節點a出發,已經有邊d存在。於是順著邊d走到節點ad
考察最後乙個字元"d",這下從節點ad出發沒有邊d了,於是建立節點ad的子節點add,並把邊ad->add標記為d。 應用
除了本文引言處所述的問題能應用trie樹解決之外,trie樹還能解決下述問題(節選自此文:海量資料處理面試題集錦與bit-map詳解):
有了trie,字尾樹就容易理解了。本文接下來的第二部分,介紹字尾樹。
第二部分、字尾樹
先說說字尾的定義,顧名思義,通俗點來說,就是所謂字尾就是後面尾巴的意思。比如說給定一長度為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樂。
那字尾樹同最長回文有什麼關係呢?我們得先知道兩坨坨簡單概念:
最低共有祖先,lca(lowest common ancestor),也就是任意兩節點(多個也行)最長的共有字首。比如下圖中,節點7同節點10的共同祖先是節點1與借點,但最低共同祖先是5。 查詢lca的演算法是o(1)的複雜度,這年頭少見。代價是需要對字尾樹做複雜度為o(n)的預處理。
廣 義字尾樹(generalized suffix tree)。傳統的字尾樹處理一坨單詞的所有字尾。廣義字尾樹儲存任意多個單詞的所有字尾。例如下圖是單詞xmadamyx與xymadamx的廣義字尾 樹。注意我們需要區分不同單詞的字尾,所以葉節點用不同的特殊符號與字尾位置配對。
從Trie樹(字典樹)談到字尾樹
從trie 樹 字典樹 談到字尾樹 0 引言 咱們先來看一道面試題 乙個文字檔案,大約有一萬行,每行乙個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。之前在此文 海量資料處理面試題集錦與bit map 詳解中給出的參 用trie樹統計每個詞出現的次數,時間複雜度是o n ...
Trie樹(字典樹)
trie樹的核心思想是用空間換時間,通過在樹中儲存字串的公共字首,來達到加速檢索的目的。例如,對於一棵儲存由英文本母組成的字串的trie樹,如下圖 trie樹在實現的時候,可以用左兒子右兄弟的表示方法,也可以在每個節點處開設乙個陣列,如上圖的方法。trie樹的主要操作是插入 查詢,也可以進行刪除。插...
字典樹 Trie樹
字典樹 trie樹 顧名思義是一種樹形結構,屬於雜湊樹的一種。應用於統計 排序 查詢單詞 統計單詞出現的頻率等。它的優點是 利用字串的公共字首來節約儲存空間,最大限度地減少無謂的字串比較,查詢效率比雜湊表高。字典樹的結構特點 根節點不代表任何字元。其他節點從當前節點回溯到根節點可以得到它代表的字串。...