本文主要作為自己的學習筆記,並不具備過多的指導意義。
字首樹
何為字首樹
字首樹又名字典樹,單詞查詢樹,Trie樹,是一種多路樹形結構,是雜湊樹的變種,和hash效率有一拼,是一種用於快速檢索的多叉樹結構。多用於詞頻搜尋或者模糊查詢。
查詢時只與單樣本長度有關,而與樣本量無關。
舉例:
給出一組單詞,inn, int, at, age, adv,ant, 我們可以得到下面的Trie:
如此,在進行依次輸入進行查詢時。只需要順著之前的樹繼續查詢即可,而不需要每次修改字串都遍歷所有資訊。
在刪除了字元時,也只需要回滾到上層即可。
基本程式碼結構
-
樹節點
一個多叉樹結構的節點。字首樹的功能,取決於節點的健壯性。
//字首樹節點
public class TrieNode {
public var end :Bool //是否為某個單詞最後節點,查詢用
public var path :Int //記錄有幾個字串經過了這個節點,刪除用
public var nexts:Dictionary<String, TrieNode> //子樹路徑,基本功能
public init() {
self.end = false
self.nexts = Dictionary.init()
self.path = 0
}
}
複製程式碼
其中子樹路徑,如果只儲存字串的話。當樣本量太大可以乾脆使用陣列[26]
的形式儲存。
-
字首樹結構
持有一個根節點,具備增刪改查的功能
//字首樹
public class Trie {
private var root:TrieNode
public init() {
self.root = TrieNode.init()
}
/// 新增字串
public func insert(word:String) {
...
}
//查詢
public func search(word:String) -> Bool {
...
}
//刪除
public func delete(word:String) {
...
}
}
複製程式碼
-
插入資料
將字串按字母分割,從根節點依次追加進字首樹
需要注意如果樹中已經存在該節點路徑,則複用
/// 新增字串
public func insert(word:String) {
if word == nil {
return
}
let strs = wordToArr(word: word)
if strs.count == 0 {
return
}
//從根節點開始建立字首樹
var node = root
for i in 0..<strs.count {
if node.nexts[strs[i]] == nil {
node.nexts.updateValue(TrieNode.init(), forKey: strs[i]) //沒有可服用路徑,新建節點
}
node = node.nexts[strs[i]]! //node跳到下層
node.path+=1 // 將當前節點計數+1
}
node.end = true //標記最後一個節點
}
複製程式碼
-
查詢資料
查詢與插入類似,但需要判斷最後一個節點的end屬性是否被標記
//查詢
public func search(word:String) -> Bool {
if word == nil {
return false
}
let strs = wordToArr(word: word)
if strs.count == 0 {
return false
}
//從根節點開始建立字首樹
var node = root
for i in 0..<strs.count {
if node.nexts[strs[i]] == nil {
return false //任何一步有空,說明未被插入過
}
node = node.nexts[strs[i]]!
}
return node.end //返回最後一個節點的標記狀態
}
複製程式碼
-
刪除資料
先類似查詢的方式確定是否存在該資料,然後嘗試刪除。
每次刪除,其實是將節點的path屬性-1。當path屬性==0時,從上級節點的nexts列表中remove掉該節點即可return結束。
//刪除
public func delete(word:String) {
if word == nil {
return
}
let strs = wordToArr(word: word)
if strs.count == 0 {
return
}
//嘗試查詢指定字串,並且儲存在nodes備用
var node = root
var nodes : [TrieNode] = Array.init()
nodes.append(node)
for i in 0..<strs.count {
if node.nexts[strs[i]] == nil {
return
}
node = node.nexts[strs[i]]!
nodes.append(node) //將查詢到的節點放入陣列備用
}
//如果最後一個節點不是end,說明不存在該字串
if node.end == true{
//遍歷nodes陣列,當某個節點path==1說明可以直接刪除該節點了
for i in 1..<nodes.count { //從1開始,0是root節點
if nodes[i].path == 1 {
nodes[i-1].nexts.removeValue(forKey: strs[i-1])
return
}
nodes[i].path -= 1
}
}
}
複製程式碼
字首樹的應用
- 字串的快速檢索 字首樹的查詢時間複雜度是O(L),L是字串的長度。所以效率還是比較高的。字典樹的效率比hash表高。
- 字串排序 多叉樹本身已經是有序的,只要按照每層都先遍歷低位節點的方式即可。
字尾樹
相對於字首樹來說,字尾樹並不是針對大量字串的,而是針對一個或幾個字串來解決問題。比如字串的迴文子串,兩個字串的最長公共子串等等。
比如blanana
這個單詞,在字尾樹的結構中將以如下結構展現(為了方便看到字尾,我沒有合併相同的字首)
字尾樹的應用
- 查詢字串
an
是否在banana
中 如上圖所致,a
+n
的節點順序已經存在於字尾樹中,所以返回true
- 查詢字串
a
+n
在banana
中重複的次數。 如上圖所致,a
+n
中n
節點的path
計數為2,所以重複次數為2.