|
| 1 | +/// The Trie class has the following attributes: |
| 2 | +/// -root (the root of the trie) |
| 3 | +/// -wordList (the words that currently exist in the trie) |
| 4 | +/// -wordCount (the number of words in the trie) |
| 5 | +public class Trie { |
| 6 | + private var root = Node(character: "", parent: nil) |
| 7 | + private(set) var wordList: [String] = [] |
| 8 | + private(set) var wordCount = 0 |
| 9 | + |
| 10 | + init(words: Set<String>) { |
| 11 | + words.forEach { insert(word: $0) } |
| 12 | + } |
| 13 | + |
| 14 | + /// Merge two `Trie` objects into one and returns the merged `Trie` |
| 15 | + /// |
| 16 | + /// - parameter other: Another `Trie` |
| 17 | + /// |
| 18 | + /// - returns: The newly unioned `Trie`. |
| 19 | + func merge(other: Trie) -> Trie { |
| 20 | + let newWordList = Set(wordList + other.wordList) |
| 21 | + return Trie(words: newWordList) |
| 22 | + } |
| 23 | + |
| 24 | + /// Looks for a specific key and returns a tuple that has a reference to the node (if found) and true/false depending on if it was found. |
| 25 | + /// |
| 26 | + /// - parameter key: A `String` that you would like to find. |
| 27 | + /// |
| 28 | + /// - returns: A tuple containing the an optional `Node`. If the key is found, it will return a non-nil `Node`. The second part of the tuple contains a bool indicating whether it was found or not. |
| 29 | + func find(key: String) -> (node: Node?, found: Bool) { |
| 30 | + var currentNode = root |
| 31 | + |
| 32 | + for character in key.characters { |
| 33 | + if currentNode.children["\(character)"] == nil { |
| 34 | + return (nil, false) |
| 35 | + } |
| 36 | + currentNode = currentNode.children["\(character)"]! |
| 37 | + } |
| 38 | + return (currentNode, currentNode.isValidWord) |
| 39 | + } |
| 40 | + |
| 41 | + /// Returns true if the `Trie` is empty, false otherwise. |
| 42 | + var isEmpty: Bool { |
| 43 | + return wordCount == 0 |
| 44 | + } |
| 45 | + |
| 46 | + /// Checks if the a word is currently in the `Trie`. |
| 47 | + /// |
| 48 | + /// - parameter word: A `String` you want to check. |
| 49 | + /// |
| 50 | + /// - returns: Returns `true` if the word exists in the `Trie`. False otherwise. |
| 51 | + func contains(word: String) -> Bool { |
| 52 | + return find(key: word.lowercased()).found |
| 53 | + } |
| 54 | + |
| 55 | + func isPrefix(_ prefix: String) -> (node: Node?, found: Bool) { |
| 56 | + let prefix = prefix.lowercased() |
| 57 | + var currentNode = root |
| 58 | + |
| 59 | + for character in prefix.characters { |
| 60 | + if currentNode.children["\(character)"] == nil { |
| 61 | + return (nil, false) |
| 62 | + } |
| 63 | + |
| 64 | + currentNode = currentNode.children["\(character)"]! |
| 65 | + } |
| 66 | + |
| 67 | + if currentNode.children.count > 0 { |
| 68 | + return (currentNode, true) |
| 69 | + } |
| 70 | + |
| 71 | + return (nil, false) |
| 72 | + } |
| 73 | + |
| 74 | + /// Inserts a word into the trie. Returns a tuple containing the word attempted to tbe added, and true/false depending on whether or not the insertion was successful. |
| 75 | + /// |
| 76 | + /// - parameter word: A `String`. |
| 77 | + /// |
| 78 | + /// - returns: A tuple containing the word attempted to be added, and whether it was successful. |
| 79 | + @discardableResult func insert(word: String) -> (word: String, inserted: Bool) { |
| 80 | + let word = word.lowercased() |
| 81 | + guard !contains(word: word), !word.isEmpty else { return (word, false) } |
| 82 | + |
| 83 | + var currentNode = root |
| 84 | + var length = word.characters.count |
| 85 | + |
| 86 | + var index = 0 |
| 87 | + var character = word.characters.first! |
| 88 | + |
| 89 | + while let child = currentNode.children["\(character)"] { |
| 90 | + currentNode = child |
| 91 | + length -= 1 |
| 92 | + index += 1 |
| 93 | + |
| 94 | + if length == 0 { |
| 95 | + currentNode.isAWord = true |
| 96 | + wordList.append(word) |
| 97 | + wordCount += 1 |
| 98 | + return (word, true) |
| 99 | + } |
| 100 | + |
| 101 | + character = Array(word.characters)[index] |
| 102 | + } |
| 103 | + |
| 104 | + let remainingCharacters = String(word.characters.suffix(length)) |
| 105 | + for character in remainingCharacters.characters { |
| 106 | + currentNode.children["\(character)"] = Node(character: "\(character)", parent: currentNode) |
| 107 | + currentNode = currentNode.children["\(character)"]! |
| 108 | + } |
| 109 | + |
| 110 | + currentNode.isAWord = true |
| 111 | + wordList.append(word) |
| 112 | + wordCount += 1 |
| 113 | + return (word, true) |
| 114 | + } |
| 115 | + |
| 116 | + /// Attempts to insert all words from input array. Returns a tuple containing the input array and true if some of the words were successfully added, false if none were added. |
| 117 | + /// |
| 118 | + /// - parameter words: An array of `String` objects. |
| 119 | + /// |
| 120 | + /// - returns: A tuple stating whether all the words were inserted. |
| 121 | + func insert(words: [String]) -> (wordList: [String], inserted: Bool) { |
| 122 | + var successful: Bool = false |
| 123 | + for word in wordList { |
| 124 | + successful = insert(word: word).inserted || successful |
|
741A
125 | + } |
| 126 | + |
| 127 | + return (wordList, successful) |
| 128 | + } |
| 129 | + |
| 130 | + /// Removes the specified key from the `Trie` if it exists, returns tuple containing the word attempted to be removed and true/false if the removal was successful. |
| 131 | + /// |
| 132 | + /// - parameter word: A `String` to be removed. |
| 133 | + /// |
| 134 | + /// - returns: A tuple containing the word to be removed, and a `Bool` indicating whether removal was successful or not. |
| 135 | + @discardableResult func remove(word: String) -> (word: String, removed: Bool) { |
| 136 | + let word = word.lowercased() |
| 137 | + |
| 138 | + guard contains(word: word) else { return (word, false) } |
| 139 | + var currentNode = root |
| 140 | + |
| 141 | + for character in word.characters { |
| 142 | + currentNode = currentNode.getChildAt(character: "\(character)") |
| 143 | + } |
| 144 | + |
| 145 | + if currentNode.children.count > 0 { |
| 146 | + currentNode.isAWord = false |
| 147 | + } else { |
| 148 | + var character = currentNode.character |
| 149 | + while currentNode.children.count == 0 && !currentNode.isRoot { |
| 150 | + currentNode = currentNode.parent! |
| 151 | + currentNode.children[character]!.parent = nil |
| 152 | + character = currentNode.character |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + wordCount -= 1 |
| 157 | + |
| 158 | + var index = 0 |
| 159 | + for item in wordList { |
| 160 | + if item == word { |
| 161 | + wordList.remove(at: index) |
| 162 | + } |
| 163 | + index += 1 |
| 164 | + } |
| 165 | + |
| 166 | + return (word, true) |
| 167 | + } |
| 168 | + |
| 169 | + func find(prefix: String) -> [String] { |
| 170 | + guard isPrefix(prefix).found else { return [] } |
| 171 | + var queue = Queue<Node>() |
| 172 | + let node = isPrefix(prefix).node! |
| 173 | + |
| 174 | + var wordsWithPrefix: [String] = [] |
| 175 | + let word = prefix |
| 176 | + var tmp = "" |
| 177 | + queue.enqueue(element: node) |
| 178 | + |
| 179 | + while let current = queue.dequeue() { |
| 180 | + for (_, child) in current.children { |
| 181 | + if !child.visited { |
| 182 | + queue.enqueue(element: child) |
| 183 | + child.visited = true |
| 184 | + if child.isValidWord { |
| 185 | + var currentNode = child |
| 186 | + while currentNode !== node { |
| 187 | + tmp += currentNode.character |
| 188 | + currentNode = currentNode.parent! |
| 189 | + } |
| 190 | + tmp = "\(tmp.characters.reversed())" |
| 191 | + wordsWithPrefix.append(word + tmp) |
| 192 | + tmp = "" |
| 193 | + } |
| 194 | + } |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + return wordsWithPrefix |
| 199 | + } |
| 200 | + |
| 201 | + func removeAll() { |
| 202 | + for word in wordList { |
| 203 | + remove(word: word) |
| 204 | + } |
| 205 | + } |
| 206 | +} |
| 207 | + |
| 208 | +extension Trie: CustomStringConvertible { |
| 209 | + public var description: String { |
| 210 | + return "" |
| 211 | + } |
| 212 | +} |
0 commit comments