Trie樹的構建和應用
Trie樹又叫“字典樹”,是一種在字串計算中極為常見的資料結構。在介紹Trie樹的具體結構之前,我們首先要搞明白的就是Trie樹究竟是用來解決哪一類問題的,為什麼這類問題可以用Trie樹高效的解決。
我們為什麼用Trie樹
1. 節約字串的儲存空間
假設現在我們需要對海量字串構建字典。所謂字典就是一個集合,這個集合包含了所有不重複的字串,字典在對文字資料做資訊檢索系統時的作用我想毋庸贅述了。那麼現在就出現了一個問題,那就是字典對儲存空間的消耗過大。而當這些字串中存在大量的串擁有重複的字首時,這種消耗就顯得過於浪費了。比如:
2. 字串檢索
檢索一個字串是否屬於某個詞典時,我們當前一般有兩種思路:
- 線性遍歷詞典,計算複雜度
O(n) ,n 為詞典長度; - 利用hash表,預先處理字串集合。這樣再搜尋運算時,計算複雜度
O(1) 。但是hash計算可能存在碰撞問題,一般的解決辦法比如對某個hash值所代表的字串實施二次檢索,則計算時間也會上來。而且,hash雖說是一種高效演算法,其計算效率比直接字元匹配還是要略高的。
所以,能不能設計一種高效的資料結構幫助解決字串檢索的問題?
3. 字串公共字首問題
這裡有兩個非常典型的例子:
- 求取已知的
n 個字串的最長公共字首,樸素方法的時間複雜度為O(nt) ,t 為最長公共字首的長度; - 給定字串
a ,求取a 在某n 個字串中和哪些串擁有公共字首
對於問題(2),除了樸素的比較法之外,我們還可以採取對每個字串的所有字首計算hash值的方法,這樣一來,計算所有字首hash值複雜度
Trie樹的構造
1. 結構
Trie樹是如圖所示的一棵多叉樹。其中儲存的字串集合為:
從上圖我們可以看出,Trie樹有如下3點特徵:
- 根節點不代表字元,除根節點外每一個節點都只代表一個字元(一般的解釋是,是除根節點外所有節點只“包含”一個字元,我在這裡說“代表”,而不說“包含”是因為後面的演算法設計中,為了使Trie樹的結構更加清晰,我並沒有讓任何節點“包含”字元)。
- 從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串。
- 每個節點的所有子節點包含的字元都不相同。
其實,一棵完整的Trie樹應該每個非葉節點都擁有26個指標,正好對應著英文的26個字母,這樣整棵樹的空間成本為
除此之外,由於有些字元就是集合中其他字元的字首,為了能夠分辨清楚集合中到底有哪些字串,我們還需要為每個節點賦予一個判斷終止與否的bool值,記為end
值為True
,表示從根節點到end == True
的節點標紅。
2. 構建Trie樹
理解了上面Trie樹的結構,就可以放手去寫程式碼了,實現起來其實非常簡單,幾乎沒有任何難度,需要注意的是我們究竟以一種什麼樣的形式來定義節點。這一點其實每個人的想法還是有些區別的,我是這般定義:
class TrieTreeNode(object):
def __init__(self):
self.end = False
# The labels of pointers in the node
self.pointerLabels = []
# The pointers
self.pointers = []
除了end
之外,還有兩個list型的變數,pointerLabels
和pointers
,前者表示的是此節點的所有指標的標籤,標籤表示其實才是字元,如上圖每個指標上面的字元,而pointers
代表的是此節點的每個孩子節點的地址。這樣設計的好處在於,查詢時我們能夠直接根據當前節點包含的資料判斷一下個字元否存在,該往那條路徑繼續遍歷,而不是依次訪問當前節點的所有孩子。而除了根節點之外的所有節點都“代表”著被指向的指標的標籤。拿上圖的節點
root.end = True
root.pointers = [u4, u5]
root.pointerLabels = ["b", "c"]
下面給出完整的構建Trie樹的程式碼:
def buildTrieTree(stringList):
"""
:param stringList: the collection of strings
:return:
"""
root = TrieTreeNode()
for ele in stringList:
cur = root
for char in ele:
if char not in cur.pointerlabels:
cur.pointerLabels.append(char)
newNode = TrieTreeNode()
cur.pointers.append(newNode)
if char != ele[-1]:
cur = newNode
# When char in cur.pointerlabels
elif char != ele[-1]:
pos = cur.pointerlabels.index(char)
cur = cur.pointers[pos]
else:
cur.end = True
return root
3. 查詢Trie樹
給出在Trie樹上查詢某個字串是否存在的程式碼,這個非常簡單了,不多說了。
def trieTreeQuery(inputString, trieTreeRoot):
"""
:param inputString: the string that need to be searched
:param trieTreeRoot: the root of Trie tree
:return:
"""
cur = trieTreeRoot
for char in inputString:
if char not in cur.pointerlabels:
return False
else:
pos = cur.pointerlabels.index(char)
cur = cur.pointers[pos]
if cur.end is True:
return True
return False
效能分析
從上面字串檢索的演算法我們可以分析出,無論有多少字串,我們檢索一個字串的時間為