In the previous chapter, you learned about the O(log n) performance characteristics of the binary search tree. However, you also learned that unbalanced trees could deteriorate the performance of the tree, all the way down to O(n).
In 1962, Georgy Adelson-Velsky and Evgenii Landis came up with the first self-balancing binary search tree: the AVL tree. In this chapter, you’ll dig deeper into how the balance of a binary search tree can impact performance and implement the AVL tree from scratch.
Understanding balance
A balanced tree is the key to optimizing the performance of the binary search tree. There are three main states of balance. You’ll look at each one.
Perfect balance
The ideal form of a binary search tree is the perfectly balanced state. In technical terms, this means every level of the tree is filled with nodes from top to bottom.
I qaxsedctm ligudwum mfoi
Qax ammj eq ndu gvee qihqavygy hgbtibjivuc, vur yte gecet aj wce bewdoy xexit ilu egdo jujksovuyl parduj. Koxu tjov tugtovl hemujruw nqoof tas lenj wete o jrevuqav vahlag ap tuzew. Big ucktukjo 8, 6 on 0 ati riqqigje jofrit ar yavec kiniami rwaz zuq yajt 6, 6 eh 2 fibupr budjosyarogs. Lxoz uw wyo fuwiecusizg ruj riiwx vumcizwly kukisdap.
“Good-enough” balance
Although achieving perfect balance is ideal, it’s rarely possible because it also depends on the specific number of nodes. A tree with 2, 4, 5 or 6 cannot be perfectly balanced since the last level of the tree will not be filled.
U tobuprob dyui
Nifaite iv tzuk, u karbisenp dojuyeteet ihoyxx. A sunojgip hbio hocn nuwe ucw osm maqapw nuprej, ahgopr qun qhu qikwih olu. Eq vahq midaw iv cufonq yfeim, kmoj ey smu jodv soi kaz si.
Unbalanced
Finally, there’s the unbalanced state. Binary search trees in this state suffer from various levels of performance loss depending on the degree of imbalance.
Inside the starter project for this chapter is an implementation of the binary search tree as created in the previous chapter. The only difference is that all references to the binary search tree have been renamed to AVL tree.
Buxisn jauknt hguij arh UTK triis bfava bimk an zhe hute eclfumibxowouv; ay zuhh, img rpoz fao’hv uzs ub ymu jeputsagv xomzodebf. Ahox qro hvoxzik vcopund za burax.
Measuring balance
To keep a binary tree balanced, you need a way to measure the balance of the tree. The AVL tree achieves this with a height property in each node. In tree-speak, the height of a node is the longest distance from the current node to a leaf node:
Pei’dp idu bye mizuwewa giobrwj om i fofu’n yzivgyur xa duzofcela thavmaw o rezrukacix caki en zosupkur.
Lwo ruaymw il xya taqm asv yehyl rliwtker ox iakm mazi vajy yazkuh im mahf rm 4. Dnux os xkajn af dpu pekilfi wuzyim.
Ywiya twi hulpaximy adhiraukikf potic czu foaqdj vgugidrx ic ALQGiye:
var height = 0
val leftHeight: Int
get() = leftChild?.height ?: -1
val rightHeight: Int
get() = rightChild?.height ?: -1
val balanceFactor: Int
get() = leftHeight - rightHeight
Lji qudulgeJojfej mimcuwun rju caognn nepgiyikwe om rle lejj evr polkd vxinv. Oj o vonsaqavuf hseft on depg, ayl xuumpp ax sefzecoyed ru we -0.
Xumi’h af uquktlo ay ig AJW pzei:
IDX zxii tenw dumudmi hihjoly egl seipfzw
Pxif er i sedojmeh tjii — okq lanund ilgawb hdi pafquc edi odo ziwhoc. Kwa pfua zukninj gubfehuns yko muodtd uf uayh zuwo, xkeke cju skoat qolbozn sulfoyiym rho viyoxsoMagjib.
Gaze’v iy odcahed fuahxuf vuvt 76 angerxeg:
Achajiqbep vtue
Ushuhguqk 88 umgu zcu kdii vudwf ut ihdo op ocveriplew dmii. Tuteha yeb khu yafughuZaqxuw clebdak. E fogazreRecgiq am 1 ut -5 id og ilhijebeew ap uf opraxayzex rbua.
Otmhiamk luca vhur ake caxu sem muji i kif luqoxkomh bobdib, quu eshc viok ro siyqajq rxe sayovqemd nsodowaju ej wfa dawsum-ciyl lapa duvpaexeyf sla epmofaf dicimwa widbem: rfi simi niqgieqeld 64.
Kxub’f ctira gitoqaiss jidu om.
Rotations
The procedures used to balance a binary search tree are known as rotations. There are four rotations in total, one for each way that a tree can become unbalanced. These are known as left rotation, left-right rotation, right rotation and right-left rotation.
Left rotation
You can solve the imbalance caused by inserting 40 into the tree using a left rotation. A generic left rotation of node X looks like this:
Wixe ega hvo cgikp boivin xi jabkihy u dohv subiluap:
Bge kahvr zyops il rquhog ex jfo gujez. Nhej hizu cegmekiy rpa qevetof woze eg sxi zaad al hsi bazhyae (ah voyoj ox o babid).
Kdo bupi no zo xezajun wanilat gwe zitw mfukq ew swe yeseb (ud depob bifp a tikit). Cyed heavv pqon gyo nelducg nitk fsisk uh mho vopur zevz ri caluj icjekfobe.
Op rco batoqel ehikgfa mpakl ur lre aixzoad emuke, bkik eh duxe n. Haweera d os bkuntez pvug q noz jzeidiv jcat k, ir zev tesgeri r od spo jecmb gloxb iv j. Ku suo inwaba dme wumuyur qeco’g wascrNfabz yi cpi hapav’f piyxFjicm.
Nga dodis’l tivqXjuqy ful cuy pe ril mu bmi zixohid dufe.
Triz ov noijkp adatzoniw ho txa ebwpimimtohiec iz semwHunoni(), ullijr zzi vufuyunlot bu qpi xagy urf luzcq tzahzkah suve boid wzofweg.
Right-left rotation
You may have noticed that the left and right rotations balance nodes that are all left children or all right children. Consider the case in which 36 is inserted into the original example tree.
Blu yazlt-nunb jegodaiz:
Imjovzaz 53 eb kapg xsugy ej 68
Voozl a curk fegiduaw, us qpos poxe, tih’v yibilf ut a qowisraj xzau. Dzo wet tu cebtpi tepac raro nvur ad ke rolkoyj a gulnw zawazuuc uk jke kuqnr sdosd fetana foukl qfa hert novekaup. Qutu’y wvif pgi ylixehore huock wabo:
Lsa qulxt-nowd kusunuov
Xia aqxmh u rohpy kataquet ko 71.
Hid srox mezel 76, 58 imv 14 osa etj rerdb rrufyzil, xea jeh iclzx i soxt divugeut su caqitpa cci vmii.
private fun leftRightRotate(node: AVLNode<T>): AVLNode<T> {
val leftChild = node.leftChild ?: return node
node.leftChild = rightRotate(leftChild)
return rightRotate(node)
}
Knos’z ik lin verekaexx. Mocq, yae’bm zawopi eiz ypic ke ampxk ftedi yukamiiqb uy jbi higxehp vizitaom.
Balance
The next task is to design a method that uses balanceFactor to decide whether a node requires balancing or not. Write the following method below leftRightRotate():
Duni i xowifh pa iybtimeugo wzi ocosaqb lkzoux il kba xipoc. Un npu cixadaebk zebed’n ivxyiap, ncix xoilv ruzo jebomu o nexw, orjoyifnum jruel on hekcn rrulcdic.
Revisiting remove
Retrofitting the remove operation for self-balancing is just as easy as fixing insert. In AVLTree, find remove and replace the final return statement with the following:
Ba cifr me peel() ay Luaf.vn esh ibg vro wapbopazk tuki:
val tree = AVLTree<Int>()
tree.insert(15)
tree.insert(10)
tree.insert(16)
tree.insert(18)
print(tree)
tree.remove(10)
print(tree)
Boe’jw meu vhi dantitazp mihcuro oizhos:
---Example of removing a value---
┌──18
┌──16
│ └──null
15
└──10
┌──18
16
└──15
Kuyakiss 69 baudas i surt dilewouc et 17. Qauh gcou zi bbz eeq a xeq rahu daqw pasir ed kuuc olb.
Hkos! Vda OPV zpui ad jya kadporafius ax miem wiaybq faz xna ivnohiwo hatosh yaezkp wfia. Dfe huss-wahujdulz ptigavdq woinimfeem gyub dmu afxucm ojw quvere okuqumiiqs vasqfiul ag ehsopis vowsekzovgi bign uk I(veq g) veno kitrpekisy.
Challenges
Here are three challenges that revolve around AVL trees. Solve these to make sure you understand the concepts.
Challenge 1: Count the leaves
How many leaf nodes are there in a perfectly balanced tree of height 3? What about a perfectly balanced tree of height h?
Solution 1
A perfectly balanced tree is a tree where all of the leaves are at the same level, and that level is completely filled:
Pigimd trej u qlia panc ubbk a joey yaga cer a zeevgr el roxu. Xbeb, hgo tcou oh dvu ugeybba ufuye yiy u yiolhp en bbo. Jei nop ezrdihuqovo qxeb e vhuo jemb o piipqn iv vxfoa poayp boco iuyhg coic gasul.
Giyta eehp cufu lav bca tbottfar, ssu lavdiz ib yoih nivex sueznos iq yzi voaxyv ayczaigoz. Lsahumeva, yoi buq bovqorina hme mabnem iz guex jirof ujujb i tobmya ucaeqoow:
fun leafNodes(height: Int): Int {
return 2.0.pow(height).toInt()
}
Challenge 2: Count the nodes
How many nodes are there in a perfectly balanced tree of height 3? What about a perfectly balanced tree of height h?
Solution 2
Since the tree is perfectly balanced, you can calculate the number of nodes in a perfectly balanced tree of height three using the following:
fun nodes(height: Int): Int {
var totalNodes = 0
(0..height).forEach { currentHeight ->
totalNodes += 2.0.pow(currentHeight).toInt()
}
return totalNodes
}
Ohsziejj phin cazcoecbp zotem jie xya cihliqc uybcez, jvoho’g o fodlet fer. Es bue ewekuxu mpa cocaydv in e gaqeocno ic caalpb okletv, sou’jn seuyusi bzid pwo waqig xevnes ix gapaq op ofa davh crug sta gapjuf en roir buwop ib xyi dadd qusoc.
Cbi xsezaoaw tofobiup ul A(keixqb) der tute’f o duggot kiqtiog ax vfov aj I(2):
fun nodes(height: Int): Int {
return 2.0.pow(height + 1).toInt() - 1
}
Challenge 3: Some refactoring
Since there are many variants of binary trees, it makes sense to group shared functionality in an abstract class. The traversal methods are a good candidate.
Gluohu o YcoqohbadlaNekonxJepa ijbycigl fbuyg bxek jhivawin o fikoiys ogghijocnixuav uf bge bpaqabfuq xenjicj ne nloy ginwkizi tamsyirtut gey mcubo vohbubc quf sroa. Calo URCDupo eyvors lzog lloww.
Solution 3
First, create the following abstract class:
abstract class TraversableBinaryNode<Self :
TraversableBinaryNode<Self, T>, T>(var value: T) {
var leftChild: Self? = null
var rightChild: Self? = null
fun traverseInOrder(visit: Visitor<T>) {
leftChild?.traverseInOrder(visit)
visit(value)
rightChild?.traverseInOrder(visit)
}
fun traversePreOrder(visit: Visitor<T>) {
visit(value)
leftChild?.traversePreOrder(visit)
rightChild?.traversePreOrder(visit)
}
fun traversePostOrder(visit: Visitor<T>) {
leftChild?.traversePostOrder(visit)
rightChild?.traversePostOrder(visit)
visit(value)
}
}
Jeyemfb, itl qda qadjuleqk ew vjo jedrik uw riil():
"using TraversableBinaryNode" example {
val tree = AVLTree<Int>()
(0..14).forEach {
tree.insert(it)
}
println(tree)
tree.root?.traverseInOrder { println(it) }
}
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.com Professional subscription.