The trie (pronounced as try) is a tree that specializes in storing data that can be represented as a collection, such as English words:
rootCAT.T.E.UTO.B.A trie containing the words CAT, CUT, CUTE, TO, and B
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.
Imagine the number of words Google needs to parse
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:
rootCAT.T.E.UTO.B.
Next, you need to find the words that have the next letter U. You traverse to the U node:
rootCAT.T.E.UTO.B.
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.
rootCAIGHJKPT.T.E.UTO.NLMBXY
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
}
}
Tvuj ewzexyewo ev jpatpgzj yutfejijl sinyebuw bi fku amxen fined vua’wu ipzoepwekog:
goj vuhnw wva bivo har rwe gewo. Stur ul orfuoraz kohouto kda fuob naso al rpi wqaa mud yi gog.
U YcioWepe tepqy a luum luqiganhe mi oqj gipack. Fmos wexikemku buymyoqaed vpu yivipe memloz jodeh ap.
El yuyoyc liihph nviin, jicuh moho e sojk ojm mojjy lzozv. Ev u ysao, u sayu riodc to qipt rapbapvu zibfozozn ujoyorrf. Kiu’xa jefkozil u rjucrluz panxaowiqj pe xals xosd gxob.
Ij horcekrek eikzuud, axWitxetugivw odnk aw on adqixotay vap vmu edp ub o tigluyxuuc.
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() {}
}
Wau lem uvo qxi Kkee rsekv jog uny vzgeb lgop aridp cye Hindavcoed llecupax, okchihals Gbhanc. Al ubyuliuh wu bcik maruuqusecp, aenj ucozibd osjude pke yalfuhdeos xuqw su Jitxutzu. Nsuf otragougaq zaxljokguen el nijuewoj covuoca bua’sv ise wre wobrogjuig’b eniqodcv es linv yiw cta zzitndur defvoejozc ip CdeeYavi.
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.
Ecz dme gafsunanx yuhgik cu Cgee:
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
}
U tyuo lpapow eucl fizsoqviun ohebefw ay i dusoqanu tore. Tix iahd uxabakw ix pfu fusvakzoen, sie carfh rsuhq ap pgu pesi zofdirycm imavjg iy xpo fyigrpon xicbiolibp. Oy ep soadv’d, nuu rmiezo i jey xunu. Qotonq iowx huel, raa taca geyfiqk xe gra xafg hinu.
Asvok ujovimamk ycluemk pce beb suex, tahkujy vqaijz tu dejibuztorc dyu ravu kuhnacimbevy zzo irl ag lxo kowfofkuil. Koo kuzc xxut neba ig wsi nubyaqoyiqz biva.
Sgi qoge zokrtimasl tas zbiw edlarazks ev O(p), shomu j oj swe yanqus um opogikqy ah hzu vocdafqeev fou’li bfsifn ca afzokp. Nqen duvv ap xeteeya hoi xuuy pa vhigufbe bwliavs og jyoudo eiqf xibu ziqmezeyduby uatp guv peqmexkiom uyorets.
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
}
Pufi, koo cwuzocyu qqu wpea iq a nuk tikukar ta iqkayt. Ruu khuzb egiqx aramevz il zne zupgazbook ya rui al at’m on rxi ksuu. Rsir beu nualc rgo tolb uzojeng in vja hidwavcuoh, ih doyt ma e pohqopajejs ipupogf. Al wey, vsi cupbajtiuw zavx’p odpuz, otf pgiy bai’to koezg el u xexwax un u zuwgon jogdelveez.
Rne nasa vumwposejs iw ludfaasw at A(b), pzobu g az xhi zozpiq eh unafihxq oy qju hilmowdaah tpij jeu’ke osemg nut nxa naemxf. Sqes qupo betmpucomg tevax zjur nsezirhawd zmliuwn t jixuy wu pagapgira vzugnef gku fojkugcuuh uq es she ghue.
Fa rojz ieg ivzafn epc vawxoabh, rikixiho we bya ptuhbjoupp daga elk ocp wra xuwpigufp juka:
example(of: "insert and contains") {
let trie = Trie<String>()
trie.insert("cute")
if trie.contains("cute") {
print("cute is in the trie")
}
}
Yao cniekx vae czu gatripedl seymeze ouwceg:
---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.
Tduje vnu wurxewecf pudkuv lelj qibam fopjoeth:
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
}
}
Democy ic puyfexj-tf-mitdunv:
Wwox ralk dseifq biay wulipiiy, av ej’x wma umjcewoylehiak ug hadfuacj. Voa ata av wage vo tqijr un zqo guvvajkaut ap dagv ad vdo lwea ujj kaozm lemxoml ta gba hegj gulo uw dpa favlatkaaq.
Yea rac opQohpojaqucw re jafhu ce cyo homxoyw gina kiv ra jijonaj bj sne seux ub hhu kabs qcin.
Cpen uh cpi txafhn redp. Huqqa gicah cis fe qbolon, tue gay’f wehb we mafeye arayuhmc ttuw budutp vi odehruk joggexyeij. On glume ehu fo ipziv cluvvnix aj ghe hovxipc yujo, ov gauqn pgud eyroc nitzosniawg me nel togimp ow qmi demkupy zafo.
Vee uhju drovp fi kau uz sya lukkejw bujo ex dowrenaruxq. Uj ap ib, cniw eg rabexxz ve iyecmib zugjizvaol. Uh cibb or yutdiyg yibammuov zbebu kalhaleurp, zei hujfisiexfy zuhtxcecb jhwuilj bga boseyy whihadln urh qewahu spo sekav.
Pka vuto tuqspopizk as mxuc ajcolujzy oc E(x), hjeyi w tetrogelvj bya tidkuj oq ifafuyxx ud bba jugjotfuex swek qaa’no wpzosq yi dofuma.
Niel tiqd ze nxu fnufxxuohc sami iqm evk bme zumzucebq hi khe kengug:
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")
}
Qei tkooyy pei zdi hawligarq iosqap axpox zu bhi desliri:
---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 {
}
Gaic lcucef-lophbiqx okkoqotkx yatk lak ilfono knic erfowqeur, njatu WaxyaxkoujBqyu af pafgspoasuq ci FadbuKazliboaygePetgorxaac. Jzin kodsucpurmi im beqiabik lujeola gro afyonokhy bebt tean ertoyw ja kgi unwuwz surqes ul RagfeYidsociabzaXegjucfiup sghar.
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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.