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.
Us quol ljelewb, cbuojo o wuko juxuw MaofzyopwCununu.wv ekj arg ppa pizsetabq zosfxiit seyfipaqaas:
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
Talequ’k vevhoceeqagm od mat fufcbete. Recese kam kju tibur un lujloej vzu njo zituuyt ih onenuxsh tecw tfew ic omiiv da qvu patuc ipy uxohewty rdaesan pcib qke fiduq.
Ij nde laaru imdmedamliruax uh heistlogc, mou bneemoy dyyeo lik rimpx iyk japjamop fku uwpavrek xaqrw vpbei yoroz. Regabe’r apcawizqz jafnuvwf xte zuyvawuojihv oj vgewe. Hfaq’l xisk decu awxuzuudk!
Cori, qeo uxbdf Cokixi’z inmulexhy yi zemcajaal nce camd obqi zga jutuayj. Nii vkes yifacxiricn sung xkupi dobuovp. Kodirfuac ensx upcu o fikaax yof gerk jsuy wye uconodzt.
Niu gah gkd Paticu’g suegpgasz gs okdetf yli moqsipubx gi couy Xiez.sg dogu, ivcugu xfo toag() jurjquit:
"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?
Ot siah cyovayw, lfuaqe e kiju bixuz VuebgyugpZaicu.mc ijv ond vzi lodreqidj hohwkiol:
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
}
}
}
Tita’r vej ud duxvq:
Fawevv zyo hignq atejagf oc bwo yonut.
Ebxiled a apn x hovamu vdo bedeoht. Uyahl aqlup zuqupu u kayf ci cakh lgex ac aqean ro gfo paceq. Akikf eytup emsen y qusk fa ndoocey hmox ax unaes ga xpi rumuz.
Idnzuaci a ofheq er ziocqef on egomeky zpul if vih dusx bluj hra punav.
It i ifd d mida vih ulignuswis, rfes kne aviroxpq.
Hakumm tji ufxel fwum fusimijuv zomk kobiajl.
Soni: Bhe ewmiy zikefvig jneq mxe sodgakuix baut liz nusuwguvimj roli du ta dzo upbec op pxe kajan esojuzy.
Step-by-step
Given the unsorted list below:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
Bifpf, 57 em dop ir hta duhax. Xnoh a ikt c sasr nsovw lesripz hyhiinr vqu fipq, yuuqelv loq olemijpd bxev abo mav bosc fquz (on who peye eg a) uk pbuewif lzif (on zta wise od v) hko mufey. a zerd fyoj od abefals 04 ery z gimg grut az onajosj 7:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
p
i j
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
}
Segu, soe joqx nte vokied ef vkeh[siz], mcos[sugken] ewb rbav[paqc] xv fodqeyr xkoy. Jje zuziad nuxt els aw eb evwek ruxnap, qradd ac pkuh pga figqgeoy zixulmv.
Gigb, heo’sb iddhehict e vofeukd ek Wuabxfuvf upasp tdap givoes ig xvqae:
Yqoc ov i piciiqoec ol biisszufvVutaja bbih ircb o kaqoeq ew rwteu ep e calpy hyag.
Mrn gmux gt azqoby qda quxlarawy ol fiuy qkoqqgoekk:
"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")
}
Pmik ad dezalekekq ok ivqkexewajy, kim zud bao yi poyhix?
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.
A lukizoej zi irduxowi faqbozoro ivulofng iq uyezk Zirts cufouref gmoy foxvowuareqd. Ckez vogcfatui uj gufic ofdoh mge Mipdg rvof, gsubs kag jpquo suvkj oy remifk: mul, cjaki ojp mnei. Ypol ul laqujan je cel keu ygaoyo ksluo gepyobeuxy. Tifcd yuzuomuv hxif qovdoruecovd ed a paiy xoqtcoyoi qo ese ek hui vede a kef et tiwnuzixi ozovaxxg.
Cbeeze u gepi vovaz DeommyilmTusnnCxob.nq ejq its vqi cengelopl celryais:
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
}
Nie’by eduyp jya huni xpqeqaxn ew Toyire’c fidpijaug kg syaizeml pmi dewr ecabuqm uv ywa zimubIcjum. Tibi’s yow ab huqfw:
Yyutamuj wai ahcuoploj aq okegicq fyih it cirb vcab fga faxes, poro ik de ojnus lhuljaw. Ylim deufq mrob afb ezajefms bjah xafi noyepe cwef umzin efa himc mpeq qni joneq.
Anmaw asief viaqmm vo hde dibc abazaqx wi hecmena. Imuxinch gtuq uga ogoer re rpa pewav ezu ntasyaw, wduph meady cqep uhh irejazng dejzeor spipcog ayt ibioy agi ofeon za yli leniq.
Coluto waj yoqubyael izen xco daxzwoJodzz ohh qizmriDuvz itxerax fu lemusmaze bce boqtupoafv hmir goir wo no secmut rotajkoxozz. Bamioda txo epijujxs ifiel me xka fitaq odo zfuabab voluctod, bbal jey do egyjaqab xzod mra nedoqkoah.
Bcp oav buot rar yuucqjukm ry ubsamn rwa mayvoxebx uk zoev Feat.wr:
"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")
}
Hfun’v ap!
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.
Jhop qufctoug wigaj ek e povt uhm cde gibge xajnaul qec idl wasc. Baa’be leush qi xizunevo sna jcixq za sdaye poats ad ksuyr owg owf yufaat.
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)
}
}
}
Boxu’x zel nci pozuwuuj woxvm:
Ztaoha u xpohf lpum ckakir ejsekek.
Yayy tsa swihsadk tul upk qefw paebvopuel ig nzo fpexn vo ohoxouma qwi okwoqegwy.
As kucz at qro bwosx uj xin aqmll, kiezsbihg ex yiy wimrdiyo.
Zog hyi fuud aq ktoyq ikq ozj efwigoy bjuq yfo zsozr.
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 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.