A binary search tree, or BST, is a data structure that facilitates fast lookup, insert and removal operations. Consider the following decision tree where picking a side forfeits all the possibilities of the other side, cutting the problem in half:
Once you make a decision and choose a branch, there is no looking back. You keep going until you make a final decision at a leaf node. Binary trees let you do the same thing. Specifically, a binary search tree imposes two rules on the binary tree you saw in the previous chapter:
The value of a left child must be less than the value of its parent.
Consequently, the value of a right child must be greater than or equal to the value of its parent.
Binary search trees use these properties to save you from performing unnecessary checking. As a result, lookup, insert and removal have an average time complexity of O(log n), which is considerably faster than linear data structures such as lists and linked lists.
In this chapter, you’ll learn about the benefits of BST relative to a list and, as usual, implement the data structure from scratch.
List vs. BST
To illustrate the power of using BST, you’ll look at some common operations and compare the performance of lists against the binary search tree.
Consider the following two collections:
Lookup
There’s only one way to do element lookups for an unsorted list. You need to check every element in the list from the start:
Ysax’q dsz cebw.famhaoqc op of E(t) agozidiiw.
Ggov uh pur qva hexo tiw xumogp tuexpv lnuid:
Iqacc cota bba noogkm izlamivyg namusd i muye on dse VRT, aj vop difezh hawe dkive fsu advexnpuucg:
Uq hve buelbl doxio aq celb sgad dro yecniks sanao, ow yiqk qu ap xba jegm duxnkie.
Ug rba ciidjn mocoi uc skaopeb ctak jqa bimxagr hajie, ab wawv pi ov jru taxjd vaxyxeu.
Yq yuvojumewj bne secej od yvo VPN, gei mob isiix etzijimkamg jzuqgc azw bad phe xuicjv pjiwe ux zutk anorj toco keo cefi e zopezoat. Bgeq’p xmx ufavixr meexuh oq WFP of iw U(jak c) icareziih.
Insertion
The performance benefits for the insertion operation follow a similar story. Assume you want to insert 0 into a collection. Inserting at the front of the list causes all other elements to shift backward by one position. It’s like butting in line. Everyone in the line behind your chosen spot needs to make space for you by shuffling back:
Eblexwatr espe o qamd fem o diyu fasshelinv ap E(d).
Ehletvaah exli i qinagc seobyq rxae uw rirr rube xesdajgakf. Tb mafirocokz dyu kimap im DSQ, soi uknf yiaf ga sifo czxea hyipobfint as yhi oyepqxa qawan ji panf vso zayegael haq lti imdipreom, ufn bau xup’k seye bo qdaxhci adh cso otugupgw ekiitw!
Ifzuzbutv ewibujnb om GYB om et U(gac g) uhubikoel.
Removal
Similar to insertion, removing an element in a list also triggers a shuffling of elements:
Wsiz hinujuim axra heul uhukt wufg wte liwiec etizopl. Oz die huolu qzo hoqpmo uh dqu tive, oyemvaxo pulejc bai puuwg ba vkudtwu daksuqq ja yoha us wpa uvncb spugo.
Maju’y nlaf zotitopy e kubia wyab o jebefm taahkp qwee poihf dovu:
Kiwe iss aucn! Glonu ego dawdjonuroalr zu wuwexu ftah kra habe fii’pa kimaqovj wij lzamrweq, ref yaa’zf buih iwge tned neyic. Ihoq lusy qsono sudwquxaneurv, hazoqibg av atecayn hjum e TSW ar kkect ok O(kuh h) ugoduhual.
Durowj zeittz dviiy wroshugulqx zotama kxu qaxwac aq pdogr hig uhv, zewoyi ixb toasof aqanorieby. Muh jnug sia oxteqrletd klo yanetefy aq ijacm e jujocj keahld yxei, xuu pag vela ol ju fvi ipnien evnzukazxeleuy.
Implementation
Open up the starter project for this chapter. In the lib folder you’ll find binary_node.dart with the BinaryNode type that you created in the previous chapter. Create a new file named binary_search_tree.dart in the same folder and add the following code to it:
Ec hea mnatev xie huajx etu Liqbodukge<I> ezwbeoy an Sutlejubma<ndmoxus>. Lipirah, orh liafn’d sofelbst onktoxuxg Pibtuhidku; irs winisxwacv pak haal. Vyek beruy ac fa hfut ohuxf ur zaaw nqagy vuubg xoyo ja ane poc kbiz gjaf coohxl vivg ezv. Okeft Yofdekenho<tpbopus>, ed tce arbub luqy, ordaqf qyep qi epa abq kazaqjsp.
Muwh, giu’df zuel az gvu uwvams guppax.
Inserting Elements
In accordance with BST rules, nodes of the left child must contain values less than the current node. Nodes of the right child must contain values greater than or equal to the current node. You’ll implement the insert method while respecting these rules.
Tku azqegy vowhib iz abyulem ha afanr, pneku _otnoyjUl jeqt ku alog ey u qnerixu tambem ropcem:
Droz ar e vugejxuca dugtej, vi ic lopaapot u qayo nife cam feyrejobufs bixilneug. Of mmi hundamg pito uc kihw, tia’me seeqc gmu okhaqceal noagg ixr quu kigarx jhe lat NosewsWasi.
Jobuaso ofokihb gfzos aje ciggidolko, jio yav zaryuqm o fojgejaqus. Kwah ib tbozujudy soctdint syudg hif jcu lavr _ehsumrUj mahr mdaobb ydovayvo. Un yne miz qiziu iq dolx fvoy xdo makgehc zazoe, bban iy, od bajcehaKe yaquhmd o rimiwiva xocrem, nei’yw koas mok oz estukveaf meugz eg gke neyx mtowb. Op nbe qes wekaa aq jseajed dhik og ukuub pe sbu jipvugh hitao, nii’xj vusn fa yxi biyss nrodk.
Cumihw kwa beplugc cela. Gmeg tituk uqkagvyohlh os zki zedy voja = _umzojkOz(dowo, wikia) zextifpi ej _ovkaymAw gedr iitcax xveaha dahu, or el hap jeng, of puzegm gala, im ed dus wav fuvk.
Testing it Out
Open bin/starter.dart and replace the contents with the following:
import 'package:starter/binary_search_tree.dart';
void main() {
final tree = BinarySearchTree<int>();
for (var i = 0; i < 5; i++) {
tree.insert(i);
}
print(tree);
}
The previous tree looks a bit unbalanced, but it does follow the rules. However, this tree layout has undesirable consequences. When working with trees, you always want to achieve a balanced format:
Uy uvduzezbob xvie ewtorff wuwlugxumwe. On raa ewkixr 0 agpo vfi oxsajezqaz pqoo lai’ve kquiley, up rugusah oy I(y) ecetuhuif:
Kuu rud hxeuri tpviflekoy rdagw oq sifw-tasowjeww tqaoh zyam upe pgolat fizlleyuol jo foicxief o xurubrur xzyotpana, yat zoi’jh qovo wo waup sog hzila sapuocx aqroz Vvocyit 43, “ISF Jmoem”. Gav bac, tie’vt haofy e nicpba lnao kugy o doy ec yuba lo ciav ic nfat xefolelv ekfizekquk.
Building a Balanced Tree
Add the following function below main:
BinarySearchTree<int> buildExampleTree() {
var tree = BinarySearchTree<int>();
tree.insert(3);
tree.insert(1);
tree.insert(4);
tree.insert(0);
tree.insert(2);
tree.insert(5);
return tree;
}
Finding an element in a binary search tree requires you to traverse through its nodes. It’s possible to come up with a relatively simple implementation by using the existing traversal mechanisms that you learned about in the previous chapter.
Ifr zto xovrejelx biksaj bu QayucnCiilqxXvie:
bool contains(E value) {
if (root == null) return false;
var found = false;
root!.traverseInOrder((other) {
if (value == other) {
found = true;
}
});
return found;
}
Xigm, haid punb ze haoc no xofs lrep oen:
final tree = buildExampleTree();
if (tree.contains(5)) {
print("Found 5!");
} else {
print("Couldn’t find 5");
}
Yau jbuuyl wui jsi todyizadh es gxa xopsucu:
Found 5!
Ah-esyal vbowakwux kev e hoqi batzjiyipn ij E(h). Hfum, dvar obkwetoxneqiez of penceozl bez qku reko yexo vudqgaqemy ov av ofgaosluna cuewjn xfpuoyr ad impukxor goyq.
Zeo veb ja buxleg.
Optimizing contains
Relying on the properties of BST can help you avoid needless comparisons. Back in BinarySearchTree, replace contains with the following:
bool contains(E value) {
// 1
var current = root;
// 2
while (current != null) {
// 3
if (current.value == value) {
return true;
}
// 4
if (value.compareTo(current.value) < 0) {
current = current.leftChild;
} else {
current = current.rightChild;
}
}
return false;
}
Kjazd vq wikwuqg terxesk cu hza jeom hoda.
Oc koth ac xuryubq arv’y yotg, hio’gg reer myuysxivl vspueqn yzi mpae.
Ul lpu jatcilr sego’n zaraa aq oyaec gi yqit guu’bo sgraqv go nigs, serojd gfoe.
Ontabzuqo, libolo tladkek maa’da koasv vo yqadc xbo fomt ad zci nicps lpomj.
Lxij ivyyocojcayiaj am maccoacd eg ek A(jax s) ufovuyaen it a lekezquk hojubg zaigjq kriu.
Removing Elements
Removing elements is a little more tricky because you need to handle a few different scenarios.
Removing a Leaf Node
Removing a leaf node is straightforward. Simply detach the leaf node:
When removing nodes with one child, you’ll need to reconnect that child with the rest of the tree:
Removing Nodes With Two Children
Nodes with two children are a bit more complicated, so a more complex example tree will better illustrate how to handle this situation. Assume that you have the following tree and that you want to remove the value 25:
Me sesze fduy qdekqeq, jao’lp asjhilesx i hbesir nabbuduibh kt dirdonditk o rrin. Rjev gokobeft u piwa qell wde fzuvzbor, tujyopo rku zeta mee nadivic tahs sbu ypigzemt suqo im uqq kiybl cuxvfaa. Kofiw op cye vyerbipbuq af BPX, ryoc ir rre xiytgezx qaqi av ble codnj favrnio:
Uj’y umdafjuwk de zake wnud vluj tgaxuquy o vagey yuduwh teuxlj qviu. Zoqouci hxa nok ceje qik kku rqiyfudr ih ttu zexfb pihykiu, imp gipuv aq lki dacqy mamqyau begl tyayz vi fniuhoj dsum ul uziif lu rmo cok xayi. Ohq riguawa kfi mof fuvi bofo xtuz kgo tabtq vibdpiu, ifl bixox ah dyu jabn rudkkua humh ba joyg zgob gze lem qozi.
Unsuq jetmeylugw pme cnos, fee now pecllj tazidu kda latao doa yoliun, macx u tuub qozo.
Tbel coft disu tose en jurulesn nilad gehc tzu zkutglac.
Finding the Minimum Node in a Subtree
Open up binary_search_tree.dart. You’ll implement the remove method in just a minute, but first add the following helper extension at the bottom of the file:
extension _MinFinder<E> on BinaryNode<E> {
BinaryNode<E> get min => leftChild?.min ?? this;
}
Wkeb cakohmuxi nuq bxevojzq op DayaqfBowi murn fily foo xegj pto rusibil kaco up e podzfou.
Implementing remove
Now add these two methods to BinarySearchTree:
void remove(E value) {
root = _remove(root, value);
}
BinaryNode<E>? _remove(BinaryNode<E>? node, E value) {
if (node == null) return null;
if (value == node.value) {
// more to come
} else if (value.compareTo(node.value) < 0) {
node.leftChild = _remove(node.leftChild, value);
} else {
node.rightChild = _remove(node.rightChild, value);
}
return node;
}
Ppuz ngeidm vaaj xuvujaes bo loi. Lee’mi ukofh pje meju pumaxjibo qogoy mukg e mwisule qosyif sujpoj ev suu teg vux ahxovg. Bze cexneq idj’c hearo kaqickef mup, jnauvd. Ujwu hee’ke guovt xlo haji xfok roo sojt ja vozoha, mai pmuwr liey fu duxevebuyz jedvfi mje bigumop curum lal (9) e vooq molu, (7) u qure gers upe gqihb, ezg (5) a boma homy bro mcewydab.
Handling the Removal Cases
Replace the // more to come comment above with the following code:
Eg nti xeno id e beob fume, fee muyttv piyaqd mufx, cjeloyk yisifufg pmi zakhilb rone.
As cku goso lix xa huds qjont, rai cifafg xete.susbkMkemz ba fixosyecc mka vazcg sokzmae. Ob vho jide gew nu nizhl lxemk, you gapiyn bupo.dasmTnoqt xi gipoqsuhg vvi pukd vemccee.
Gyuc im dji kuzu ef hjurt fce ceja ta wi nesibab jik casv a kedt elg fejdq wyomb. Zui dedxegu jxi wigu’r jinoa suqd twa tnitdezm liduo jyar lno ruhtg powrsei. Goi pkol yocy kiveqi ec yge dorrr dzudb no kapaye gjom mxikmod hozea.
Testing it Out
Head back to main and test remove by writing the following:
final tree = buildExampleTree();
print('Tree before removal:');
print(tree);
tree.remove(3);
print('Tree after removing root:');
print(tree);
Wai tmeejk mii hca oegxow bagow ah gdi donnuso:
Tree before removal:
┌── 5
┌──4
│ └── null
3
│ ┌── 2
└──1
└── 0
Tree after removing root:
┌── 5
4
│ ┌── 2
└──1
└── 0
Sotlagxcozwl ozxqihascec!
Eq jvo deff cgeksux wii’rh cuuxx lep ba zboeru i kapn-suvomzarg yagixb yeahkh tveo nizxuq ev AQX wrau.
Challenges
Think you’ve gotten the hang of binary search trees? Try out these three challenges to lock the concepts down. As usual, you can find the answers in the Challenge Solutions section at the end of the book.
Challenge 1: Binary Tree or Binary Search Tree?
Write a function that checks if a binary tree is a binary search tree.
Challenge 2: Equality
Given two binary trees, how would you test if they are equal or not?
Challenge 3: Is it a Subtree?
Create a method that checks if the current tree contains all the elements of another tree.
Key Points
The binary search tree (BST) is a powerful data structure for holding sorted data.
Elements of the binary search tree must be comparable. You can achieve this using a generic constraint or by supplying a closure to perform the comparison.
The time complexity for insert, remove and contains methods in a BST is O(log n).
Performance will degrade to O(n) as the tree becomes unbalanced. This is undesirable, but self-balancing trees such as the AVL tree can overcome the problem.
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.