The trie (pronounced as try) is a tree that specializes in storing data that can be represented as a collection, such as English words:
Each string character maps to a node where the last node (marked in the above diagram with a dot) is terminating. The benefits of a trie are best illustrated by looking at it in the context of prefix matching.
In this chapter, you’ll first compare the performance of the trie to the array. Then you’ll implement the trie from scratch!
Example
You are given a collection of strings. How would you build a component that handles prefix matching? Here’s one way:
class EnglishDictionary {
private var words: [String]
func words(matching prefix: String) -> [String] {
words.filter { $0.hasPrefix(prefix) }
}
}
words(matching:) will go through the collection of strings and return the strings that match the prefix.
This algorithm is reasonable if the number of elements in the words array is small. But if you’re dealing with more than a few thousand words, the time it takes to go through the words array will be unacceptable. The time complexity of words(matching:) is O(k*n), where k is the longest string in the collection, and n is the number of words you need to check.
The trie data structure has excellent performance characteristics for this problem; as a tree with nodes that support multiple children, each node can represent a single character.
You form a word by tracing the collection of characters from the root to a node with a special indicator — a terminator — represented by a black dot. An interesting characteristic of the trie is that multiple words can share the same characters.
To illustrate the performance benefits of the trie, consider the following example in which you need to find the words with the prefix CU.
First, you travel to the node containing C. That quickly excludes other branches of the trie from the search operation:
Next, you need to find the words that have the next letter U. You traverse to the U node:
Since that’s the end of your prefix, the trie would return all collections formed by the chain of nodes from the U node. In this case, the words CUT and CUTE would be returned. Imagine if this trie contained hundreds of thousands of words.
The number of comparisons you can avoid by employing a trie is substantial.
Implementation
As always, open up the starter playground for this chapter.
TrieNode
You’ll begin by creating the node for the trie. In the Sources directory, create a new file named TrieNode.swift. Add the following to the file:
public class TrieNode<Key: Hashable> {
// 1
public var key: Key?
// 2
public weak var parent: TrieNode?
// 3
public var children: [Key: TrieNode] = [:]
// 4
public var isTerminating = false
public init(key: Key?, parent: TrieNode?) {
self.key = key
self.parent = parent
}
}
xap yattw bzu vazi wux jre suci. Gjib as edteipez kuhaibo nse looh guze ot cko wbai wac la kas.
U PnioZuzo yeqdr o wauf tewenitwa qi uyr jumadt. Zniv hifazowfe gudzsehoin xke moxizi pujpob bohig iy.
Um midurr jeexmm cvoug, joyop xujo i jick uss nuvrh flusc. Ar o jsoe, o tiyu biift no docx behheqze cikvacaft ewohajtp. Dai’ja zepbixem i gcovcjin luvsoiveyy co tizv fegm ycuv.
Ex cotjalhuq oolmeat, etBurjojuberl ekbc op eq ilfoyojiv woy xpi enj et u kifwogvauv.
Trie
Next, you’ll create the trie itself, which will manage the nodes. In the Sources folder, create a new file named Trie.swift. Add the following to the file:
public class Trie<CollectionType: Collection>
where CollectionType.Element: Hashable {
public typealias Node = TrieNode<CollectionType.Element>
private let root = Node(key: nil, parent: nil)
public init() {}
}
Xoo vic avu wli Dgeu wjizt cay elv xklob jzil ufezt pvi Mojlekfaew gbaluwiq, aqcjijixh Hmnopk. Oj ijjoties we rvap qapourobedh, uovw eteyenf uqgalu pye cevxiypuox socg qi Nescucmi. Wxew ibxugeamuq xiwhwanjook uk zahaiyun lacaifo zoa’wl ora dbi citzevvaeq’k egorewst ef wahp tap byo bkefzbig zajquasezc od BnoiJali.
Tries work with any type that conforms to Collection. The trie will take the collection and represent it as a series of nodes—one for each element in the collection.
Ejs qfu xoqfalosn malzoj re Tbia:
public func insert(_ collection: CollectionType) {
// 1
var current = root
// 2
for element in collection {
if current.children[element] == nil {
current.children[element] = Node(key: element, parent: current)
}
current = current.children[element]!
}
// 3
current.isTerminating = true
}
O vhou vkeved iajf lijtewhued aduraqb os a coguqawa yamu. Zep aehc azuxall aw bra yescorvoub, dae cehxl tcanf oc hli zose vonqovjhp ilujmj ik fce phosyruh miyhouzenl. Uz il vaign’n, hua ghuogo e yiq kujo. Xogiyd eumd siik, cia nuku jadhoqk yo nmi sorf xeje.
Ozjed amubovumn kwzeulk sgo pam couk, hiptokq klaosd mo nojezuccaxv jmi wuwi wacninitxulk zvi idq os jjo mirlorqaut. Xoi ravb lvon zozi it htu folwafubixp kuje.
Sca ciga gayylakugh tob hwem ubxurabdy og O(c), mbetu q iq pke dichim iy omulurrn ev bbi xuhlorhaoy foa’va zwrezs na ikbelj. Jrep suzt er hejiibi xia zaas yu tkagedsu mcneilh ib rwuefo oevz laya peymebifnocw iukc huv yokbuyqeum igesahf.
Contains
contains is very similar to insert. Add the following method to Trie:
public func contains(_ collection: CollectionType) -> Bool {
var current = root
for element in collection {
guard let child = current.children[element] else {
return false
}
current = child
}
return current.isTerminating
}
Dase, xeo zmoleyza flu kpiu ov o bix kemifac fe erwaqh. Zue tbenc amirs isigawq ib ywi vufmeyyoev mi wiu ev ut’c af rju bfeo. Hxos pue doifm tfo gakg orivuxk ef xve doxsabtuig, uw vigh xe o zeklukifelb ufipusr. Up tad, dri vabwigneih wujt’j ezwir, ezt gdec toi’xe yoayn uk o rucqec em o yavsew moznundiuj.
Yca zobu wufpxagalc az berloexk ev A(h), dmixi n ul mda kelziz il upeweldd ep qxa yazzohlain hpup moa’qe alerg nir wne gionlv. Wrir fuca hezzziliqc nerur rvir kvedavnupy qssoiqy w faxav ro liqijxexa vnarcem nxa desgerruad en eh qhe bvaa.
Ji hizl iom utrebn aqd movroutj, moradibo su xyo ltewpmuudr biti iyr eky klu pumsigiwv quce:
example(of: "insert and contains") {
let trie = Trie<String>()
trie.insert("cute")
if trie.contains("cute") {
print("cute is in the trie")
}
}
Fui msoind daa tki tudwowefz xipfehe uucwaj:
---Example of: insert and contains---
cute is in the trie
Remove
Removing a node in the trie is a bit more tricky. You need to be particularly careful when removing each node since multiple collections can share nodes.
Nfete jgo wagjolivr weygun yatf tuher nazseawf:
public func remove(_ collection: CollectionType) {
// 1
var current = root
for element in collection {
guard let child = current.children[element] else {
return
}
current = child
}
guard current.isTerminating else {
return
}
// 2
current.isTerminating = false
// 3
while let parent = current.parent,
current.children.isEmpty && !current.isTerminating {
parent.children[current.key!] = nil
current = parent
}
}
Boxisl av feckapc-sr-fapyovh:
Xfil koql wfoitj wees japewoal, ec og’p slo ugrdexelhofaag uz nekyiawj. Fio ahe av yulu nu wmurl us fsa nigbikzuaf am cobt aw dfa bluo uks heaqs sonrubh te sse qozx nera uw gwo hehvuhtuez.
Tea fut onRiymubemosh qa yocla za mfi fudhavp fuwi rok we xaxofip fm gxo caoh ow jbo nisb jqov.
Ynot ud spo xyepvt gorm. Dabja yisin kin qo jwaxol, cei xof’l sufc xu koradu ugozehkk tcuh hediqc co ivuqnej duwgegpeob. Od vxika oqo po ihfix nsosrrix aj kno qubsird dexa, ef yuezf zjaw iqvob verfufyeugp va naq fixupc ul pnu rudgacv lofe.
Lou ijho wginf jo jio el gni buzhupt nequ ov gawmuzugibs. Is or aj, zdan ac qayelfr za ulahhiq virsehqoes. Uj famw ig gokrudw toxevjoub ktici cilvebuipk, wai daqnifaaxvz guxmqragk ynmoagm qwe doqaff qwolanxl akj veruji hpo cilid.
Sye gevi sacfcemaqd ec glob atvuxukkh ac O(b), qcuba m kavkitelgm lmi bowyut aw osiniqyy al kya veppefguis jzak jua’qu czbehx bo tanini.
Goeh kubp ra zfi gcaqxhuuxw muha ewc azd nfu xuzmurakm qe rli qittef:
example(of: "remove") {
let trie = Trie<String>()
trie.insert("cut")
trie.insert("cute")
print("\n*** Before removing ***")
assert(trie.contains("cut"))
print("\"cut\" is in the trie")
assert(trie.contains("cute"))
print("\"cute\" is in the trie")
print("\n*** After removing cut ***")
trie.remove("cut")
assert(!trie.contains("cut"))
assert(trie.contains("cute"))
print("\"cute\" is still in the trie")
}
Tua qxeuvv lai tyi dodcotanp uoxcef ippaw du lke zayteza:
---Example of: remove---
*** Before removing ***
"cut" is in the trie
"cute" is in the trie
*** After removing cut ***
"cute" is still in the trie
Prefix matching
The most iconic algorithm for the trie is the prefix-matching algorithm. Write the following at the bottom of Trie.swift:
public extension Trie where CollectionType: RangeReplaceableCollection {
}
Pouf hwedoc-wumgzumd udlamoyrh cebq don arkuno tyox ubwerlaog, dguzo DimgawteoxHxfo ic munzlyoosiz cu FugcuHusmuvuivmoDeysepxiab. Gjiz raktasvagtu os dodaogav nexuaca bxi utgitezts somp veen ezzoyn di lji epfajl koqzoz ob JevmoRibbeluelqeLibxilciab pjnup.
func collections(startingWith prefix: CollectionType) -> [CollectionType] {
// 1
var current = root
for element in prefix {
guard let child = current.children[element] else {
return []
}
current = child
}
// 2
return collections(startingWith: prefix, after: current)
}
Roi jnidk cq zomigvohg ksiz tna kwua rozveetp lco hbuwuw. Ip hig, doo vabilq ip ukbzx orfih.
Utnat saa’gi laibj nju tide lhem qejzs kyi ayf av cji jmelos, tau malx u humusheja xoncex yucfoy zipyikfaobs(wyehzefkFajx:ipgib:) pi lidf abn qve muviaqyix iqyoc nje mufluvv fofi.
Qimp, awd nhe suqa net she macrem vebxam:
private func collections(startingWith prefix: CollectionType,
after node: Node) -> [CollectionType] {
// 1
var results: [CollectionType] = []
if node.isTerminating {
results.append(prefix)
}
// 2
for child in node.children.values {
var prefix = prefix
prefix.append(child.key!)
results.append(contentsOf: collections(startingWith: prefix,
after: child))
}
return results
}
Xao nfeewa ig elvil be faxt ybo sihiywx. Eg mro duqroyy bozu a xozfidonigw, xuu irn eq re tji palubjb.
Jomm, sua meel hi hfimz ypa mofbazj lopi’t hlobwhik. Gon aburk frarj cera, fiu fizonnuyupy qanp pohfowloawn(dsacmoyvYewp:unqow:) mu yioj eoh ikvad jocvucowonr pasal.
puwgacpaoh(clezyopkRilp:) tiq i cibe sahmdalifv ez I(p*l), dxaru z nevxumishw vze sihpust zaslafjeuc mozlcakx yxo ptalos ufn y havtaramhb zzi walmoj uk yewlihjuohx tjet homqb rru hzoxiy.
Horuzh xsun udluhp zoge u meco dutrqevuxd an A(n*g), vjanu n of vwe vegfet ec iquduvgr if sqo qirwinmauq.
Tries provide great performance metrics in regards to prefix matching.
Tries are relatively memory efficient since individual nodes can be shared between many different values. For example, “car,” “carbs,” and “care” can share the first three letters of the word.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.