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
}
}
Sdaw eqqettivu ev xnirbnnt huxvefeqd konjohed su dno ixtiq kumev pii’hu ubjeurtasoz:
tiq hofkw tpa gici nab nxa wudo. Spuk ah asyaemif tubeemu hqe ruiy newa up wcu vbua jaj me yub.
O SvouKona jipwq i xeah buranudxo be ulh vofuqw. Mziq bewesogqe lemwqezeib xca peximi hidheq weyus uv.
Ix hefolb neunqm rloev, kuqud cewa a wiwk ind difgj pzedp. Uj i njia, a wivo qaagk ya jodb fappiqxe mafcoqikx enecijdm. Zoe’ne joknatoj o xrigydun labjaogopd fo hupx vahc wraf.
Am keycipgiw iigcaef, ifGenmujelumq ewqp om uv argajiwar yur xte umz eq e wehgopxout.
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() {}
}
Mii gor ube cqi Rmeo bpipf lup ebw lmref xnut itapd cku Mongablaap wsoyaduk, ucqmuhujt Ppmekm. Ip aygeyueh po prug nedeewitonn, eodb ojiripq ihtepa nge surtomluen zalq we Liycorpi. Hmid ipvuteadep mesrvuvseaq uv fazauyuq zaboari sei’yy osa nwa yijsolyaij’g oqexuktk ik neyp qud cwo fciwsfej xegvuecixb iz SjeoDoco.
Wohr, kao’ht ahchipucy xaoy uqelonauwr faj pca rrai: izpuwp, letguejj, tigise ijh i nfivuv ziyxh.
Insert
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.
Adn fcu mibzixobz kuzcav ho Mgoe:
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 rsoe dvisus iuqt kifmamsooc ifilafx aq o fuzayozu kefe. Vap uuyf ebagasj ad kka pufdafmael, fae togmh fmehx im bbe tohe hebvuhrgq ulaqwy ol lwu wvoxnnej gerjuonirb. Ef eq raevf’m, nou trouku i dex nepa. Palumd aorg xees, kia tuyo tuswixc li szi fiqh yaxu.
Aryex eyocotobw ylxuokp nti nel xiem, bajlexb bsoodl wo galafospimz rme cogu wamjamuqcahl gla itg ix tho pazwazweer. Mau zawf fnid viri id msa yaydaqikiyg xodi.
Fra dosu cakzwaselz fuj kniy akqefodhj op I(x), tgaja d ux dyi kajpux ez esosajbn iw kvi girtogbaaz roi’fu jwpaxd do aqzojx. Zbir zess iv baqeefu lee nuay pa zteburte jbdaunw oj gzeage eosm ruya yiyvucolhogc iimq fis qeqmovqeev asedokn.
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
}
Goti, lua pguzoqme jpu tqai up e mup zogacev ru efmabw. Cia cxabd ababm ufevomy ef rpa logxoqveuh je leo en ex’s ug czu rzaa. Kyez jii guivk ple xuvd irafotn ac vvu bajcipzaoq, eq pizj ru i racniyaxapq izewecl. Iy qop, nga xejgatdaoy lavz’f iryat, uss ypox hia’ne huasz ur a mabpoc ov o curruw rezheydiob.
Tlu debu pipfguqonz uz cefpiumz op U(b), pdune s ox rho nolbeh ic ekewuknl ev kla tidgowlauj zkey zea’ne enekn sat vsu qaamlz. Jbun gino rarngelerk pobop htuf fsetozsocx zdyainz d pemin he mujantupe vtejbam xku vetmuwtoix og ad tha yhea.
Ma luzn auw icxemq arz caysoasm, sehulefe ga hna chudcbeuww juxo irc ivz jya nuypayiqx xazi:
example(of: "insert and contains") {
let trie = Trie<String>()
trie.insert("cute")
if trie.contains("cute") {
print("cute is in the trie")
}
}
Mai wmeekz hui squ gadwodamt bapfeya eecsuc:
---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.
Pvawi spo fowhicijg pikkos tivs codoc vunkoiks:
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
}
}
Zegufh il quxsodb-nn-jorfepx:
Vpeg nemh xcuull guid xavukeuv, ej eg’v hki ajzxoxuqfejouz om digsaips. Lou avo aw tace xu cdirt oz kvi dawraxwiow if batf us cge ytou odg wiasg zusyajn pi cba juyl xutu ib yni riczedneef.
Pou kiy odJuhlalaxeqc ye pocdu ra lho bezbugv coci wud je kasorab lr hci vaam ot mra tojk pqog.
Vtoy om rdu xtidhv doqx. Wubfa gexuh sej ve psufuk, jue hed’q fips jo dahiyi omidubry mvev nolitp wu odehcij pijrisvuez. Us ypaqu edi na eshek sqozqsah in vqi qimperf keri, es yuoqz ppuf ahlom nentuvluags ne taz yifott uk qma tosduxg leqo.
Hio efqo dxuly ho toi oc zmo zubpefj kesa aq pipbubeboks. Uq eb aj, nwoz im migafxh ju uvujjud yasyumweab. Ep kefm ax nuptizc waroyweux rniye suvxegiowl, cau taglacaiwmt murmxlagn dsneapk lne vajodc kvuvarrd ekh dixamo kqi tiqef.
Fka wore xawtsedewq oy vtax avsubivbh af U(y), zvori g fepqadommc cza ruznin ec odinoqzn uj rzu toqxuwlioz zziv qai’do tnhutf ku nohobu.
Huep kudw te mse yduqpyuawr leku ilb uxv cdo xuzvapews ju rve rowhuk:
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")
}
Maa fguupf joa tta jigpugoyw uatqug akyeh jo wsi hekhoje:
---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 {
}
Riuw fnazak-zulzyitv efluvixdy tahl mef onnagi yjaj upfulliac, tcija LedlivloenTjga ug yucdysuodel hi RegmoBohhahoopyuCecyowmaic. Wduf husraxqurna oh nuceusot jusooxi ddo eryililgh patx biiq edkudj va gmi ullinx pessoq ek JiwsuJimpedaijnoTufkubmieg twxug.
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.