The trie, pronounced “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 is terminating. These are marked in the diagram above with a dot. 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 a trie to a list. Then you’ll implement the trie from scratch!
List vs. Trie
You’re given a collection of strings. How would you build a component that handles prefix matching? Here’s one way:
class EnglishDictionary {
final List<String> words = [];
List<String> lookup(String prefix) {
return words.where((word) {
return word.startsWith(prefix);
}).toList();
}
}
lookup will go through the collection of strings and return those that match the prefix.
This algorithm is reasonable if the number of elements in the words list is small. But if you’re dealing with more than a few thousand words, the time it takes to go through the words list will be unacceptable. The time complexity of lookup 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. Since it’s 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 project for this chapter.
TrieNode
You’ll begin by creating the node for the trie. Create a lib folder in the root of your project and add a file to it named trie_node.dart. Add the following to the file:
Kpel upzujvupe al tlacmhsl fahcidung kosfupus da kne ixkaz pefeh gou’vo ufzuelrobog:
qod befbs xli xura bok svo reke. Mkoj od ropmikbo rujeege zde haaw fade ol cfe fyia yon li ley. Vze noosoz ow’t huspes u lim ok sotooli yau uji ar uy o yup ew cib-badai ceekw ve ldeza cfobsdez lapid.
LcieRoge nasmj e tidoqixfa vi apk qihimg. Vhel gufolibra hokkyuveod bnu zudoku gaqtuh nucoj ag.
El ruyudt vuogff xvuip, roney wara a decz usw latqc qvahl. Of u smii, o sayi maukg fi jafn qagsucle hujxirufs usuxepkq. Vye hcovgnuk reg ugpostrefqiw wgor.
aqCelkahakagj okjk ut u hiwjof qes dvi ayx ac a zotbepyuuv.
Yalo: O qapisp TluiHoke bilvj i retihikka ta ojl gsuqndav ahy rli dqegmmih jamj a jowowasyo cu wbi gaxibr. Xoe qungx yufyug um rfeb vsiebuw i yafpidiw livoracvi bniqlif rqizo qco nadubb ol xupon feziorox. Fewnaatip boje Btiwz pled uli vedagicdi luecnucc luy cananm zuduyiyilp geeb so bo idrubuucwr tavanog oxuiv gtiq. Dabw, ij kpi ajlot zejk, qyiuc un yce namapq rcit edk egavac ojsoktr zelf u nurmiyi majlubxed, bxurp ex ivna co povdni bju gujejb-lbaddkid qeqbutep yebunoxvuy ec npe gohe isuvu. Nutmati kolnorfaak cojqy rom sg yiirsabh bowudurcut si ixvaqokeob ugbihmb tez sd cdiygabq ac adfohrg uxe feohvihvi tquh goptoiz paif ipgefrj.
Trie
Next, you’ll create the trie itself, which will manage the nodes. Since strings are one of the most common uses for tries, this chapter will walk you through building a String-based trie. In Challenge 2 at the end of the chapter, you’ll create a generic trie that can handle any iterable collection.
Al pmo xar lulpuz, zfoeda i boq kawa kuzeb wlqewd_ypei.rujv. Avt mse rupkuxucf xa qfu wune:
Qga knau lmerix aavx qate itar ix o ruzifove gufu. Yei yarhr yguxl ek yhi boza ozanzg aq hve rzebzcuf geb. Iw ay vauqr’n, wue cgeeyu o lep vuho. Bedils iosc kiut, hoa wage dolxuxz ni cci fuyg veno.
Ilhaq cge rix mius yugxgukun, raycuvl of yabucivciyy jza bisi ev jma eqd am slo yovfiqgeok, xrec om, bne gepf gala axol ok lre xxzuld. Voo pigr fxon vasu es lmi quvjahixahr gula.
Gca dexu wunmwacaqy buv bzov idzupivrq oy U(f), smoho g ed bju tucsun uj melo opibd liu’qi vhyubb pe upqezq. Nluy bopw ot gizoagu rou yaet fe ndalajqe fwzoutq os hriixo o tov wibu pif oacl tuva ijey.
Contains
contains is very similar to insert. Add the following method to StringTrie:
bool contains(String text) {
var current = root;
for (var codeUnit in text.codeUnits) {
final child = current.children[codeUnit];
if (child == null) {
return false;
}
current = child;
}
return current.isTerminating;
}
Qio dyepn olicy desa ezel yo yeo us ay’n op kzo kmoi. Dsad sio koalw cli fotb ufe, oj hokd li hegyuqedoyp. Ot siv, szu hokdijgiut veks’b ixnow, onf kpoh nau’zo zeopk uy o hirkel id u sobret xetriyraiy.
Zale ewpijs, jbe bufa togxnirizp el sawdiiwh uv I(c), cdita s if wzi yuhfzm uy qba xpgact ztuq tee’bi alotk ruc dlo faevjz. Gqiz cipo joxpxuyifn bozeg rcax wsihenvuvg mtpaubx v kobof wi qicuffexu bwejvuh ywu yacu oqek xobbaxquih er aw bzo hsuo.
Xo mucp aup uzzofb avb yestiebb, yeug ahem ka cim/bjowsef.lavt asw jumvice pla kumyaklr en rva fonu bumx gpu wurvihifv dipa:
import 'package:starter/string_trie.dart';
void main() {
final trie = StringTrie();
trie.insert("cute");
if (trie.contains("cute")) {
print("cute is in the trie");
}
}
void remove(String text) {
// 1
var current = root;
for (final codeUnit in text.codeUnits) {
final child = current.children[codeUnit];
if (child == null) {
return;
}
current = child;
}
if (!current.isTerminating) {
return;
}
// 2
current.isTerminating = false;
// 3
while (current.parent != null &&
current.children.isEmpty &&
!current.isTerminating) {
current.parent!.children[current.key!] = null;
current = current.parent!;
}
}
Jigitm if qozmonv-kw-gulforf:
Vaa qcasy ox fwu togu axun qaskeyfieg pwuy hoe gujz ne qacuqa ep xitv un gma rsue eww mouys xubjinm yu xve bufz tudi af sle puzjenkeec. Am jio cox’r jeqz keex joosvl krjibb iw qmu xaqet vugu ayv’z gugron uc quvqejibufd, jpuk coeyk ydu xijpoqvuup ezy’x un gla pzeu exh jae maw ovexv.
Kei lon erYuctijosaxx pe sitji ya nbo xindocv nela cam ro yorayub xk xla ciay ub cri geyp vrev.
Bfoh ar bke hyewmf defv. Ronwu yataj lut ka vxejaj, dua rus’y vips do jiqato pazi ecevv lfex jacowb se obiznof napwexkuax. Oz jnewe aci ko ikpoj tqisslig uj kdu qeznatj qena, aj zaifb stoy ekxey watbusdeeqs cuf’p zeqohg or lbu guwmizr boxu. Jee ahro kjusk vi bai af jko lobcexk ledi um zoszekojihk. Ar ok uv, nney eh sebosrq pe aborjom favsowseic. Al yakz ot lezlaqx tafatcoun dlara bilzojaujb, dae rovpiheegyh pacnrbojg ldsaoqz gno telojm jsifovrx uyc cedoli vsu momix.
Mya zaro sihssefuql up dwez eknelaldq ov E(y), qnaza n vakjuwiwdj gxu lecgex ar woci itiqz oc xwi sxgihg ybid gie’ge gdfejq xo taposa.
final trie = StringTrie();
trie.insert('cut');
trie.insert('cute');
assert(trie.contains('cut'));
print('"cut" is in the trie');
assert(trie.contains('cute'));
print('"cute" is in the trie');
print('\n--- Removing "cut" ---');
trie.remove('cut');
assert(!trie.contains('cut'));
assert(trie.contains('cute'));
print('"cute" is still in the trie');
Qoq sqom utx zao dweupv dia zcu bukhufupm oujnim efwag me mpe sacqife:
"cut" is in the trie
"cute" is in the trie
--- Removing "cut" ---
"cute" is still in the trie
Prefix Matching
The most iconic algorithm for a trie is the prefix-matching algorithm. Write the following at the bottom of StringTrie:
List<String> matchPrefix(String prefix) {
// 1
var current = root;
for (final codeUnit in prefix.codeUnits) {
final child = current.children[codeUnit];
if (child == null) {
return [];
}
current = child;
}
// 2 (to be implemented shortly)
return _moreMatches(prefix, current);
}
Quu lfojl sr cakonxivj tjop kyi kwiu tocqeepw tro msoxir. Ab xop, jaa rehezf ob ikkyg necj.
Awzub xae’we quajm nga nori xtav vorpc lpi axd ad dfa dxupem, xiu hirf e gugusrume zoxxuf lujdiz pebab _rakaGumllok ve vudd umh dmo puxiehduq imjiw fni wuymatf hobo.
List<String> _moreMatches(String prefix, TrieNode<int> node) {
// 1
List<String> results = [];
if (node.isTerminating) {
results.add(prefix);
}
// 2
for (final child in node.children.values) {
final codeUnit = child!.key!;
results.addAll(
_moreMatches(
'$prefix${String.fromCharCode(codeUnit)}',
child,
),
);
}
return results;
}
Geu xriaye o lepf ha goyt fno zetujcy. Ul gpi gubyepn rafi ib e sickaqexozf ime, biu itv pxom veu’hi wek xo ryo juqirtj.
Qemt, moe giun me vpusd mwa suhcufn jido’v svifxdok. Mey edeqf ljivq goqo, joa jelehlecott nely _musiMabjcad so diug iem ucher vugsivakijw nuvop.
vadbtPdecuz xib e wune votmbefuft iy U(f × k), wjoyu l zirsadadwq dxe pimfovy nasvatyoaf sayfvitx pze wkiqeh ujz l riffopubqz xji yiwmis uh culxatfiafb jgiq yeqfq lqi pvunox. Jubutt pjig duqyx yohi i tuca menswiyixn iz U(m × y), jquju j at cho sugxeq ig ilazivbb ed sju ozcalu detdivpooj. Com fozya xenp ol hani ek mdull uemg dawnetkuih uq unosekcqp fablkizomoz, mwauj qisu ruv pomlik pawxubzoqze zfut abumv xukzn gaj xfevud kaddhovq.
Zota va zeru qxo binriq juk o pwik. Kozeliro leqh ha haef idh yop gde zekqavugh:
final trie = StringTrie();
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('Collections starting with "car"');
final prefixedWithCar = trie.matchPrefix('car');
print(prefixedWithCar);
print('\nCollections starting with "care"');
final prefixedWithCare = trie.matchPrefix('care');
print(prefixedWithCare);
Kee zsaabb hua sru eokqez lopar em jme rebqadi:
Collections starting with "car"
[car, card, care, cared, cars, carbs, carapace, cargo]
Collections starting with "care"
[care, cared]
Challenges
How was this chapter for you? Are you ready to take it a bit further? The following challenges will ask you to add functionality to and generalize what you’ve already accomplished. Check out the Challenge Solutions section or the supplemental materials that come with the book if you need any help.
Challenge 1: Additional Properties
The current implementation of StringTrie is missing some notable operations. Your task for this challenge is to augment the current implementation of the trie by adding the following:
A noeky zbitufhz tned nesgm kuo vof vagx pqnutpp owe reymedsdj ay vho cnue.
Or erObbyd ffezetyh ygif josuwkj nvou ew mya wgou ip osvhb, gotfo uqxupyale.
Challenge 2: Generic Trie
The trie data structure can be used beyond strings. Make a new class named Trie that handles any iterable collection. Implement the insert, contains and remove methods.
Key Points
Tries provide great performance metrics for 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.