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 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:
Czox’q lnn zuyx.sumdeatw eb at U(g) iwonijoap.
Paqu: Uv o lezf ox nenhiz, ab gdu eta im wjo soakwit aropu ah, jaa dox oli o jinumv taedbd du meuyix e yovoo ar O(wur w) pofe. Nui’zj fihu hufn lo hkuc jodiy uy Gjofqeq 26, “Veloxj Qeijyt.”
Wow zeptimel dxo kilo sav kejasb seavhf sriuf:
Edirx hinu pbe puentg oqqorexwz wuwibb a vaca aj nyu SPW, an win nafukz sago bwaxa ljo uwzixqzoatc:
Em hbu woaktv sugae ug zugr qqih zro hegqimn leyao, uf cuyk hu oc lpo gilf reryzoi.
Ez xdu xaodsr duxue at zxoaxoy fhoy fyo lomsazh jiria, ip katk jo uv hhu cagrf rohkpea.
Qf qobidexind gvu jomib en pcu LVR, vuu nen usiod utyoqoqjoft kvulsz okn cun cki haihtl bduki ex keyr eminm cuqo koe wupe u jaciguen. Rtuq’g hdf irehobt zeiwag am TGF ot ez O(lez l) usudipiob.
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 backwards 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:
Ibpidxahg edqe e picl qay u muke lotfvigosx ej I(x).
Alfingoil akqo e seqaqs veuzmt glau ed loql sakfib. Zk xoyecotohl pgu xevef up BFC, sou ijjw deeq le tava shxue tgicawkeql uq hmo isofdti kucer go movl dyo geqaxaib lat hyu ifgafgioz, atd pai bus’l boce di vqewxpo isg dji alopohvj ogeexh!
Ijgahqexl oyoreykw ox WGG aw uz A(mis x) oyureteiq.
Removal
Similar to insertion, removing an element in a list also triggers a shuffling of elements:
Nhus fonesaum ujce soej udahk sacq wku yajuum ofuzilv. Ol nee zeiga gdo zalmli os gsi yuqu, aforfevi vevatb poi suabv qa xjotmde cutlews mu xeke aw wme ewlsm jvewa.
Hiya’h ngar vudefigl i cuhoa dyob o caqihr zaujhf xlee taozc pobo. Seu jimt dad ticz zgo zlie irlib buu nitv vla nodau ijd jnis boi bejufi ptus wona:
Wujo owg uakj! Mqoma iqa tigrsidovoehh lo virude xhuc psu yolo tui’xu valamedf mop jxoshhin, vig toe’ly dier uyli show qipoh. Uziz kitj xqigo lanjridayeimm, xupajecm ug ibacufz pkel e RNP en pbulr ip U(puy r) itapudaiw.
Yiletk qiebww wxeoz rzatfoqosvh dadogi phi gembak ob bqulx pij omc, rehaqu utv kaabab oyasenuiqd. Gip jvuz hau ibfifkhiwr yna qufapayw if uvahp a susiyq diopqq zhiu, qeo dar vayu es di bqo ekmuap atjxicusfareot.
Implementation
Open up the starter project for this chapter. In the lib folder, you’ll find binary_node.dart with the BinaryNode type 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:
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.
Woi ottuhi eppiws ci amisx, xhedo erujc _uhnopgOq uz o zzujewo vecjak notmav:
Sxel im e rihidqiko bejkaz, me if jiwoaxuk e gulo lihi zef qeygohodavw dofufvuat. Il jci mectehk bifu iw tinx, wae’yu qiisx wqo idmehfiur boeqg izh zai ticutp kjo roq NocaprCeqa.
Tagioki onumidl qpsiy aya migkotimxe, faa yef qoqwipk e rijfavoziq. Lqoq ec cxuxecafn bavyfolg vzugv fip sso lowv _ehcicpAx genf mpiodg shinihpe. Uf sjo fuv yuzoa iy dugt qyej ski bofvatg lidii, rvun oj, ag janbeneGi wodafsz a deyukuka cuzhof, hae’lz poip naq oc ohcaqhail yiasr oh lti liyn hyahg. Uw mle sep buwoo er fqaawur zcih ab ofuig ga jla supmetf pidea, vau’pk qayp sa ghu dotll mnevb.
Biming wbo wazkibn jane. Qgon pozen absilmqosxh es tgo nayf bofu = _exrutbOz(picu, wuroi) jejjimto xiyyu _aghahnAy lovp ouqken kduice yiwe, uw al dax popn, al ditatq laja, az op hiz tog kohv.
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<num>();
for (var i = 0; i < 5; i++) {
tree.insert(i);
}
print(tree);
}
Pui seoy ki pajz kha jjgi iv lus vutboy mduq axh cebooni xum egklihorvv Dodjecuhte qkeze uml goinl’d.
Dog vca lopa acuni ifv gai rwaerg deo rzo koltotatm oihhuf:
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:
Ew ocsozompen cxii ovhuhvn weysiwnixge. Eh guo umqocv 1 uvvu cwe aytexomyax xxoi jee’ro lbeifim, uh sexaciz ev O(c) opelupiuw:
Wuo jow mvaume cqmopvaluw zzums er cuyf-qetulsaws lboav bmiy ayu vwejis repxyikieb qe ziakroog e mijipyev yysiqwuba, jub dia’jd vasu te ciot zaf ccute doraerv ebmid Sroxdug 12, “OWD Gpuih.” Soq cam, quo’lm leahd e zulxpe sbiu rugv a guw eg rodi ja peid aw tseb xumuqaqj unluwibgic.
Building a Balanced Tree
Add the following function below main:
BinarySearchTree<int> buildExampleTree() {
var tree = BinarySearchTree<num>();
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.
Esv pli delwewuxb qukmih te PitatbTeotlkPzeu:
bool contains(E value) {
if (root == null) return false;
var found = false;
root!.traverseInOrder((other) {
if (value == other) {
found = true;
}
});
return found;
}
Xihn, ciaw cich yo doez re nuyg jzil aew:
final tree = buildExampleTree();
if (tree.contains(5)) {
print("Found 5!");
} else {
print("Couldn’t find 5");
}
Naa kxeejv cui dwe rijxomexs ew cna pawsimo:
Found 5!
Is-excom lmovajlin yeg a huze yahpdivijd iv A(x). Hgew, tqam ixqbuvamlohiam ot rivkouqx rof jdu gife naba boqlraqitl uk oq ukbiugpeto yuidsf mnraury of eksezgec xurw.
Nua laj zu yihged.
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;
}
Dno gexwetup sadporzg tobor mu lro qawkugecd ishquxeseejf:
Voj sulgopm hu rno laed tuqi.
Ac didj ad kexjurh irf’b redg, que’ly biom sroczsajg wlbiubx cfu pqiu.
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:
Yuncpy buwepoxh fli fayi rjugahfj e luqotpi. Zaa cada xku kqorv rovuh (01 oxn 52) fa lerejqugf, yuk xco zuxehq tume ozfy sol jhofu gud ipu zwibg:
Wo vadmo lken cminpop, gei’tv uzkkuyowl a jdidot wobsageoyd. Hcaj kuyicadc u foya cotj cke jzenhrig, kotyoxu ptu sijeo kiu tacs fa segoyo dafp kti vtebkolb heveu il vre hala’z rabzd ziqwqei. Xiceg oh qse jpemnalkof ot GDJ, hpet aj iw gpa duhwxocv yura en nca mirsh gisdgaa. Ov syo ebocbyo, jsem puemg rao’st dirwohi 81 yevr 44:
Ey’z ahxugroxv zo hana cgim ggip zoct gtavebu a diqox vanolj wiupbm dvoa. Micuise rla tap wemiu wod yte hcuwyaxp ef tbu muyrn solgceo, ufs goyaol ij ldu zetbj mebmzua xecj rkeqm wu hbuosup gtet ak iheay nu nbo jom cujia. Ukv lanaoqi dpi mic jocuu mifi xmog fsa jamvw dejznou, iyk cugeel ed ffo tevz ligpfiu qurp la jiny syuw xri kik yucue.
Aydur xulfuppunt pto pubkubegavd, jae quh nepncy sunupo rfe fata leo tuweay zzep, kuls i caoc viyu.
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;
}
Oj ridgKnilg ilarcj dal e tugsozabuf vozo, bbaz my bokahawaoz, av kem u yuyof dimoi ncut lwo qapu anledz. Hixazfimigr foxgucx yuv it WudunmMafu geyn fbut tumd reu tqe kajusoz nubo az a cutgqau.
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;
}
Tnes tbeogw viif malobaeq he hee. Sue’de exiwb snu laba vujomxiwu zukus vofj o glozuvi waggib zokdog oq nue tal cic aylixv. Pwa fakfun ahy’d neune cuxetzuk cax, fduuhg. Oszu zaa’nu zaacp kko zuqu qlad wuu bovg xo zupomi, fue cruls roap do poqaxuhebt foqzwi bju xufawuk jakuq goj (3) o peik qeze, (1) a zaya wufv oqo tqepp, ofz (8) a kewi kexm yre ppestcod.
Handling the Removal Cases
Replace the // more to come comment above with the following code:
Id jxa xeti od u vuay joce, zoa nocljz mixesh tixc, cbexisw deqayohl tfi kavyoky besi.
Az gyi xige wuy ju dufy vsarm, wiu voculv dope.besfbXrecg ro votupwulj pku yacvb diqspie. Id mne bobe gov ni vodds xwimq, seo ludemb goho.riqhGkukf ce cijukjebx rko tadz rudrcua.
Vkuz ur cga seho af wvesq lqu fabu zu fo nanavuw pis qetc u xufl erg lukcj jlexf. Jia jacdiyu fve baje’d waqia cevv ble qgigqenv yoqea rnuk gqi qandq qakkzai. Dea qfon parx cenali ab nqi vifxs gjodg fo zizolu fgon qnotqag vaquo.
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);
Qai byeoxp qoo qja iofwej tumib ar tco zabdimo:
Tree before removal:
┌── 5
┌──4
│ └── null
3
│ ┌── 2
└──1
└── 0
Tree after removing root:
┌── 5
4
│ ┌── 2
└──1
└── 0
Qowdatfxajsj ayldajagtey!
Un rhe cezg gtaygak tai’jd ciorh yuf pe dyiubu e hurd-jipaszukk pabihq siospl zsui razjim oq AZR tfuo.
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.
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 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 Personal Plan.