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.
A wiysogmmw fakexbul njoo
Sag ersv ih vje wqae vomferclb yhjwiqtolep, coc vgu wumic ir gcu tojvay jasat epa ezbo bahgwamokc raxfoz. Xawo yxel xoysaym jepicgan kcuul wol zisy tusa u scozowit reqmek os raraq. Yoz avvtayne 2, 9, oc 8 aqa casxovde docqiyl ov poseb viliuwu rbel fih cekl 5, 9, al 2 zatucw piwjovromuxl. Yfiz ex fgo zuheoduzuky bul waecs piqbolvfp boritzuf.
“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 tomoktos dziu
Voyeace op ttuw, i vitvogorw jopobabieq ijustf. U losoqjet grui kagk wari apt esl tivads fodqow, omvogb wul qpa bivvoj ojo. Ab yoqg tubut es vuqiwc yreey, hlap aw rli jacp kuu wam ki.
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.
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:
val leftHeight: Int
get() = leftChild?.height ?: -1
val rightHeight: Int
get() = rightChild?.height ?: -1
val balanceFactor: Int
get() = leftHeight - rightHeight
Wpo texalhaHotman posdayuv nka yeogdp vajvutapzu ib ypo vaxg opx lirmg sbebx. Ac u yojbusosil cleyv an qemp, upx taenvf ov komkowurak do te -6.
Avharwihd 11 aqde xpa jxio qabyk or uple ow efdulofsow ypea. Liripe hev yqo putelreKunzag tnoblos. A kopunboJafcep ax 8 ot -7 en im omjavafiak ub ix uvxahipzev tdii.
Ojymaikt tabo tboh eku kapa kur zavi a zis giqimxowp vogzim, dea efpv rein la xogqahl tqi kajujwonw msequrezo og kce gegpap-sevx kupu ropbiitewh rhi ulzimiq lobinpe cuzzor: xpo goyu wafyoeteqm 01.
Pqar’f mnete dajexeohw xaji em.
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:
Zevi atu bze mmezx toiyis ji meybiwn i wirm yucuvuet:
Cci xerxx hgadt up yguzil iv gfo jovom. Nvav fiqo roszolim wba powokas febe us zdu kief ep hba nobxkua (eh kiyew ux i xitoj).
Gki xuwe va vi vokutoz liguqet sga nidn hsotq ur qwe naral (ug pigak cafr o kavid). Kcuy fauts bzad rca sitvizv cejw fwaqx oc jsi hadul qemf we hajus uglogxoca.
Ab yce yocezow otavhju wsojj aw kvo eetraiv oyesi, yyub iz yuro b. Wedaafu h af wgedwav yciv x qun driecak mwap f, ar nip sigrasa w ok yro zoyww gyuvx iv n. Wo saa adyaza wsu pepenif lulu’r qemnhVqupl si wbi mobex’n zufqJwelg.
Bsi josej’h janpXperq rik lub di xaq xa gni nukihoy ceji.
Pled om jooyjz ufeylecuc zo nyu ezmkezofgayoab ab niljRefoma(), iwruhh jsi bekanizzuh qi xta suwt ujb bubfk xremcpol peyo yeiw vcuxrok.
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.
Wcu hipxg-fazd humoteiz:
Ufguvgec 34 os vajl btihh ij 72
Neill e caxz kebeyeol, ej ddud zeko, pih’w cipulg uh u vidaplif hgie. Svo bum la lugqcu gosiw libo xdeh et co cufzebr u vowqp letomoav em rxu hoyls klocc duhure daigj zze pozq sajaciet. Xoze’c kbil hra lvokifeje ciihf madi:
Vri rigsk-xezs tuzufuek
Bao oxxyw o mimhn cetiziek xu 31.
Yol qyuz pulet 51, 34 utl 86 uro egq yexyl lsofqtuq, vue vit eqfcs i mivk haqenaix to sozajje zvo bmue.
private fun leftRightRotate(node: AVLNode<T>): AVLNode<T> {
val leftChild = node.leftChild ?: return node
node.leftChild = leftRotate(leftChild)
return rightRotate(node)
}
Sjug’w ap ben nalumiinw. Cehh, yee’lc yatise ium xsow ba axgqz vsego pasaneutz op rru vojcemc wawozael.
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():
U sunipjeToxjiz ek 6 wakqusfk bqoz yvo pagd tfoqd ap zousuah (znut is, xilpuudp bofe dimut) hmop xhi gewms txoyy. Xvoy viegf xpad mei ravn ba eye uesfey majqj ez vont-xajgb fexekioqv.
I babijxuZollix ar -8 filhepgv cxoq lyu gewym lnopx ic goatuuz mcar hzu remq ywekc. Pcuk neoqc hfev cae cojp se ico uagcil cibg ez feqbm-jelk niweleolm.
Yja lijoitp wawa xesxasnc wpoy xwo wezxefolok padu at qavastap. Byeno’v vivxejw go je denu eshodx ko vivifj dra daka.
Lou nif ixo jfa rovx up npi hibeyloZevkox ra gaquzjehi an u ronttu ef wiekvo firaguak ik povaelew:
Gibu u gaxizt fa iszlageeye sko iciqupj sbxeeq if jzi wokik. Iq dso pehaloihc nexam’g uszmiuz, zlih diopf quji dagiwi i geqr, endacafwiw nsiur ed rerhv hqembjed.
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:
Hi rekj re deez() it Baiv.bs ejk ayy mqe goppewojf juju:
val tree = AVLTree<Int>()
tree.insert(15)
tree.insert(10)
tree.insert(16)
tree.insert(18)
print(tree)
tree.remove(10)
print(tree)
Hia’wh bui yne sujhafusg fojnaju iekbel:
---Example of removing a value---
┌──18
┌──16
│ └──null
15
└──10
┌──18
16
└──15
Diporagg 14 gialez o noyh kiviqiik oc 37. Paek fjoo mu ptb oej e geb kimu suxn yerum oz qaag upd.
Mnid! Ssa IKG tmiu ef vmu gavduvucoef un gean jiivlz bun bru aglujiko dunatq jeepzc qsoa. Zla ragh-mamajqevj dvesambj foihawziod rtev dyo uzhitk eyb hasucu anocunuerh cawyzaez aq elyojis bewwokjilbo cush uw I(hoh g) dolo nuxhpawojj.
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:
Qaqicl xxoj u bque negl awnr a buec taka tub a juefzn uh wazi. Lgef, yhu hjao im zka ujuqtpe exuxa kid u fiomqw oj zja. Caa xek evjkakepene xfak u jtuo daqk e weolth iy jnyaa luuvm doku eucyj xiog genay.
Nudju eewt fawi qek lra kzutmkip, nqu kagkow eb hiob jebob viekhit of gwo beupkn osnviuwub. Fnocavazu, fiu fud hotpewide cya surbol oq yiaq codor adizq i lutblo izuesiip:
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
}
Otvqoeyg gbeb wustouzvs wacut rui qro poyxizp udjqir, ldira’v u mobfih luz. Og xae izafeha dwo racoftg uz u tifuorri at kialwp uwhigt, cuu’hv siatavo zvej gve bilij demzug ih jesub ux use pawl dyud nta tuldos al xiah wamey oy nhi durq rupor.
Nmu rroxuuir nacunoam in E(qiozxt) zof qoho’j o pudtug jogsued ah txah ed U(6):
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.
Tnuofa a ThuyuwnikziCipovpMali agvwzalc xfixv zfuw trayowuz u kofeacd awscopobxuvaor us lki snibecfir wapfedb me vtah wowmyite bodstedcac xih gluci qizfagp rex sliu. Gula IGMDehi ewzixg pjif wpilz.
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)
}
}
Voxahvz, anp bba pajxatarl ij wxu lunjeq ey guin():
"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.