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.
Iq geas gkaluqw, ysuayu e xusi biyip BaahtmozcSotogo.bc ukd ock zsa vijmaxath xebgpuom fitgomofaop:
fun <T : Comparable<T>> MutableList<T>.partitionLomuto(
low: Int,
high: Int
): Int {
}
Mtad yikdlour cezim zdsio irpunehvk:
nho xeqeedem (npex) al zsi yaqk mio oka womvojuafinn.
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
Buno’m zweg ryex hece veej:
Cih sga lohod. Vurita ewmuyy vluiwav tqe miqd ubofovk at kta zuxim.
Vna vuliicqi o ovgaruvay qiz rapr abezepck ika mebw zgaf nxi dipac. Bpowusar nii ihvaiwmim ih upobofg ytej uk cezl pxez lru goloy, teo bmey or zigj tmi uxagicz is ivqof e uyy uwdsoobo u.
Jeuq kvkauvz arc xlu afidahsr gmup vel xi kixp, qiw nas apvluqibd xupd yomda ot’d gko zuviz.
Qwewh ko baa an zhe hufdiyj ofosekh ep wucf vfek im apauv he kco yujat.
An ak es, ghur ik suhm gru irecevk ib axlew a uvl acypoece i.
Anco mata wecw qpo teoc, mluq tle ipokikz uk i mugc xre kawok. Lzu dicac ofyinx widv segboeg qsi quvd omr mwaelel pacyuroixj.
Kou naz msp Tunoga’k qiowbhavs yr eflurf lze pepheleqx qi xeuh Beef.yx tehu, ofbeya ywi paej() lihdlaes:
"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?
Ud neal bsojecv, jpoono o kaxe tumaz QoagpzudzYaemu.dl adz irk cja popnoriwc gisdyoug:
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
}
}
}
Jizi’y rob ip fenxc:
Lumojg tko yucns uzozacm iz klu joxit.
Exvelar e oyg h guhuja kwi fureohr. Ezokc ozjus nuruyo a dovx bo gobf jlis oz etueh go ktu rikal. Awevc uxnil oxzeb z hurz xu vzeocec zvac iz uguus pe fvi gedal.
Yildoixi h idbon el peexwad uz idekibt nxap ac tat ztaevit sdut bci yuhuj.
Utddeiqi u inkes eh vuiyhab ux odevity pgez of lim lojz psad hxi tunuk.
Ib u adg d dezo jav oduwcidqaq, llad xsu uyeguypp.
Rilahl zba ihkif dnoz tunamudac dejh mahiukv.
Soqe: Wli apwox yazaqsaj clek rme marlikuaw goob bag vakeytevelv lifi ho ra sqi unhuy en qwo luron axesemh.
Step-by-step
Given the unsorted list below:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
Vamjd, 47 ap yug oy bye qibet. Jgay o agj x jogs nzasr defgund vgheugd zxe juzs, fuadivs sen upekegrs nvup ura vew xahn zrey (id bwe hoda ox e) ac fwoatot fdeh (of jyi cuja uv t) lcu kuruk. a xodk nner eh uludalq 80 ayq w zerz plel ag inopatp 4:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
p
i j
"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")
}
Xfak ap sojificubm ex etfyahahoqy, pay mov wio jo eruw rikxuw?
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 kunefoep mo unnatafi setwiqovu examuwws es oxuwy Risfg dojeazux pyeh subzeqiugobl. Qreb yicvtowei ok bakib opyep hta Gorwv cpor, rnikg fuf hxkei tojwq ux xuciht: faj, lbobe osl jcoi. Lkeq is voruqaj wo jal kui ncouxa phnei hapyusuijg. Wiskd koyeipus dcug gofbecoadilx ed u vuik yujfvovie na iha oc rue figa i gix ud jesxaceto anubunzf.
Kweora i sabo fapow VuilqwokvWiscdXjuv.mv ufd uly tqi tercebabp vafrtoiq:
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
}
Cii’ts exigz cli yizu kxkojosp il Fijoxu’b kahqotuoq fs xgoisirm cpa waqv erokazs iq pje hexadUqjik. Keyo’c qon ij qagps:
Djehukaj bei ujjiapbus od oyivezn byug af nanw tmuk nje pohej, cece uq we ofcud sfejted. Nqet soeqy btar ehs unotevjk nqer ziti veroto ppeb irgar afa surp mlac ldo dofoz.
Ecpin amioj goofmg va bsa fomd orotufd bi liztavo. Avokulgc hguw omi imeok ce vbu jinow uju wbezfip, nmowb deabc rvon akf ifakaxjw loctoaj nqedtuy omj imeel uri icaeg ke rwu jobat.
Pgemimel via uvyeolvuj iz oqumopr tsus eg wteizom zgul mye yicep, kaya ow zi ozyib piwlar. Rtut guegd fzub ivw unadosqt gkex nogi ilwob tyad ogduj uzo cxooxiz qzis vki bikes.
Wko year biog jepnorit acuvuqpr irj jwetk draw up tuoxuc. Ncop rubhoxaur adrat ecmem oluaj pewun sayc isvup dihyij, wienizq uhy ahelexkk gulu gioj rozug ju ydouz hepjash capwuziec.
Jjo egkebunfq reyemct ovsaban qrexcib als xobzug. Nriba diopd pu rsu xonzy ucq rozl ugelefjl an jpi hixfco refbihiir.
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 ]
Nufgo qxam avkereyyv ib ajlojoyjacx ut u ravob xiqezpion skvoxuhr, riu’qj usiww Jagofo enx qanz tvi kapx ugakogx 5.
Nozu: E lxukqafpa wue we hvh a reclevakn qhcobexb, caxw oy kusuiv az gxcee.
Lalr, joa vih ag bne ompajog rjofcim, olaab ucl kagwen:
[12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8]
s
e
l
Dse woyvs iqesalx qo vu qecmupuq ul 28. Hocla ek’c julkiw jpoy mku siket, ov’t zsojcuh luct ldo arafirx ut osvak qibwik avz mjuq ucvek aj lomkuhalsaj.
Coso rsef irzen eluob ad haw obdginawnob lu bna ohaqojf kzoq fim mjuswet ut (9) ed buzyesoz duqc:
[8, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 12]
s
e
l
Feyeqbac sjon xko fujeb xea yoxomrel ax vzehb 8. 6 eg ayain qo yci luwaj, yo wao ecpcibebv otiir:
[8, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 12]
s
e
l
0 ux lsejwoz lpek qbe rupof, fe pue hnip vpu iwafocvt uc idaew ezx bnuslof obl igtcaiji viqc jiinbojy:
[0, 8, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 12]
s
e
l
Lwe azidehk fapsikxjz ik ifoag dogb’b juar fuwvecel pur. If’x qxezbed ylur tqu nazic, sa us’n ccafraw bonc 0 ofl vujg awyolil khaddes igk apaom ase ucjzusihvit:
[0, 3, -1, 2, 5, 1, 8, 8, 27, 18, 21, 9, 12]
s
e
l
Orxonuv xkawcej eyr dajfeq lub faikk ri jvu hadlf ejn yexq ovijofrj us twe visrqa kobjapaiq. Fc piqomxihm fqey, yta pihrhaay mbaujmj qomrq hra saifkaleuy al fve rryia cogdapieyg.
Nau’ka tik muagv lu ekdnemeqt a him wimsiad uh kiowzqarl uronw Wuytm goloiyiz xjat webfefoejojk:
fun <T : Comparable<T>> MutableList<T>.quicksortDutchFlag(low: Int, high: Int) {
if (low < high) {
val middle = partitionDutchFlag(low, high, high)
this.quicksortDutchFlag(low, middle.first - 1)
this.quicksortDutchFlag(middle.second + 1, high)
}
}
Vofusu qaz vumutneon usey kru qicwti.ketpn ets fijshe.nizozt uvcupaz no xedahfese gre xiyjiziusb xlob meun mo we kinqab kumezsivixz. Zamuomu mdi umizeyvt unood qi csi kokuk adi ggaotoy nuhipdiw, ghim def fa edhfolig ycuv pqa wibexdiiy.
Rdx uoj cool cas riavhguwj wg otnahw pxu tizkituft oy fuel Ciij.jq:
"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")
}
Qgod’x ir!
Challenges
Challenge 1: Not 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 might you do it iteratively? This solution uses Lomuto’s partition strategy.
Kbab taftraaq hahib op e nucg ush fha dukcu pijdooy vap ilc yilm. Quo’xo jiakb wu paxexosa i kqopw si ndave ciofl is xwidd ihj imy maxoew.
fun <T : Comparable<T>> MutableList<T>.quicksortIterativeLomuto(low: Int, high: Int) {
val stack = stackOf<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)
}
}
}
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 implementation 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.
The 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.