A trie for handling strings for an autocomplete dictionary. This passes my fairly casual tests, though it's always possible that there are broken edge cases, but I'm mainly concerned about design and efficiency: is this a sensible way to implement this data structure? (In particular, is it sensible to do the insert method recursively?). It feels a little gnarly to me.
struct Trie {
var root: Node
init() {
root = Node()
}
func search(_ word: String) -> Bool {
let letters = Array(word)
var curnode = root
for letter in letters {
guard let match = curnode.children.first(where: {(key, _) in
key == letter
})
else {
return false
}
curnode = match.value
}
if curnode.contained {
return true
}
return false
}
func remove(_ word: String) {
let letters = Array(word)
var curnode = root
for letter in letters {
if !curnode.children.contains(where: {(key, _) in
key == letter
}) {
break
} else {
curnode = curnode.children[letter]!
}
}
curnode.contained = false
}
func insert(_ letters: [Character], parent: Node) -> Node {
if letters.count == 1 {
let letter = letters[0]
if parent.children.contains(where: {(key, _) in
key == letter
}) {
let newNode = parent
newNode.children[letter]!.contained = true
return newNode
} else {
let newNode = Node(letter, final: true)
return newNode
}
} else {
let first = letters[0]
let rest = Array(letters.dropFirst())
if let subtree = parent.children.first(where: {(key, _) in
key == first
}) {
let newNode = Node(first, final: subtree.value.contained, kids: subtree.value.children)
newNode.children[rest[0]] = insert(rest, parent: newNode)
return newNode
} else {
let newNode = Node(first, final: false)
newNode.children[rest[0]] = insert(rest, parent: newNode)
return newNode
}
}
}
mutating func insert(_ word: String) {
let new_subtree = insert(Array(word), parent: root)
root.children[new_subtree.char!] = new_subtree
}
}
class Node {
var char: Character?
var children: [Character:Node]
var contained: Bool
init() {
char = nil
children = [:]
contained = false
}
init(_ c: Character, final: Bool) {
children = [:]
contained = final
char = c
}
init(_ c: Character, final: Bool, kids: [Character:Node]) {
children = kids
contained = final
char = c
}
}
Clarification edit: this isn't meant to be production ready, is only meant to be a concise and straightforward implementation of the data structure. So I intentionally left off checks for problem input; it should only be able to handle sensible inputs right now, where "sensible" means "input that someone using the data structure correctly to add, look for, and remove data would intentionally provide." So, not empty strings, removing stuff that isn't there, inserting the same thing twice, etc. etc.
The casual test cases I used, meant to represent sensible behavior, are:
var testTrie = Trie()
testTrie.insert("cat")
testTrie.search("cat") // T
testTrie.search("car") // F
testTrie.insert("car")
testTrie.search("car") // T
testTrie.search("cat") // T
testTrie.search("ca") // F
testTrie.search("cad") // F
testTrie.search("carburetor") // F
testTrie.insert("carburetor")
testTrie.search("carburetor") // T
testTrie.search("car") // T
testTrie.search("cat") // T
testTrie.search("ca") // F
testTrie.remove("car")
testTrie.search("carburetor") // T
testTrie.search("car") // F
testTrie.search("cat") // T
contained
is not as familiar, to me, asisFinal
,isTerminating
orisLeaf
for Trie implementations. According to the API Design Guidelines: "Uses of Boolean methods and properties should read as assertions about the receive". Alternative implementation here. \$\endgroup\$