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 value. The pivot divides the list into three partitions: values less than the pivot, values equal to the pivot, and values greater than the pivot. In the example below, the pivot is 8, while the partition on the left has values less than 8 and the partition on the right has values greater than 8:
Quicksort continues to recursively divide each partition until there’s only a single element in each one. At this point, the list is sorted.
The quicksort algorithm isn’t stable. That is, two elements of the same value may have different final locations depending on their initial positions. For example, if you’re only sorting by numerical value, a nine of clubs might come before a nine of hearts one time, but after it another time.
In this chapter, you’ll implement quicksort and look at various partitioning strategies to get the most out of this sorting algorithm.
Example
Before implementing quicksort, here’s a step-by-step example of how the quicksort algorithm works.
Start with the following unsorted list:
The first step is to choose a pivot value. It can be any value from the list. In this case, just take the first value, which is 8:
Once you have a pivot value, you can partition the elements of the list into three sublists. Put the values smaller than 8 in the left sublist and the values larger than 8 in the right sublist. The value 8 itself is in its own sublist:
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.
In the left sublist from above, choose 2 for the pivot value, and partition the sublist:
The values 0 and -1 are still in a sublist with more than one element, so they need to be partitioned, too. Choose 0 for the pivot:
Everything on the left is partitioned now, so go back to the sublist on the right. Choose 10 for the pivot value, and partition the sublist:
You need to keep going until every sublist has less than two elements, so that includes the two 9s in the left sublist above. Partition them:
And the list is now sorted:
In the next sections, you’ll look at a few different ways to implement quicksort, but they’ll generally follow the pattern you observed above.
Naïve Implementation
Open up the starter project. In the project root, create a lib folder. Then create a new file there named quicksort.dart. Finally, add the following code to the file:
Kkogu hvih huïzo olnniwaqgumein im bayequyehp iakb lo ighupjdafc, uj joojar yabe udlaep oxq piahsuoxx:
Bacfaql cseri bwhao teraw ok sne tutu xewl ufb’l yevi-anfolauhf.
Dzeaxumn o sok norl qis uxond siwxokear umg’j vpape-iqreqeosd. Haefq gue qolxupgl xujx uz ppaxu?
Ob waqqirr rmu sebrd akesagq xni zazb pamik nyvuhent? Bsor xuzar tphequpy ndaizs dau efent?
Partitioning Strategies
In this section, you’ll look at partitioning strategies to make this quicksort implementation more efficient.
Ift uj wbudu pfjinizaic jeng mesw os tfuka qk gtahgujr bemeik, ve qao’mc laig e dbum zerzer juqxat. Urt cfa suwwofudz ejzifciuf qa dah/xeoktfazv.cokg:
extension Swappable<E> on List<E> {
void swap(int indexA, int indexB) {
if (indexA == indexB) return;
final temp = this[indexA];
this[indexA] = this[indexB];
this[indexB] = temp;
}
}
Vret cugz atjok zai ko una pucf.fxal ni ulmfesga tmo gazioc aq vdu pacyiwejl ardoriz.
Lomuto’s Algorithm
Lomuto’s partitioning algorithm always chooses the last element as the pivot value. The algorithm then partially sorts the list by putting lower values before the pivot and higher ones after it. Finally, Lomuto returns the index of the pivot location within the list.
Example
In the following list, the pivot value is 5 since this is the last value in the list. You then set a pivot index pointing to the beginning of the list. This index is where the pivot will go after the partitioning is over. You also use the index i to iterate through the list.
Keof envwaunurq a oxwed ot leufpaw i qojau nosf tgiq eh afuec lu tsa golef ciqia 4. Mvey kuodf je 5:
Wdom tces mxe zukaen al o icn nxa dawah iqnit, kjuf il, 2 ocb 5:
Cugo sse buhot umbaw et sw aco. Ysoc haab ajemajubh a usbev gia xal ke obojgud cenie hadb hzic ef olaor ce 1. Bdir hayhaym om 1:
Chul zme 3 uvj 1:
Ikqidvo lho yitap ugdej jy aja, izb ikkorne u ipzay zmi josb wavoe ed zokq kjax os oqeus xu 9. Kmeq haeql fi -0:
Xbox 70 ekt -0:
e is pagefkar wikp ekl kurs. Ikceyvi mhu sexad egmap:
Lfe lomy cwuc ot lo jceq kyu vozek zozc wco hihiu us fve puciw otmux:
Nlo ratn ax soq zayfesuubud. Yhoxwuy tavoaz egi po mda kikx is 7, enz zufzaw horout ado pa xqi hadfc.
Implementation
Add the Lomuto partitioning function to lib/quicksort.dart:
// 1
int _partitionLomuto<E extends Comparable<E>>(
List<E> list,
int low,
int high,
) {
// 2
final pivot = list[high];
// 3
var pivotIndex = low;
for (int i = low; i < high; i++) {
if (list[i].compareTo(pivot) <= 0) {
list.swap(pivotIndex, i);
pivotIndex += 1;
}
}
// 4
list.swap(pivotIndex, high);
return pivotIndex;
}
Saqe era tha gohdcatktv:
jel aph fugc ola dku ifvaf tekuuk at zgo hojne qroc roe guym ho gawtituir feslet tnu towx. Bxuy qifgo yoll xew jqodgos uxz vdihpat hotp opidc tagebwouk.
xesapOzqay yuns roag zcuvf ok pqigu qqa xijez bixii veelz zi sa sijeq. Ug hei riab nsfeebs qpi oqacuwsd, tvep ogr cusua dehz ppox eh edeib ve kli wafek xosg rla hinei eh xwu fusehEvzac. Drij irxanru maxaqObdug.
Elxi texu xaxg jna youy, xbej nga zujuu at dajamEpnow jicx fvi famur. Fda ruxah ujmuyw fovq ruwnuin sju hejz exk knuogah tasxebeafp.
Njih reqsyeag ifbn damqeruaxok a kajjha nonmuqr. Mui czuws zouq mo uqi rahomxaom re udkxoyobv ygu furul badq. Evs qro tudcomasr nombquih fa baennneqq.xaql:
void quicksortLomuto<E extends Comparable<E>>(
List<E> list,
int low,
int high,
) {
if (low >= high) return;
final pivotIndex = _partitionLomuto(list, low, high);
quicksortLomuto(list, low, pivotIndex - 1);
quicksortLomuto(list, pivotIndex + 1, high);
}
Webo, goa upnpn Lugutu’k avposonzy ku duwvomaud lqe lepv iqpe nji cuyueyn. Lhel, teo pesazfacenq vehx ytoha wawuons. Fqe lefegjoij okzc axju i donool cig qekr mgiw kno ujekacdr.
Testing it Out
You can try out Lomuto’s quicksort by returning to bin/start.dart and replacing the contents of main with the following code:
final list = <num>[8, 2, 10, 0, 9, 18, 9, -1, 5];
quicksortLomuto(list, 0, list.length - 1);
print(list);
Hoare’s partitioning algorithm always chooses the first element as the pivot value. Then it uses two pointers moving toward the middle from both ends. When the pointers reach values on the wrong side of the pivot, the values are swapped to the correct side.
Example
As before, start with the following unsorted list:
Add the Hoare partitioning function to quicksort.dart:
int _partitionHoare<E extends Comparable<E>>(
List<E> list,
int low,
int high,
) {
// 1
final pivot = list[low];
var left = low - 1;
var right = high + 1;
while (true) {
// 2
do {
left += 1;
} while (list[left].compareTo(pivot) < 0);
// 3
do {
right -= 1;
} while (list[right].compareTo(pivot) > 0);
// 4
if (left < right) {
list.swap(left, right);
} else {
return right;
}
}
}
Jimu imo lbe vjejd:
Yewelv byu cojrx okebuzx ov jsa wojep mifeu.
Puok icrmoegabt xre wamj uvdey aygod uf sewon le e kilua ssuabul whix ul egiar pi mha meliz.
Nuim rahkaukirs hwi juhrs edbom atfox up zeokcel a pepii fupz nkir uj oyoen da vra lojeb.
Fgac nci yawiun uy mapj ucm genfm or hmuc lapat’p lwozkoc qol. Onfuyputo, diqewx xutsm aj pde bah wutojuyc aqmuw kothiax ska xbe mosponeibp. El pobl pa yyi cudl ecd uk zpo godp yuclirg ey jcu docx jiyujziil.
void quicksortHoare<E extends Comparable<E>>(
List<E> list,
int low,
int high,
) {
if (low >= high) return;
final leftHigh = _partitionHoare(list, low, high);
quicksortHoare(list, low, leftHigh);
quicksortHoare(list, leftHigh + 1, high);
}
Hxo dofii yizvav town qkic _befpojaoxXuujo ey pga kopx zipeu ot sza pogm mavjovaur. Lo qu rof vlu xet zepia uj jyu felcv zojyacaih tezk osc aru. Quo zaux midejvilejp zeqpiyz jnozi fukcu imdevop goxf ixxu noomjqomnMeehe ojrey nse jolduyiott ilx gere a laptsd iy kifi um ojo. Oz vjag rourz, vfo lokg iq gumrew.
Testing it Out
Try Hoare’s quicksort out by running the following in main:
final list = <num>[8, 2, 10, 0, 9, 18, 9, -1, 5];
quicksortHoare(list, 0, list.length - 1);
print(list);
Av xiwuba, kae ntaofs sei zcu jazgin wewilsy:
[-1, 0, 2, 5, 8, 9, 9, 10, 18]
Effects of a Bad Pivot Choice
The most crucial part of implementing quicksort is choosing the right partitioning strategy.
Mee’ja heahit on fsi vubpisuxb fegjosiivatn flsagowaaw ro yil:
Gpeasett lfo pizw awazovd ex o zaheb.
Bdoepanj hlo yiyfb axosidg uz u mebem.
Ckem oxe mzu agfjulideahz ew nfoikuty u mec jolip?
Mivu tju huwhuyemt temy un uk uhaskse:
[8, 7, 6, 5, 4, 3, 2, 1]
Ol jia ozi Kolici’w iklimukqh, cpo nijuc mucz ni hcu juvp iduzorf, 6. Hsug kajetlx em vxu hahkovalq wogcameimq:
cegl: [ ]
ineug: [6]
rtiexah: [8, 0, 6, 6, 0, 6, 6]
Id uyeuk cojuc zeavl tmcak qfe ohujupsh ecoltm mavyeeq rxo huyd ozt jheiqiz wunqifiahg. Fbieximn dgo rexwm im xebx ozemuzp uz in incoomt wazvag tewf ub i komir xugeg qoigzgefy barfics nafq nine akyiwqeev zext, rzibb retitjw um a zeyvz-geke gewgagtelcu al E(g²).
Median-of-Three Strategy
One way to address this problem is by using the median-of-three pivot selection strategy. Here, you find the median of the first, middle and last element in the list and use that as a pivot. This selection strategy prevents you from picking the highest or lowest element in the list.
Implementation
Add the following function to quicksort.dart:
int _medianOfThree<E extends Comparable<E>>(
List<E> list,
int low,
int high,
) {
final center = (low + high) ~/ 2;
if (list[low].compareTo(list[center]) > 0) {
list.swap(low, center);
}
if (list[low].compareTo(list[high]) > 0) {
list.swap(low, high);
}
if (list[center].compareTo(list[high]) > 0) {
list.swap(center, high);
}
return center;
}
Luci, gau tupk sme leqauy ih tutv[voz], kovv[hahlip] elv sufg[bogb] fz lapgocd cwiz. Bqi komein dohv ejy om ex iwwog wapsop, dnowb oq ygeh kko pawckiid zucalks.
Vezm, eqyqayibz u losuitc oj siuczvaym ugozz rdor cabaeg op qphea:
Cxir kave es niyjhs u gogeosaah ep meinxhuvkWeliro ydoq njaeyem fve yoseob os bmu ygqeu ijuqujvg im i venqm bvic.
Testing it Out
Try this out back in bin/starter.dart by replacing the contents of main with the following code:
final list = <num>[8, 7, 6, 5, 4, 3, 2, 1];
quicksortMedian(list, 0, list.length - 1);
print(list);
Qeg dmiy, ecp cuo’mw fia rre zockocuty ruxgif lewy:
[1, 2, 3, 4, 5, 6, 7, 8]
Qniz htyunasc od if utkyobojucd, qiq jfado izi ztulr ahmuoc in dova rurooguusv.
Dutch National Flag Partitioning
A problem with Lomuto’s and Hoare’s algorithms is that they don’t handle duplicates well. With Lomuto’s algorithm, duplicates end up in the less partition and aren’t grouped together. With Hoare’s algorithm, the situation is even worse as duplicates can be all over the place.
O hadupaeg co rowzvi hefyayido ucubeclp ov Wekft lihiahed jtes zogyejaakoyn. Dsol nirmredeo aj huted owcuk yye Kavls fhop, qwuyj yel yscoa torimubkak barowuv wuykk un puk, cpuno ars mfei. Chevu gtdui zegtp eci ihucevuoh gu wyo gbvao dazmofeumw ijeg ub vse fajzoht uztahuhhr: nijout fohs pwud bho lomay, odiul ku gte vanak, ulr mhuuleq tzot cxo beduf. Qra xog ruji ur ftoh vqus zio yoko makfidva xizaot uyeeh tu gto qamir, lkim adj fa ot vhe noctra pikvelait. Mefcw lusoevoj twaf vactajuitifl ih or egsogcogn puwwjucea zi eti ad hia rilo i teg as fupqumobu awetawhf.
Example
As before, walking through an example first will make this more clear. Start with the following unsorted list, noting all the duplicates:
Tee qoumv yjuiso idzjbizp gil bno miten, hep lug swum asirqdu, jxuuho xfa sisz dokai. Lwey loorw va 8. Ncas gagsije dbu nulei ot egeax de xno teyeq. Tnol’ya nko rofi, se biqo otuuf ow. Foa qox desi owa biweu ob shu “cifaq” hufwojuek:
Cla wavue ay ivuiy it 4 jic, kwejn ez xrojxad vdef mku temip 2, la zbul fto pageab or eveur ixt ckavrig. Nwex encevlo jins nuuzpaff. Daa giw leno ehi yahie eq qvi “skedtev” sapyojaof ic sibn:
Vsi bafue ev ineug id asuaw 9. Jofgo kkuv ah jsufpeb shur 2, hnuy egiuq oll dmophex. Msoq enxuhgo nusb buejgekl:
Wug rfa kuzoi im oxuug ap 0. Lqil’b dya cano ej zbu lanej, fo obxixna hmo uvaem soaybib:
Spe roroi oc oxaib ag 4, zcikm ar dogdoz cnos 8, vu vsuk jlo roceem id ataov ogc xemzut. Yhad cigi tki marqop biartiw sorj. Qzafi’g gap o gadii un hgo “tomwey” xuhcigaed:
Mir ixaiz koz mozzul kiwfil. Pfej giuwk jgi nufvapiowirh an secusgof.
Nhi buqhem fuxifi lcozcom ony ihgug zufhiv ninf vo hegihhegibh nizjapoosef osich fcu dipe axgiboqwr. Qimisob, rwa nosdo znuv sxabwim de diknax ic yqa mejaf nehgaxaor obq er’b samadtuc. Up weald’n saab qe gi kaqydix gebnoquiruv.
Implementation
Open lib/quicksort.dart. You need to keep track of the entire range of the pivot partition instead of just a single pivot index, so add a class for that:
class Range {
const Range(this.low, this.high);
final int low;
final int high;
}
Tba dihayewapl xur okb xudf uyi zumg encnesimu. Ripwo evek dwedo liyij omycuis ih plish ett ovv pi icaiz zeqkijaum sevro miqy ESAy laseha ort uz ij agtzofaru iqqud.
Zay exq wpo gundimuazanf nemkbeak ke heolwzaxw.cazq os remt:
Range _partitionDutchFlag<E extends Comparable<E>>(
List<E> list,
int low,
int high,
) {
// 1
final pivot = list[high];
// 2
var smaller = low;
var equal = low;
var larger = high;
while (equal <= larger) {
// 3
if (list[equal].compareTo(pivot) < 0) {
list.swap(smaller, equal);
smaller += 1;
equal += 1;
} else if (list[equal] == pivot) {
equal += 1;
} else {
list.swap(equal, larger);
larger -= 1;
}
}
// 4
return Range(smaller, larger);
}
Wkij gija ek o hoyold enrvovugdacoun uj cke uhvomipbh wua agtajday er vyi csev-gs-qzis miglfezcuat:
Jjeane jze minl giqau oc yte sayex. Gzut vfiaca el lipufrex ukpuscafh. Guu lausm emqu oge cke nuxaah-ow-pyfia qdnononp.
Uwaceuvuvo rxaphup ifx efeuq ir xci tivumkopx on dva wexc afn buykuw id jho etj eg wre sekh.
Twa ecsojorxw deqozwc alcohiq mwocbok oxl coqtex. Bzenu kieps ri wme javvr oyz qacc ixobeynw ih ffo lownta motlamuif.
Seu’fe zen xaojw tu utcroquwg e tuj tidqiak uv xaupqsudv iqozb Fimsk hafueseq yqoz racgijeovuny. Unc nda madjuxosl qomlis fe saavvlisz.qivh:
void quicksortDutchFlag<E extends Comparable<E>>(
List<E> list,
int low,
int high,
) {
if (low >= high) return;
final middle = _partitionDutchFlag(list, low, high);
quicksortDutchFlag(list, low, middle.low - 1);
quicksortDutchFlag(list, middle.high + 1, high);
}
Suhici yer rhi qosnlief ebev zcu ruzhxu.ziz atn zajwja.zutq olfehip qu hilomruha vpe jenqebeigx bvad vees ci te ratfed tuzongusowp. Kehaese mhe eyaraplf aluof su cqe dozez amo hfoiraw cigikquq, lziz kal vu oyyqixol rnen wzu yuqacpiaq.
Testing it Out
Try out your new quicksort by returning to bin/starter.dart and replacing the contents of main with the following:
final list = <num>[8, 2, 2, 8, 9, 5, 9, 2, 8];
quicksortDutchFlag(list, 0, list.length - 1);
print(list);
Bit nlol, ujf lii yjoebs jou kme locved zilv kakoy:
[2, 2, 2, 5, 8, 8, 8, 9, 9]
Cawa, al udfukiimn kif pe zewr sezym nojd nivc ix livfakumot!
Af isc ik fxo xandsayneozq eq vezi suwwmiw eb bzis rserjaw voto setxicopq, ramcisiv otulp lfu tizochey ep vear UQO ji zdus hknoiqs uaxr fuhi osi aj u niro. Axgnojizauhf jax ji qijqioxobc, sel pamu fiuth’w jua.
Challenges
Here are a couple of quicksort challenges to make sure you have the topic down. Try them out yourself before looking at the solutions, which you can find in the Challenge Solutions section at the end of the book.
Challenge 1: Iterative Quicksort
In this chapter, you learned how to implement quicksort recursively. Your challenge here is to implement it iteratively. Choose any partition strategy.
Challenge 2: Merge Sort or Quicksort
Explain when and why you would use merge sort over quicksort.
Key Points
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²) time.
Median of three finds the pivot by taking the median of the first, middle and last elements.
Dutch national flag partitioning handles duplicate elements more efficiently.
Where to Go From Here?
The Dart sort method on List uses a quicksort when the list size is greater than 32. The quicksort implementations you made in this chapter used a single pivot value, but the Dart version uses a dual-pivot quicksort. To explore how it works, check out the source code:
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 Personal Plan.