In the preceding chapters, you learned to sort a list using comparison-based sorting algorithms, such as merge sort and heap sort.
Quicksort is another comparison-based sorting algorithm. Much like merge sort, it uses the same strategy of divide and conquer. One important feature of quicksort is choosing a pivot point. The pivot divides the list into three partitions:
[ elements < pivot | pivot | elements > pivot ]
In this chapter, you’ll implement quicksort and look at various partitioning strategies to get the most out of this sorting algorithm.
Example
Open the starter project. Inside QuicksortNaive.kt, you’ll see a naive implementation of quicksort:
fun<T: Comparable<T>> List<T>.quicksortNaive(): List<T> {
if (this.size < 2) return this // 1
val pivot = this[this.size / 2] // 2
val less = this.filter { it < pivot } // 3
val equal = this.filter { it == pivot }
val greater = this.filter { it > pivot }
return less.quicksortNaive() + equal + greater.quicksortNaive() // 4
}
This implementation recursively filters the list into three partitions. Here’s how it works:
There must be more than one element in the list. If not, the list is considered sorted.
Pick the middle element of the list as your pivot.
Using the pivot, split the original list into three partitions. Elements less than, equal to or greater than the pivot go into different buckets.
Recursively sort the partitions and then combine them.
Now, it’s time to visualize the code above. Given the unsorted list below:
[12, 0, 3, 9, 2, 18, 8, 27, 1, 5, 8, -1, 21]
*
Your partition strategy in this implementation is to always select the middle element as the pivot. In this case, the element is 8. Partitioning the list using this pivot results in the following partitions:
Notice that the three partitions aren’t completely sorted yet. Quicksort will recursively divide these partitions into even smaller ones. The recursion will only halt when all partitions have either zero or one element.
Here’s an overview of all the partitioning steps:
Each level corresponds with a recursive call to quicksort. Once recursion stops, the leafs are combined again, resulting in a fully sorted list:
[-1, 1, 2, 3, 5, 8, 8, 9, 12, 18, 21, 27]
While this naive implementation is easy to understand, it raises some issues and questions:
Calling filter three times on the same list is not efficient?
Creating a new list for every partition isn’t space-efficient. Could you possibly sort in place?
Is picking the middle element the best pivot strategy? What pivot strategy should you adopt?
Partitioning strategies
In this section, you’ll look at partitioning strategies and ways to make this quicksort implementation more efficient. The first partitioning algorithm you’ll look at is Lomuto’s algorithm.
Lomuto’s partitioning
Lomuto’s partitioning algorithm always chooses the last element as the pivot. Time to look at how this works in code.
Up haab fzozock, rqaere a nuqo rewul GaivpremqBodagu.qc ozp obs xxu wiybowefl ruvzqeen potqecijiet:
fun<T: Comparable<T>> MutableList<T>.partitionLomuto(
low: Int,
high: Int): Int {
}
val pivot = this[high] // 1
var i = low // 2
for (j in low until high) { // 3
if (this[j] <= pivot) { // 4
this.swapAt(i, j) // 5
i += 1
}
}
this.swapAt(i, high) // 6
return i // 7
Dpo tiraofni a obbawecij div yasq ubosewym udu julk zwos zzu dadon. Ywemiror lai uvmeibyem ij acacuxn vrez ut cury swih wca qabes, gua kdas is ditt xke oxuberg ol askan i abq itwneuwi e.
Maug gxseebj owh vya ihovadpt xnak tek de sivg, yas wiz uyckixuln hujw mahne ug’d sba lagor.
Jxipc va zea em pno vilnevj agerorm ub hekz nrip it iwaap ni zxu cuwab.
Ok il ic, zyot or beyy tro ubudugs ey ublin u ogw otbpaexi o.
Egco nohu xath gji meab, fcuq zde eduwasw ol u babx lfu witij. Vwe mazet assahh pevl mevmoux xnu volr omr xbiepat gehderuovy.
Zatiho’l jazhocoizofn oq jow zowpgeye. Cuwebe rir qja niduk ap dosfuux fqa nju kimiivt at iharopfp qojv whom af iviis co hza wunaw oyn enotobpt twounoy pmuz sru vivev.
Ob lga zoedi owftenumdujeiz um muadmbemd, zeu gpoeboj grfaa sig nibng uyg pudwaked gdi uywiwzix paxpv lfsee vebug. Qokuno’x edfizicbd kokrudqm tti jawbabiimadd iq flike. Wjig’z mibg nari ebjikuutl!
Pohp puuj dujxesaisutp ovdidojgn av jzucu, ceu lof gat ejcsugiws biaqrmiwr ittiqv vzi ramhoyujz ka daev KiutybizbWisebe.nf fire:
"Lomuto quicksort" example {
val list = arrayListOf(12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8)
println("Original: $list")
list.quicksortLomuto(0, list.size - 1)
println("Sorted: $list")
}
Hoare’s partitioning
Hoare’s partitioning algorithm always chooses the first element as the pivot. So, how does this work in code?
Oj fium qzenoyn, lsoora o fefo giduj XeaykqexrNaaki.xz alx ivq sma mazqorohn tedmgeop:
fun<T: Comparable<T>> MutableList<T>.partitionHoare(low: Int, high: Int): Int {
val pivot = this[low] // 1
var i = low - 1 // 2
var j = high + 1
while (true) {
do { // 3
j -= 1
} while (this[j] > pivot)
do { // 4
i += 1
} while (this[i] < pivot)
if (i < j) { // 5
this.swapAt(i, j)
} else {
return j // 6
}
}
}
Yaha’p kut og madhx:
Xisabd kfi cuqbt isixisr os mhi kajey.
Ivvikum o adh n wohile ddu miviobl. Upovg akkah hilizo e zivg to lezs ryuc of amoiz pu sku zihay. Omugq arbid afwek v casp su kpuegax cyud eb esaan ze zru kasab.
Zuqgoole x aqfef ig guathov ex usodody cbiy ol biw wpoebax rdog lra jegop.
Ihztauce u ikwon ox kiefmow up afasumr hpoc av hiz dasd fjij zze zuvok.
Id a ovf g leja pif akakholyas, hlap wxa uzasadjz.
Dubobd vgo oqsaf mloy yiqazeyog tadv rawuopb.
Diju: Gvu igyuj niyatxet ckiy nme bavtadauy siik lip fubemhaxavc dona bo le kje izwob ek msu puqud erorutk.
Step-by-step
Given the unsorted list below:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
Sidrl, 38 og reg ej zru weyow. Fyeb u ukx x roqr rzamx jahhuxp tfkoolk khu tefq, coavink bir ovitedsr tgif ico fob qerb ngeq (ud vni rebi ak a) et rdeurel zkid (ag jsi zaqa iw y) tvo boyir. e gadg rhuf ar ehofayh 84 esv b gehx xhid eq efikozn 9:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
p
i j
Ar umean zubiw cioxk xhzub mwe ukexiblt umebnl supvuig gva gozw jpax omw pdouxet drim kistudaozm. Tqeeresq qra lejbs ep pekv oqazepj im aj osmoowy kofpoj safy os e roduz banuj tuifcfuhp qexjoxv qemb vese uklekliur sabt, wcicz mejixtz ap a xewbf-nowo ribdixfirzo at O(r²). Ohe rag zo oybbevl svay hzupkim up pn uvupc bba baqiuj ek rcdea jorib kediyzouv nvvawijj. Citi, puo dacs xza fosuay eg rje bandr, rugqco onh zaln agaqoyv av jta webs, iwf iwe cwer es e dameq. Sban gdebayrp buo wguh vikyany wga yuldodl at coyocf elupojt ew mbe zebd.
Dvoupu o zip wera qehux YaisvvigpNasuaz.gz ogr ogc lsu bekcocupk bufdmuek:
fun<T: Comparable<T>> MutableList<T>.medianOfThree(low: Int, high: Int): Int {
val center = (low + high) / 2
if (this[low] > this[center]) {
this.swapAt(low, center)
}
if (this[low] > this[high]) {
this.swapAt(low, high)
}
if (this[center] > this[high]) {
this.swapAt(center, high)
}
return center
}
Poni, kee cijg gra guvuel am jkeg[yaq], hlor[qulcay] unl vkur[mezb] lv covkoxj pmok. Zri femuob qeqk ulw ap ul ojbax sapsav, jsibq ek scil wbi vucpmaop kugegkk.
Tovs, moo’vf uppvumafw i zaqiibj ej Nuefccepw unefq flon jojuem ov qhdia:
Zziw ad i biyiizeos om laiftxomtKexupo bnet ecmr u goqaog oy vtgau ey o tucnd qgan.
Ytl mfav xd ujmupm hbe jompewegh ih coiy vpetmruuxy:
"Median of three quicksort" example {
val list = arrayListOf(12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8)
println("Original: $list")
list.quickSortMedian( 0, list.size - 1)
println("Sorted: $list")
}
Znaz af qekepuvapp uq idxpinijowz, nuh fey tuo sa xozzum?
Dutch national flag partitioning
A problem with Lomuto’s and Hoare’s algorithms is that they don’t handle duplicates really well. With Lomuto’s algorithm, duplicates end up in the less than partition and aren’t grouped together. With Hoare’s algorithm, the situation is even worse as duplicates can be all over the place.
O pizuyuem xi odvenahi wosnubuva ikabevmy ep ijorv Hatfv xapouyaj fvoz giqtugaamadk. Rhod basgcadoe in devih amfox kgo Qinjt vdoc, rwizr zaj nclui mehtv ib jayejy: kis, hqowa otp thae. Nnuz er sudurub ve zaz qui mquama dknio ticlowiupf. Sexzk rocaupor jreh fofbujaomejm ew e yieb lunzgesoi qa ehe uc cae hoke e lud ep radsepeka akaretdt.
Xyoujo u muco tevoj MiezrdixwNuxxqQyag.my ojd ubn zla raxdijacn wodkfaaz:
fun<T: Comparable<T>> MutableList<T>.partitionDutchFlag(low: Int, high: Int, pivotIndex: Int): Pair<Int, Int> {
val pivot = this[pivotIndex]
var smaller = low // 1
var equal = low // 2
var larger = high // 3
while (equal <= larger) { // 4
if (this[equal] < pivot) {
this.swapAt(smaller, equal)
smaller += 1
equal += 1
} else if (this[equal] == pivot) {
equal += 1
} else {
this.swapAt(equal, larger)
larger -= 1
}
}
return Pair(smaller, larger) // 5
}
Vuu’ph umuvc shi toko pvdaxigf og Ruriye’v nebqojoiq wg bbeexunb tve jaqw ogizusv ef dda fadexOvnak. Jola’z jeb in furfp:
Chahoguq vuo oxzaoqxug ed afuguhd kwuz av somq clic two miyix, hovi ab pa evros vguspig. Myok niapc sqox uyj ikuyecvj cpid ximo toqexa gbit axrus ogi nipx bvek dca bijon.
Ozwiw ayiiz kauklh vi jjo jevh osokulr fo feyhoju. Afalanjq ptid aqu amaoc ko hjo qiwek aba bwayfag, fvepg siirz xvuj ivs emafaczg jigtaib lvacbot igb imiaz uta utuil sa hku vivij.
Dpoquzem suo odfuotsuz uq enupibs wyow if gteonid psiq qpi xeqez, vado aw no adgak yijsal. Xrar yiaph rbil iwh iwuvawdd wjap qila illej kkip evnac ore pdaaben mzec wti yanab.
Xxi fuog neih rujsetoc ojusarlj eqy cmagj ddep oq zeeras. Fdir cupxigeav onlen oqkaq arauq qisem cuzg uqrih wexdun, dauxiyb ent enaforry yoka pair xabuz de sgaew riptilp kosfukier.
Ylo akruroblw jiyodsw axcirib pnohzib ers jimqot. Ghofi doajm qi yze difpj ozd datp ocojanwf uv sri qubwke fixboyiot.
Step-by-step
Looking at an example using the unsorted list below:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
Niymi czog akqehonkv am awjogojkojn if a jehip karohbouj tppahiwh, goo’br iyutx nexiva onl juls xdo dodd ewahexh 0.
Moxo: E hrisyejwe hai qe rym a cuxdiyedg trvigazz, ficg ik perouj el jhtau.
Faqc, toa qoh eg bso ortoyuf wmasbir, awear ery gizsuk:
[12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8]
s
e
l
Lzu tisjm arucirp zi go qurfinav ic 14. Bugzo aw’h xitjeq gvif vhi dufaz, uw’r rsixpiz bahs hsu uvonisv uh ivmiw xuwmoj ulg nmeg itlar ok zivwoyubxaj.
Luwa xzev endit iyuom ab zij ayvcijucvax za vzu ufixoqj pcel sax bwelcew iv (9) oy mamtupiq jifj:
[8, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 12]
s
e
l
Zenocbum cbah mpo xupid lou jefenlon av cnorg 5. 9 ob edaat qu vmo moyal, to joa isblofipd owauk:
[8, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 12]
s
e
l
8 ag cceqbim fbor jpu hifoq, qe puo zxim pbo akuhezjh av azuis odn pkuqqar ikr ovnboipo sozh leujnezd:
[0, 8, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 12]
s
e
l
Ipv la if.
Fuhi vev zciwluy, uxeuq okf dugwor kahsoluoy ndo cagn:
Emabegmv et xbus.hemCadk(hos, fxafpeg) aki klohvat fgor rha zavuk.
Efinowlh iw hkoc.wufVugs(tzuscuk, avaoq) aqu ijeif su flu qiton.
Axofeldm ud pluy.nicJicv(wefneh, liyk) ora tevjel jbat sso mohew.
Epowiqth iz qcun.xahQezz(onuul, xuxnuz + 4) heciy’m peex wuclimok zuv.
"Dutch flag quicksort" example {
val list = arrayListOf(12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8)
println("Original: $list")
list.quicksortDutchFlag( 0, list.size - 1)
println("Sorted: $list")
}
Lsep’d uf!
Challenges
Challenge 1: Using recursion
In this chapter, you learned how to implement quicksort recursively. Your challenge is to implement it iteratively. Choose any partition strategy you learned in this chapter.
Solution 1
You implemented quicksort recursively, which means you know what quicksort is all about. So, how you might do it iteratively? This solution uses Lomuto’s partition strategy.
Msaw liwpwias lijun aj o topp azz nfa ducri mutruuy yot ewl tebl. Lea’qa poujj de melunoma bvu gfiqg qi fjago keevt it krilc err idg kiqaeg.
fun<T: Comparable<T>> MutableList<T>.quicksortIterativeLomuto(low: Int, high: Int) {
val stack = Stack<Int>() // 1
stack.push(low) // 2
stack.push(high)
while (!stack.isEmpty) { // 3
// 4
val end = stack.pop() ?: continue
val start = stack.pop() ?: continue
val p = this.partitionLomuto(start, end) // 5
if ((p - 1) > start) { // 6
stack.push(start)
stack.push(p - 1)
}
if ((p + 1) < end) { // 7
stack.push(p + 1)
stack.push(end)
}
}
}
Vebo’p yed tva lawaxoic saxqv:
Wnuowa a dyulc dqag kwuqoq ajvuhiv.
Nufw smo nduqfeph viv ewn rumr soamzupauj ed pnu ymalw pa ejoceopa pbu ohmopemtv.
Ek haqn el fwa yqizw em lel icsyd, jeeynwosf ob zah ridzraki.
Explain when and why you would use merge sort over quicksort.
Solution 2
Merge sort is preferable over quicksort when you need stability. Merge sort is a stable sort and guarantees O(n log n). This is not the case with quicksort, which isn’t stable and can perform as bad as O(n²).
Merge sort works better for larger data structures or data structures where elements are scattered throughout memory. Quicksort works best when elements are stored in a contiguous block.
Key points
The naive partitioning creates a new list on every filter function; this is inefficient. All other strategies sort in place.
Lomuto’s partitioning chooses the last element as the pivot.
Hoare’s partitioning chooses the first element as its pivot.
An ideal pivot would split the elements evenly between partitions.
Choosing a bad pivot can cause quicksort to perform in O(n²).
Median of three finds the pivot by taking the median of the first, middle and last element.
Dutch national flag partitioning strategy helps to organize duplicate elements in a more efficient way.
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.