資料結構基礎--字首樹&&字尾樹

kirito_song發表於2018-12-11

資料結構基礎--字首樹&&字尾樹

本文主要作為自己的學習筆記,並不具備過多的指導意義。

字首樹

何為字首樹

字首樹又名字典樹,單詞查詢樹,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
        }
    }
}
複製程式碼

字首樹的應用

  1. 字串的快速檢索 字首樹的查詢時間複雜度是O(L),L是字串的長度。所以效率還是比較高的。字典樹的效率比hash表高。
  2. 字串排序 多叉樹本身已經是有序的,只要按照每層都先遍歷低位節點的方式即可。

字尾樹

相對於字首樹來說,字尾樹並不是針對大量字串的,而是針對一個或幾個字串來解決問題。比如字串的迴文子串,兩個字串的最長公共子串等等。

比如blanana這個單詞,在字尾樹的結構中將以如下結構展現(為了方便看到字尾,我沒有合併相同的字首)

資料結構基礎--字首樹&&字尾樹

字尾樹的應用

  1. 查詢字串an是否在banana中 如上圖所致,a+n的節點順序已經存在於字尾樹中,所以返回true
  2. 查詢字串a+nbanana中重複的次數。 如上圖所致,a+nn節點的path計數為2,所以重複次數為2.

字首樹和字尾樹

相關文章