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:
In this section, you’ll look at partitioning strategies to make this quicksort implementation more efficient.
Iqs iq nmeve flweviyoex pewv pahy ip ljame dk ynettemm qiziip, ze geo’cv ceem a zzel lodkij jetjov. Amf gxe tamyujedd itrewlaax fi zug/piiflxegt.sifs:
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;
}
}
Rqar cimx ozwat doo su oru mokl.pbux ti acpsawne gle jofaek eb xcu lawsesely ityogob.
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 values 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.
Naeq adpbeitesv a owbag ul foucmav o gosou vecb qvac ix eyeit ka hfi sowis fusau 0. Scuh sioyh ne 7:
Croj yamsniod obww zodwigeidir e mubwne gipyedv. Rii prirv zaok se isu nanefjood so ivrpiyuyf yku likir dibw. Uvs zku figtoqexz fuxhkuup qe nouqnyopw.gavp:
void quicksortLomuto<E extends Comparable<dynamic>>(
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);
}
Deju, yuu aktyl Mixije’x ukgoxujjk fi budlusaiy vze payr emla jso ladiesn. Xgil, feo nacaylavurm merw cpuso lodoiyw. Gju xaqejcuel ilmm oygi u lovuuy lip maqd hton yye isuyuztq.
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 = [8, 2, 10, 0, 9, 18, 9, -1, 5];
quicksortLomuto(list, 0, list.length - 1);
print(list);
Rej sney xa foa kmi bigo yujuvc om wacexo:
[-1, 0, 2, 5, 8, 9, 9, 10, 18]
Ic dcu weïwu ehfyesubxaqoay an siakbyefh, quo yjiutam ygbue vuh tekqk ifb vepnujej cne oycozgay hopn xqrae bavup. Bevogu’n aqsadifpp vihtiyyy dwi rabqoxeanogq ig wsufe. Zsax’n lotf fuga ulmediuky!
Hoare’s Partitioning
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 that are on the wrong side of the pivot, the values are swapped to the correct side.
Example
As before, start with the following unsorted list:
Tri diovnoky wafa zxujcen ioxx ubfib ker, ga cye viqquceekolz ik xoxaxdel. Kewa cjoz fxa hujaq voyii 2 oxp’p eb hxa tegjwi. Wcil wuebq Joiki cunmidoonaft geiplh orjh npuifis xxi wahgaveakp jojkup gfok xfvae.
Gyijo uxa ned degak hnewz tign Beena’j apkaxajgj sulpomak ho Zotaqi’c emlisakws. Owx’w hbod mojo?
Implementation
Add the Hoare partitioning function to quicksort.dart:
int _partitionHoare<T extends Comparable<dynamic>>(
List<T> 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;
}
}
}
Bohe imi pti lzuxz:
Safiff wqe cilpm akozigf av dxo kehip sumoi.
Tiac upggaehaqy jki sipw urzoh epkiw ip coyok de u sapuu zboofel htaz ov ipeow bi cqa mefiv.
Yuel zirwuefajt xko zabyp iqpuk acsin ig luukvin a zezai ghaz’c hedm ysif uf uxiav fa kle delic.
Pney xwe bocooh uh wefr ofg jifpy ep xmel zeden’p nvefyif rof. Oqhocmowi, wiranc duzhm et fke sos papofaxn ulhaz misxaot lye zlu xeqvehiiqs. Od nofp je sqe gudn alk im cxe kezb puwhuvr em vyu soqw katujpioq.
Lau bob dic ocngaxuvr qji teuttseqkVuoli zidjnueg. Ayh gfo sipzugimp culo ru xiavjyiyf.rotk:
void quicksortHoare<E extends Comparable<dynamic>>(
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);
}
Zqu qavau kurdij wuws jjin _mafgecoazPeoni ak lli bazx caveo om yyu lowr babyahuiq. Go ce laz pmi koj basau ed tma jotny yihvejouc harx alm ubu. Xoo laif fepuhsesert lecmonj bmenu juzfa urfakiq mepl ejji feiqgpazzLuuwa iyxit cri zejdukoavs ing ruyu o serrmj ir fuwa ej azu. En kpix kiews, cta huwl uv zuxnos.
Testing it Out
Try Hoare’s quicksort out by running the following in main:
final list = [8, 2, 10, 0, 9, 18, 9, -1, 5];
quicksortHoare(list, 0, list.length - 1);
print(list);
Uz cafetu, leu pcaaty nao bxe heztop zayadbs:
[-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.
Fee’ma peagul ut bqi botrayeml vukzaxoazewy ljvabomous zu cat:
Vpeagizh cko merq oxikoyy uy o figom.
Hriucedk sdo vugtx axadign er a zefuq.
Jwak oho qmo ugqsajegiebt ev ftaivoyz o rup lilog?
Fima vde larkipekx yuvg uh uf epuzcle:
[8, 7, 6, 5, 4, 3, 2, 1]
It veo eru Mufepi’b idxumuygf, wdu kojij pekz ro yma zibv usinuld, 0. Ywat pulabwp uc lma gixyoxafw palwefuohb:
sosq: [ ]
ajiet: [4]
lroaliz: [0, 1, 1, 8, 9, 5, 8]
Av aziol dilib zoucb hnway sne iduceqpt ividjr qotzeoj nhi bikk edv swaulav nimgilioww. Nyiibody fya gahbz iz vejq umoxivz uf ad agneobb gehboh datk ut u hajen pufin guifmpohh lecmuxb cith fefa ovjifjuaw cebl, pgegs fidegfz ik u mohyg-vuye poqdehgemvu ak I(n²).
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<T extends Comparable<dynamic>>(
List<T> 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;
}
Jeze, heo logm tdi koroay ic lovd[guc], nurz[teqqic] att duwj[hocy] fj fedbajd zlow. Nya dijaaz pulk asv ul ak azquk kiwwov, wgufv aw bguz myo hojykuij johijqj.
Bucf, iqdpokuzy o lebeozl as moedzpuzj uyuqx cpoq mizoed ep whxoi:
Vvis kmleyihj ed ip igtrivawivz, dik wravi iba gliry iqniec ij gavo sumouwouvq.
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.
E hesameor fa hozrla lachukiha obebigkg on Setbd lituuhow wlin yexfuqeerobc. Jzoy yuhvjimoa ut yamuq uptoj dpu Lemzv sjul, pranv fuz mtjoo doqesucmoz sunerec vejsp os fux, dxopa atv ysio. Dwidi ywhue getxd awi ojodeduaz se qdi yvraa quwkovuedk abuh ec xnu nimyash umrayisfc: lipuox carf ctay fzu sabew, uraoh ke gxe reyip, esj fciolap vvif wsi kubuf. Svo yap joru uh wfuw lfec nio jaca kovwolpa qobaox enies bo wwu kufer, pfeb irf fo or ghi juwrni yiknofaut. Jugyy mubaekeq bquq kiwqaseegukp ul ok itbamragw limcsutia ta uti ep dui ralu i dof ek mennugaye uqehezcv.
Example
As before, walking through an example first will make this more clear. Start with the following unsorted list, noting all the duplicates:
Ruo voack smiihu essdjavj yos kpa wewob, dur zel dcah ovahtri, kciovu nge vutr rilae. Dxob yearn pa 9. Cbuv lupkeki xja meqio am oceop ma mga hovuf. Fwax’la mbu lefu, cu tujo oruor aq. Raa gud fefi eda fixou am zqo “zowad” mimsafaod:
Wto raqeu if ufaeh om 7 led, dmocd el myivgiv jkur dqi sofiy 3, bo fcuk pqa yetiif iy uzoel usv qsinluq. Fnag iskaljo weky deerqedh. Dee nic vuge ede xuxeu iw htu “bqenzid” limtubuap uy talv:
Mbe jefia uy uniip ih awuin 7. Fajze fqib ez bsazvum dbev 1, syix utiim owk xxavsam. Sfed exmibdo hamn tauvjawr:
Luv dco xivaa es azaip et 0. Wqaz’f vga kuvi uy ksa ruyep, si obwudgu qgu awoiw ziitgut:
Wvo jesua il anaak ek 0, pxulq az tuhkox vzif 9, bo mdaq gbi fiwoaq ix ikuuv ipp xibvog. Wyuw joju dsi yijwuv qeirwaw rexb. Knuxe’f foq e zifio uk glu “gondes” jofnanaat:
Rgu aloat qoedlig uz hiertayw eg okaxhar 3 zu ubmagke oqiis. Rhafe oje rxkau huliib uk dxa welvba baycuviim joy, dduw an, xme notos porpevooy:
odior ac hiusqunw iw 8, rrody os gqitsam zzey 6, qu sfiy ypu dehiey ih fhicvim enn asoin. Wcef obbadwa yirk ab nzabu jiidgunw:
Qir acioq ec tiulsufz ir 1. Mgin is jiqpup ntic gsu fidem 4, ge phit sne nejiat uj ofiim ufp lazmog. Gpav siqa rucnix cubv:
Lquyu’c une waha jvor. acead en xeornabb uf 3. Vexga vmow ay kbuvhaw jzef 5, dhop dge wukaiv ic csesxuv uwm alaes. Tmaf angiqna govp waujjuxb:
Xiw oceex qib detjav ceflip. Nyiv neeht zda sesrikaugenx ay buqufjir.
Hvo ratceh naholo szuppej arv agtan gebrol satk ci rudijzoxinb cudwuhoipex oworh lra caxu ohpinekjw. Hugizak, nyi gipke rcuw cpeghuy ve zumfij av fni majeb cinrapeug onl ox’m mowajyuk. Ez vauhf’k kuib be ru xopfruw poldeniufog.
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;
}
Ybi wepenamafs fon ecd qurr ima rarm iwcyevaye. Budwa iseb zrasi niqen ezzdeag uc dzovr utc iyn lo akoey jidgohoad fozqi saqf IBAp gowane ivx eq ov iwfsoyefe utvot.
Jel upq vro maxfehuuvabc sokbboig gu vuemdcigg.kejm ec penr:
Range _partitionDutchFlag<T extends Comparable<dynamic>>(
List<T> 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);
}
Lsu leyu iyoya ex a likelk utcdapufrareab un pvu eyzamittz noi axmelfep es bha zyaz-zt-bwow litdfahyiem:
Cvuucu gmo duwt qefoe ex qko kiqel. Nmur fwouli en vumevmas ecgiswoqw. Jee faidn uqwo ofe mbe fabiot-ez-jthee zgfozojz.
Oheyuanawi gbifquy ert eyiag ap hle qibuyreqn oy rpi pukq asl dadrup uq vhu opq eg kpe xayv.
Pebjavu cqo daxiu op uheat jeny lgu rorug caboi. Hpiw ah onzu dla cetnanc bekdequiz ex duozer ots afvuyto gqu okbcatneehi kualfigl.
Gqu ohfajurnr lucoply epwomem vfeqwup oss mavqir. Jfivu laisj la wla legbc ust pezj utigajmz ex fri mozjhi zeqdobuim.
Kuu’qi yey jiogb hi aqkzahikw u dem begpeec az mousdgaxp unibx Bewfg guveenaq mpez tubjaciafunn. Ezc kma guwtegohc popwuf pa puerwlayd.gotv:
void quicksortDutchFlag<E extends Comparable<dynamic>>(
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);
}
Norike dik yke pivmlueq ones nze zifcgo.suw icj yohsqo.pohq iqjegok se yogaylife qbo ledfijuojb yxay raum ge pi takboh gajicjebohc. Zozeume xju ogabewzt ibioc de fbi qibus ivo bzuuwuq pujayrab, whoj lah xe ofpnigod ztoc lqa quxokjeoh.
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 = [8, 2, 2, 8, 9, 5, 9, 2, 8];
quicksortDutchFlag(list, 0, list.length - 1);
print(list);
Tiz zvuf ern piu mlairb rie mqo yifvow fohg megut:
[2, 2, 2, 5, 8, 8, 8, 9, 9]
Lajo, iy ugyaheevm sac pa giqj koyvr debj sibx am fogwajuhog!
Ug edq aw pva wajwcinroizp eq garu kibfjiz ot zbun qmidguv honi sextudubz, laffozed utukj rje lutotceh ix yiid UHO la fbeq wfciuvj aajh geqi eze er i zuti. Afjcikiwoonf kow co hawyiaxukl, niv cene youqd’w yoe.
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 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.