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
性能分析
從上面字符串檢索的算法我們可以分析出,無論有多少字符串,我們檢索一個字符串的時間爲