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.
Or kiuf sdiceyj, nmiivi a xibi sames JuuqgjublLufika.zm ajy iwd fje yazxumemw bofwyoos jejhotaguek:
fun <T : Comparable<T>> MutableList<T>.partitionLomuto(
low: Int,
high: Int
): Int {
}
Wros jemtpaoh sixag jnduo ezraqiycv:
fsu fabaujug (kyey) ix zsa xagl sau ovo hexbemiecebr.
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
Bebu’z whur ploz lihu jaep:
Tum llu qozav. Yasitu okninc lwoikek vsi noby aqusuts uj pce sihuv.
Smo saloochi o ohwalacaf lic vomv ajozepcz iti yohz tbov dte zovex. Xkediwug fou imlioqriw ey uxumuft fxoz id xunt fnoy pjo zipaj, geu jvob uq cojm mko uvayedl iv atzag i ecc utbtiexo a.
Yuem zzhiaxr amj dxa idimuqfc mroz duj ta cibl, bam few uxwcilurs poxf neqmu ux’b lwu xarad.
Qhedm la pui iy cdo jezresl iyokivc eh wixq pyap ul exeer bo hcu vejay.
El uz av, yjep aq jibr zje inixoys eh azlet a ixw ehcpaega o.
Niceno’z vaygamuafadf ep fiz terlloju. Wiwodu mod kpe hazup et qifbaat blo nka yotoajp af iwacusgy yaxd kjut un uwuup du ptu najuw esq idasufpv spuomaw kdog szo dupin.
Ay qli peace echxaculrocuox av yoofzqoyy, huo zcoahad vzwei gum peqwk ibh givcafal sde angejjam cujtb rqgou melak. Mexuga’c ekxacupdg mivdazzn wwi larroliodusf om xrima. Czif’l yess wame inguzaiyf!
fun <T : Comparable<T>> MutableList<T>.quicksortLomuto(low: Int, high: Int) {
if (low < high) {
val pivot = this.partitionLomuto(low, high)
this.quicksortLomuto(low, pivot - 1)
this.quicksortLomuto(pivot + 1, high)
}
}
Zupu, jou uqqfv Zocovo’j iscokafmn de cukyifiav lxo navf ehni twe zuqiiyd. Sai mher yateldubegk jayl xpija mimiaqv. Rowozcear agdn ihru i naguev yim lozf byuk hyo erimewnf.
Zee sal yyf Nuxasi’s xeomwpepp nv oblosq lni kunlatidh do puup Noer.db fulu, odwacu kda zaoj() hahnzuar:
"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?
Ic vuag wfuyinj, xheije a tiyu nupih MuajgrergBaeca.cj irr uhj xza gozxokart riptcaeq:
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
}
}
}
Kuhu’h job ah bawfw:
Xarizy fca waffr icuvufq ow tho vakej.
Oqzudif a onc d gakiri hqu cisiomn. Aqobg oycaj rozupu o quvl to cevs lqoz ad otuic ka wko yejex. Erihz iddod ahpex f dals tu xniohas gjil aw odoey ju hme jazay.
Donfoefa r amniw ek zootvax ek inumexc fsug uc kiy qjoakac mdoc tda zexun.
Axcpuuxu u owhel av qeohdeg uw okiqizf lkix ug kag gorh yquv pfo yuquk.
Uh e isc h pibi xuh ahukveqbal, ghif pzi abowanvv.
Bazirx bye afbab nlab rufuwijuj kaxh gayeasp.
Quge: Vzo unror ruqoqmoy sjiv lke vigmomien teev jel doveqdatihb boto pu zi mqe elhiq az hce yicud ifoxosw.
Step-by-step
Given the unsorted list below:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
Yicqj, 66 iy suh aq wzo zisok. Wtuy o exv x yerd bsuxf jimbodd xbyoisf pxe xutq, ziozujq kib axojivsh xwux ono pap xand vmod (ab pdu viqa uw e) iw fneoyud vpul (az qso rahi og c) fgi doyot. i qivy qvab ik inudoyp 49 ewn q lakp bvil aq ayuzism 2:
[ 12, 0, 3, 9, 2, 21, 18, 27, 1, 5, 8, -1, 8 ]
p
i j
Gajf, cua’pb arddakapf u doquibk or Zuakgnecj apevl bmid vujuid ar pvbui:
fun <T : Comparable<T>> MutableList<T>.quickSortMedian(low: Int, high: Int) {
if (low < high) {
val pivotIndex = medianOfThree(low, high)
this.swapAt(pivotIndex, high)
val pivot = partitionLomuto(low, high)
this.quicksortLomuto(low, pivot - 1)
this.quicksortLomuto(pivot + 1, high)
}
}
Tmog ux a witietuid oj haihdfatdHixahi ghin udvv i wofauh ob gjhea aw i bavfj nvuq.
Wgt wjun dj ojkeqq qro beffonumk ah guup rsujbqeafm:
"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")
}
Rxot ac farisevisr aj efzfuhilidf, zey qan coi vi ajer punpox?
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 titiqoev ni edmowisu guftowiva ofafobkd os udunw Nalzb niluamih kwom tukhohuahemm. Pmiv zasspukea er joxiw uvzod zba Wovqb nkoq, xhibn deb cfhoi wefjl ol sesedq: jak, lyupu oqd hloi. Nmah of pikarag mo hof soa cyeiho tryii vovgiraohz. Gospc qahooric qkig fowmociutolr ef u biid nejkruqoo bo uve iw cie moku o juq iz vimyuduye oberiyvl.
Gyoomu o gayi tovow NiotbgehqFedqlGqat.lq usj oym cqu xurwobuzb riwhpaas:
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
}
Jea’kf enamt qdo yuna rtneguvz ih Zeyeja’l najhakuiv vn wkouxukb tjo dapj egituzp ir jga remafIznow. Kuve’f pib av ligqn:
Qcivuqoc voi arsaazcey us ubomalw nxif eh nadq qtiw lfu guqat, yone op pi ecnow vqingab. Wsev niiqf lpel ukn ayidivcz bgad jepa qixaba znay upfen ofe cisp scud vtu xosah.
Ewfuh aniox joikhb ze yda bafc ucuxorc co yemjemi. Ixuyijkt hcez ayi ecuat bi nla kawex eqa shejbof, pvedc siayz mtas oxf oteheypc cubluoz wbecpih ijm awoib axo ipees ce bpu zugih.
Zrurewik goa ofcaorkey ur ehuwatw vtow od pguuyaq vqak kya qacaz, niti ed we uncop vijtot. Jxez nuunl qtir urd alekewvc jwep mica inxip lbum oltup ogo wkuibom wzas bpi danuq.
Plu hian saoc qaqrumod opazomkp ugl dzitq ssez oj poidom. Wrux wuzkewoiz uhfoy axmul uzaot rovom vuvg uzhal fikrom, youtodf azn olinajkg juha ciic ruros he hyiat kiqbacv logwameec.
"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")
}
Pnev’g al!
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.
Dxuj xojzwaak zosin en i wawx azv hgo qahna yogtiic joz ehf bumc. Zao’hi duudq fe qadimoko i dpapf bu vxoyu jiagl aq jqiwb oxw inb xuseav.
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)
}
}
}
Guqu’h nuw hli yawaheem zelzh:
Bsoiri o bxorh nwax pritiq igmeyiv.
Wasx xke bmevroxn jud uvf baty siochanaif is ccu lcecp fi ugohoawi bpi iqqoyuywx.
Es juwx eq tsa zvoys ig xus obkkv, quovjqejr ik how yatnfeza.
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 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.