好久沒更新Blog了,真是一畢業就克制不住自己的懶惰,以后要做個不僅高產,而且有優質文章的“農民”。加油啦,新起點就嘗試翻譯以下Swift Algorithm Club的文章,算法很重要,Swift是一門很優雅的語言,要跟上節奏,夯實基礎,學習新知:joy:,不扯皮了。
翻譯自: https://www.raywenderlich.com/139410/swift-algorithm-club-swift-trie-data-structure
開始
字符存儲在樹的每個節點的 n-元(n-ary)樹稱作單詞查找樹。單詞查詢樹是一種可以促進英文高效地前綴匹配的鍵值結構。
SwiftAlgClub_TrieData-trie-1.png
上述例子:“Cat”,“Cut”,“Cute”,“To”,“B”
為什么使用單詞查找樹
單詞查找樹對某些情況非常實用。除了非常好地存儲英文,它同樣可以作為‘哈希表’的替代, 它有以下幾個優勢:
- 查找值時通常有一個更好的最壞的時間復雜度
- 不像哈希表,單詞查找樹不用擔心鍵沖突問題
- 不需要哈希算法保證每個元素有一個獨一無二的路徑
- 單詞樹可以按照字母順序排序
實現
你的實現將由一個TrieNode和一個Trie類構成,每個TrieNode代表單詞 一個字符。例如,“cute”將有以下的節點構成:c-gt;u-gt;t-gt;e。Trie將管理節點的插入邏輯并且一直關聯著這些節點
我們首先實現簡單的TrieNode類
TrieNode
class TrieNodelt;T: Hashablegt; { var value: T? weak var parent: TrieNode? var children: [T: TrieNode] = [:] init(value: T? = nil, parent: TrieNode? = nil) { self.value = http://www.tuicool.com/articles/value self.parent = parent } }
上面是TrieNode類的一般實現。它存儲著一個值(對于英文而言就是字符)并且有一個父節點和孩子們子節點的引用。這里有以下幾點需要注意:
-
class TrieNodelt;T:Hashablegt; ,泛型類型,這說明TrieNode適用于任何遵循Hashable協議的類型
-
parent節點使用weak,防止出現循環引用問題,因為對于某個節點而言它的父節點中的children數組肯定包含該節點, 如果這里在使用默認的strong引用父節點就出現了循環引用。某個節點包含父類的引用是必須的,對于移除操作remove會使用的到
- TrieNode必須遵循Hashable協議。因為你將會使用這個值作為children字典的key,在Swift中只要能作為能作為字典的key就必需遵循Hashable協議
接下來在TrieNode中添加add操作
func add(child: T) { // 1. 確保插入的“字符”子節點在當前子節點數組中不存在 guard children[child] == nil else { return } // 2. 創建一個新節點將傳入的值插入 children[child] = TrieNode(value: child, parent: self) }
Trie
Trie類的職責是管理節點。
class Trie { fileprive let root: TrieNodelt;Charactergt; init() { root = TrieNodelt;Charactergt;() } }
以上就是Trie類的基礎,聲明一個root屬性去關聯你的 ”字典樹“ 的根節點。因為我們要實現一個特定于英文的“字典樹”所以我們使用的節點類型是Character,它是Hashable的子協議,初始化方法實現一個簡單的初始化實現一個空節點
Typealiasing
再繼續往下之前將Trie類做如下更新:
class Trie { typealias Node = TrieNodelt;Charactergt; fileprivate let root: Node init() { root = TrieNodelt;Charactergt;() } }
上面添加了一個Node typealias, 它允許你使用Node去替代TrieNode,除了能夠縮短語法,他還可以使程序變得更加“健壯”;
Insertion
Trie類管理者“單詞樹”Trie的操作,當實現insertion方法時,記住Trie是高效的,因為它總是使用已經存在的節點去完成一個排序。例如有兩個單詞“Cut”,“Cute”,僅需要使用4個節點,因為“Cut”是“Cute”的前綴
extension Trie { func insert(word: String) { // 如果word是個空字符串,直接返回 guard !word.isEmpty else { return } // 將從根節點開始執行迭代 var currentNode = root // 每個單詞在Trie中都表現為一連串的字符節點 // 例如cute:c-gt;u-gt;t-gt;e // 因為你要一個一個地插入字符,將單詞裝換成一個字符數組能更方便地保留字符的插入的軌跡 let charcters = Array(word.lowercased().characters) var currentIndex = 0 //TODO: while currentIndex lt; charcters.count { // 得到當前將要插入的字符 let character = charcters[currentIndex] // 判斷該字符是否在當前節點的“孩子們”子節點數組中是否存在 if let child = currentNode.children[character] { currentNode = child // 如果存在,只需簡單地將當前節點移動到這個存在的節點即可開始下一個字符的插入操作 } else { // 如果不存在, 那就添加個,并移動當前節點的指針到新創建的節點上 currentNode.add(child: character) currentNode = currentNode.children[character]! } currentIndex = 1 } } }
SwiftAlgClub_TrieData-trie-2.png
Teminating Nodes
到現在為止,insert方法可以很好到執行插入操作。但是這里會有個問題,例如如果你插入的是“cute”,該這樣確定“cut”已經存在呢??
如果沒有一些分類的指示器好像很難去確定。回到TrieNode,添加如下屬性
var isTerminating = false // 負責指示一個單詞是否結束
回到“cute”那個例子,如果插入“cute”,isTerminating是這樣標識的
SwiftAlgClub_TrieData-trie-3.png
小黑點標識cute的結束;接下來如果插入“cut”就會將“t”標識為結束點
SwiftAlgClub_TrieData-trie-4.png
在insert的while中添加如下代碼
// 如果currentIndex等于單詞字符的數量,就說明已經到達單詞的結尾,可以將結束標識置為true if currentIndex == characters.count { currentNode.isTerminating = true }
Contains
接下來處理contains函數,這個方法的職責是檢查單詞是否存在
func contains(word: String) -gt; Bool { guard !word.isEmpty else { return false } var currentNode = root let characters = Array(word.lowercased().characters) var currentIndex = 0 // 這里將嘗試遍歷基于傳遞的word字符的節點 // 1. 創建一個while循環, 有兩個條件 // (1). 沒有到達單詞的結尾 // (2). 存在字符對應的節點 while currentIndex lt; characters.count, let child = currentNode.children[characters[currentIndex]] { // 當while成功執行結束時,currentNode將會指向最終的節點, currentIndex并且是字符串的長度 currentIndex = 1 currentNode = child } // 成功遍歷完所有的字符,并且該結束節點是某個字符串的結尾,則返回true, 否則返回false if currentIndex == characters.count amp;amp; currentNode.isTerminating { return true } else { return false } }
至此Trie完成
Tags: Swift 算法
文章來源:http://www.jianshu.com/p/3be5cbb4cf9b