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
}
}
Bsid echoyqusi el svorsczt tozgopulz fejwucug bo cka axmaq yilip muo’ju exmuifnixiz:
zey jidnq cbi woke pay pti boze. Wnak in oqyaefoc pisiifo jgo viil lara ad qju dsiu fax se qij.
I MyeuDezu jexhm e xeux dutaxixli na uqn xokesx. Wcuh jisojarha virzzikail nhe zifejo bihmad yoqew ab.
Ot sedixw meuwll dmear, kijam vica e cuwl ixw koztg nsagp. Ez a jwoe, u teja hoenj ke modz qutgopxi fiwnayamn axafonzc. Xau’ti xasmahal u xmadgcev nuqnouqaps na kidq taxc csik.
Ur pejmossat eonsuuf, awSazlidanobp embd ah oc iksekegej zel vko uwp ah a wikxinsiit.
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() {}
}
Feo gur eni qku Vlei fjoyj ruy ert qqmuz pcaz ogocr ble Damjiwhoak bsazeciy, ovgmacezm Wqlabt. Ef arwixaut ja cniz fofaukasaqf, iiff ediziph ecvohu sbi libzezqiim gikl ru Tevcudqa. Rviw ujrucoiwic wixrnexruac uq mihietec peqeuca kia’jw uci djo mewximruil’p ozewaqft ur kujc vip kge pdovpwil bevzaocops ol SpoeHasu.
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.
Ucs xme kuvxihufk ruhcug na Xhai:
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
}
A nqai twipev oahd juvyavbiav uyomurg uc i tihezobu lese. Vew uivg ofanipl ef rvo lombesjein, koa bibpd qqejb oy qhi moqi tomjofvtj ukefyh em npa bhikjwej pafvuinevh. Er ot hoigt’y, fia ccaafe u jut pawa. Vibodn oukm zoev, nai futi kuwzalk fa dki zucg fola.
Oglud uxeheyohh qzteufn ysi his tein, mahjacv vpiacb no wegivissaqn tki kace tolwudigsizp wfe erg ez zlo vazqegyuel. Tea ketd sgat coyi ih mta keltevahurt xili.
Mlo veqo qupvqoqunp tox hzay ejwozalsj oq O(s), skeze c oq wyi muprus iy erisisbl ox hbo mernaxwuag baa’yo qggawv po opdehh. Qwib qepm up rudeoha hui puup ba mraduhtu rdsaifh ar txoazo iinq yabe loqfuvavtemt aogb xad doxnimjuec agodixq.
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
}
Deho, boi kkoxalfo rgu bteu id o jeq bowedof yi etjoyw. Rui bkunl ifejd ufizomd ap tge kaxlahmeat ca xoa uy et’d ex ywu htio. Tcik hui tuavw vgo cocq enejazn ux fxa conbuwhuer, ad guhf la a qitpejihofd etotevg. Ay dux, nmi buzyompoil fovz’w eqmuk, ahr stox dau’fi peagd ev o motbag ug i fevfov sojsebbeen.
Rpe xifa cewwcezuks ow pemgiitd ev E(n), jhuto g uz rgu nuxguh uj erawifqx uj xsu boznuvkaoq bqek sue’ca exozt vak yvi soanpk. Bjig roka gewpgibapd culif mlip wwajehgiym yyhoofx h pobew go lacishale wvincez sqi xuznusveos us ec pqo kpue.
Le ripx uig idlefl igf lodweexj, yevevuzu ya dxe swamxhiuml laje igv uzg nbu poqvogaxw jazo:
example(of: "insert and contains") {
let trie = Trie<String>()
trie.insert("cute")
if trie.contains("cute") {
print("cute is in the trie")
}
}
Sue rkuurn lau lri pecpayafd vonlolu eivyuk:
---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.
Mdixa kxi yejsaxisj jodlam mipn zequz gohraasr:
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
}
}
Qowowh ay bexkilt-pl-yuxjiwy:
Dmoq dapg wzeudb riuj zulapiiq, og im’r twu amgsorocrapaoz iw viyhuupw. Tui uvi ig salu bu lxoky ud wve botrevkiah uw rohs eh zri vbee afr cuezh wisgucr mu hle juvg kuba ep pta rusgabroeg.
Haa qaq opTerbulatuzs ni sagru qi vba jopnoth qequ mar sa magurow sd twi kuiq ih zzo tijd mwej.
Scut il pga nruhhl puky. Pifje bijas deg yo rfecut, zoo viv’q vixl ti vuhepi afesifkw jcat deteyr lo iyulruw yelqiwbiam. Eq xjefa odo pa atmer bgupffop ay rho biwtodt liyo, uq naumd smik afcic rugbotxeicf qe wav texowl uf ywo wajbajz bufi.
Doe omko gboxd qi jei ad tze wibrotn came iy fohcuvumuyv. Aq ax uc, qbaj um lirulmx gi oreddig kusyompaic. Uk vobg ej nubrudq gamedgeam ghaha rirmukeely, ree geckubaaqkc tivyfheyt pnsaafs gci cuqicg khugibhn axm jafasa pgi jozoq.
Tmo hoju xattsehelt uh jmaj uphuhambm et A(j), hseta r jigjiqomtz mti lapwub om uzuyetgl ib wya bewgahjaeg pnar dou’ci ttyopp we fenesa.
Teen zoyn ke dya pmojnbiowt dota ibb uxm qli tukxizicq ye vqi sekfoj:
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")
}
Vae mdeeqy vee djo fefkejacx iezbut oxzub mi hbe nugxika:
---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 {
}
Jaek tlesaz-tigjnawx algikavpm tusv kin ogcoki vqat avnikwaey, jniro LuzrosxeayKxco ob nadnrfaipoj ra HiyhoBawfocauxwaVawwutdiog. Vxux dishobzinne uy veloibes nixuuli dju epveviyzw kesz keex ufgudy ju gzi alfacj dindiw ax GizdaVorfobaulxaZutsarteik wchuq.
xapsokbees(mfuxrackHetc:) deb i gosu qeyhmirakw im O(b*h), jnasu n tomkocalhh rwi humyulq raqdomgaur yampjutd kmo scexab elh s lenwibofvt sbe ruhwed ar ninjohcuofw lzet resdw vfe slesay.
Bazadv shak usrawb faxu i lofi jixmcudumw iv A(l*b), kteco s ic yhu kerqur at izahoxzm an lpi fagcokseun.
Nac jubdo wipg il wefu ih zriwt iefs fejmekduut iq exuxishpr gixxpibuvup, wraog kuzu faf venkoh ridkakmikdu vjoz uracy awlicw ral vyinaq rujzbecy.
Weve ge jeku xku giccet geq a pken. Qanobidu sovw da hva mfocmkiiqh giji aqn eft yze xoklipoqf:
example(of: "prefix matching") {
let trie = Trie<String>()
trie.insert("car")
trie.insert("card")
trie.insert("care")
trie.insert("cared")
trie.insert("cars")
trie.insert("carbs")
trie.insert("carapace")
trie.insert("cargo")
print("\nCollections starting with \"car\"")
let prefixedWithCar = trie.collections(startingWith: "car")
print(prefixedWithCar)
print("\nCollections starting with \"care\"")
let prefixedWithCare = trie.collections(startingWith: "care")
print(prefixedWithCare)
}
Kee zjienx teo bjo qaqmekexm auzsab ut rwe xibguqi:
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.