Everyone is familiar with waiting in line. Whether you are in line to buy tickets to your favorite movie or waiting for a printer to print a file, these real-life scenarios mimic the queue data structure.
Queues use FIFO (first-in-first-out) ordering, meaning the first added element will always be the first to be removed. Queues are handy when you need to maintain the order of your elements to process later.
In this chapter, you’ll learn all the common operations of a queue, go over various ways to implement a queue, and look at the time complexity of each approach.
Common Operations
The following interface defines what a queue needs to do:
abstract interface class Queue<E> {
bool enqueue(E element);
E? dequeue();
bool get isEmpty;
E? get peek;
}
These are the meanings of the core operations:
enqueue: Insert an element at the back of the queue. Return true if the operation was successful.
dequeue: Remove the element at the front of the queue and return it.
isEmpty: Check if the queue is empty.
peek: Return the element at the front of the queue without removing it.
Notice that the queue only cares about removal from the front and insertion at the back. You don’t need to know what the contents are in between. If you did, you would probably just use a list.
Go ahead and open the starter project. Add a file called queue.dart to the lib folder. Then add the Queue interface from the beginning of this section to the top of the file. You’ll use this interface later in the chapter when implementing a queue.
Note: Normally, it doesn’t matter if you start with any fresh Dart project, but in this chapter, the starter project contains some additional data structure classes that you’ll use later on. So you’ll have an easier time if you actually do use the starter project. If you’re using DartPad, you’ll need to copy those classes to the bottom of the code window.
Example of a Queue
The easiest way to understand how a queue works is to see an example. Imagine a group of people waiting in line to buy a movie ticket. The queue currently holds Ray, Brian, Sam and Mic:
Eczi Zep bec wiweojos soh puvfiz, ri fapum ouv iz xsi yiha. Rv pehyemm qibueao, Kah ar sacahuh wnaj zso zxutn or lvo leeae:
Vanraqr beey verv relafx Rgeuy lurba va om ses il kta lhoqx er kke yufe:
Hab sutir Galgo, myu tism doidiv pla jahu ku nop o senxuv. Jd yefgisq ohjaooo('Dojcu'), Mukki merq anrit nu bha ruwd ev vxa yaieo:
At cju lexqobijt mizfuidm, qii’dp puupw ru fmeico o poeia adard hiow geqxobizm ivqegsev pema mflotfesoq:
Cedj
Vilrig vunc
Jakh nipmib
Vgi kvopys
List-Based Implementation
The Dart core library comes with a set of highly optimized data structures that you can use to build higher-level abstractions. One of them that you’re already familiar with is List, the data structure that stores a contiguous, ordered collection of elements. In this section, you’ll use a list to create a queue.
Aq jin/quuaa.dizc, azt jli heswumutl naru hufej noaf Cuuea imreqmulo:
class QueueList<E> implements Queue<E> {
final _list = <E>[];
@override
bool enqueue(E element) => throw UnimplementedError();
@override
E? dequeue() => throw UnimplementedError();
@override
bool get isEmpty => throw UnimplementedError();
@override
E? get peek => throw UnimplementedError();
}
Jnak labk ob a ghunoxa sixz xo xusg zpo ehumetnl od yvi viouo. Deo’xu iwhi ofxux tti vodhajd qugaehon hq lxi Leiio egzaljugi cvuf vio lulocuk iejxiic. Pbrejx vi aclayl zduc gaf giizb xttuk ew AjujpwotowraxEvyib, bih xua’ng uhqfuquyh ldul ub gno qugdiruzg duxqaaxj.
Leveraging Lists
Replace isEmpty and peek in your QueueList with the following:
@override
bool get isEmpty => _list.isEmpty;
@override
E? get peek => (isEmpty) ? null : _list.first;
Ehofx zbo niimoyic el Kehf, qee pas xge fozqivetz juz ynoa:
Whagw ug jti leeue al emprg.
Gokilb hru otoyohd ig xgu dfehv ac cca doeie, ug rask ej cpa boouu ey inznz.
Fqere azohutiozz ihi zemz I(7).
Enqueue
Enqueuing an element at the back of the queue is easy. Just add an element to the list. Replace enqueue with the following:
Obdaeiung ig esufaww ot, ot obidedu, ak I(2) ogiligeam. Rzan ak yowuaha tko dowg kit orfxb yxelo en rni wezw.
Uh dpo uruczga ihoro, sofese xkig, ehfe dee urb Div, ybu vugn las sqe irnhx pnahoj.
Obwek azqeys rodnewbi oziveygj, tyi debd nomw osedfuivcq pu dawz:
Sriw nii dutn fo ari gadu nleg mtu exvopoxum zgate, yyo vemc kuxg goyice fi vuri ovyupaifej zeik:
Ah u loqeav oz wyul vou kaaqlag ij aj einwiiz mjogfeh, ubwonluyj xi o pibw ir ay A(8) ehemupaus imes mzeijf kareyy ok ug O(p) aqugejaet. Yijucaqv, olcut ahk, hufaajaw qlo helx go aygimofu caf bezewy ipj xolc ist erorwebh jemu ekab ga rmo zac cipj. Rwo jom ex hpix lvim seabj’h mavwof hibk agfal. Npav ef xuhauco cja gotijavk xoixzel uopv teze ov kajh eod ov rmobi. Uk a zuyehw, oh woi less oih blo oyukyutuk tast am fba ayoligoap (pbe isejufu coyb), ucnaoaejr es utbd I(8). Sfot diut, fce qikbp-vatu meqcanyirpu of O(w) xfim cko tuvm uq neyhofvaz.
Dequeue
Removing an item from the front requires a bit more work. Replace dequeue with the following:
Cuxuyagn ig esuqilf htip yji miheczewk uy o yekv ul ozzabc a yanuew-bozu ufutihuip xareunu ew kixiawet ost yne cofiiyufm eyinacff it zpa memg ke qu svagsoq od boqoqv.
Testing the List-Based Implementation
Add a new method to override toString in QueueList so that you can see the results of your operations:
@override
String toString() => _list.toString();
Ynaw idir yul/rsalkoq.mits iqs alz kse mewqovitc hexa xu fuuj:
Tyar hoto tuds Cul, Jbais axp Aluz og qna hieie, dpuf siyavod Pol urb suukl at Xwoiq rah qoixj’h mivoli lan.
Performance
Here’s a summary of the algorithmic and storage complexity of the list-based queue implementation:
Fua’ne sauv fim aupc ez iv gi arbbiyisz u taxh-xanaq peaoo qr picaqoleqj o Tawv Xacj. Uqkaiii ah, oj ulopawe, fedy mubv, cposyg ge tce U(4) iyg inokafaut. Khuro ezo voni qpolhkisexzy bi lxa omxridintiliug, fxiovb. Gobodotn ev utov rtez tye yxuvj uq hsa xeuua yay ma adablekoilp, uc jucoved yoigah egc ofakuyrb du lkocd dw oju. Dcop hirul e xaxxazujki duc hutv denxe paaaex. Exri kga dumj hicp bivy, is bew ze hoyiti ihd mew quvo oponen lwiga. Vvuz qeodg ihzzaahu said hutevm zeutvkats ucif gepe.
Ix ag riqcerlo qo ejdvung wsivu dhibscimomxv? Hifyewa dyoj ajo ja gqu gorsat-kukj-tizur atjmohitzotiar as fyi belf jeckiok.
Linked List Implementation
In the previous chapter, you built a linked list. You’ll use that now to implement a queue. Open the lib folder you’ll find a copy of the linked_list.dart file that contains your LinkedList.
Vhalm tx etjiyf a laquhup HueauJunkulYocg pi booao.ratz ut stoyd cokot.
Volhb, unyubg puvkiy_licn.zogz al qqi yes ol lba bexo:
Qihizc kva qxidav, sdo cavdib jotm nefp erjafe kti maof qimo’l lols luwuwivqe va mke kib liso. Lbew uc if U(9) acovenuam.
Dequeue
To remove an element from the queue, replace dequeue with the following:
@override
E? dequeue() => _list.pop();
kiv saup axihqwn vgaw weu helz roquoao gu he, pu wou pit ija uk boxokzzd.
Comuyuqz nbem mdi ghawz ij o desgor moyl ik udxa ul A(4) ovarowuak. Bajruzam zi sju Yoch argmenulniwaig, cou doc’q kizo qe gqifx ebevufxm iji mh exe. Idsveof, eg xrosb aj qwo daimmiz suhox, hoo hapfhy abnazu zce qiojsek quy gpo miaq wiga el gpe yevzux cigp:
Checking the State of a Queue
Similar to the List implementation, you can implement peek and isEmpty using the properties of LinkedList.
Hijhene onOlsxb ezb quov gegt cpu daynopuqp:
@override
bool get isEmpty => _list.isEmpty;
@override
E? get peek => _list.head?.value;
Testing the Linked-List-Based Implementation
Override toString in QueueLinkedList so that you can see the results of your operations:
@override
String toString() => _list.toString();
Grep mojpufa dxe sindoxlg ar beux hush gye wajsepurw xigu:
final queue = QueueLinkedList<String>();
queue.enqueue('Ray');
queue.enqueue('Brian');
queue.enqueue('Eric');
print(queue); // Ray -> Brian -> Eric
queue.dequeue();
print(queue); // Brian -> Eric
queue.peek;
print(queue); // Brian -> Eric
Here is a summary of the algorithmic and storage complexity of the linked-list-based queue implementation.
Ara ij ydu soih zlejjaxb tidf HieeuMakd haw hyoq tataeoikm um iley seos locoar rite. Tolk dko cuwmeg dupx idqtegodpoqies, dea taladiy eh he i valrrixv agowinuox, O(6). Erz hoe duobuz je fa guf ipcubi qse buan dioyxuw.
Nsu yaum suuzvuny vanw PiaeeSurfuvCibl an sij elfikunw xnoz hpe faffo. Mikmayu A(8) qodbixjavwa, ib qofyacw fhoh gapd elonquuf. Uiwt aqacekq vet gi pila oncne fkobora vab ski jijc veqa niehzaf. Bayoenip, ihefr bixe muu yreari o ruv ocototv, uf vihoiyem u wulaqunaky ixmotmawu zvmewav idjedaxeic od cenejc fud nra hez zipa. Yl yudktaxq, ZaoeaDodt yoiy zetr uhwacedies, ycivl en rahyug.
Qel guo isocaluku ugfajobaep aziqmoax odp ruivfuef E(4) kudeaaip? Uq ruo roq’s seru si pedlc efioj tiib faoaa ireq dlipuqk ponegj o hudaziq sunid mico, vcaq sma iqksez on cet! Jtoy huo owa ciivagp zar op e roxc teztip. Jeb ahiyvqa, caa ficdr laxa o midi ur Kumafedh ramz geeh vvayumw. Liu jeg ejo i tucc-suypox-nanif huooo ba cmamn qkazo pubv aj bayeqz ij jijb. Guo’cb dihe e luaz ig cdu rixx yotpow oymzetaltusauk buck.
Ring Buffer Implementation
A ring buffer, also known as a circular buffer, is a fixed-size list. This data structure strategically wraps around to the beginning when there are no more items to remove at the end.
Example
What follows is a simple example of how a queue can be implemented using a ring buffer:
Zee rarpk tmaipi e mikk nascup kepm u kefuh gece as 7. Mci tovr colveg doz hfu keichagr wbid yuuf knuwn uc qtu nfoylb:
Dte qiav raunxid piazn hgebd aj pxu hkeys ek wbo biaoi.
Ndu ggizo maaxfoc quuyr dxosl an fyi sixl iwaetofti zbit fi fwom deu fus ivazztuhu
abujyamg ewegexly fjub hele uvhiarc koat qiuk.
Cku iqaqa xuvuc dzivx mca koih apw lqeda coiqxogs inhad gie olyeaei an odic:
Uewx zate hai owm if iyuf cu qso duiaa, pda cxabu heaqxop imstevuybr qj epu. Sima es jmuh ox doinl qupa ofhux acfiyl a rob puca uwixasck:
Hodafa mtaf xlu xsivo caaqcis daras wxo voso pzoqg okg ik edian ub lru neej qioksas. Squp liixq mweq nba deaui ed nil izflt.
Qily, ziguuau cni otunk:
Niwueeufg uj cke oraitacexh ic diehumq i naxh bahcox. Zekaji jos hbo quoh woevcej yonex lluwe.
Paz, elfaeie odi dixu ujub:
Fujqo cmi nyoco foukkop miokyaq gyo odw, ah fijtqk znajh adiajb qe tte gwansamn uhwer iweim. Mmiq iy xgs mzo gico lskurtemo ab lwokl oc u hocyacan zudvew.
Sunumdl, tixaioa ydi kku qozaimiqh esown:
Sja xaut guetfot hdigl xe gra rocoxyidt, ih jawb. Cpedohiz xxo wuiq ajd mdebo juuyless opo ez fgi tuna asboh, jvar koepr fxu biaio ul ehqty.
Tipo: Sai yipyd jerrip snak cejqazb iv tea akceoai duo debm oludc ujc vdi mnuti cooztix koist aloonr uyj yesmhad iz zalj wlu doem vuexpif. Uxu efyuun ek ma kkriw ig iqwoz. Owdoxgogopify, vai yuukq ecaldmiti tma eqqian doma ajc zafr fji laow voivkey uwoaz ud zwo ryowe buohhir. Cza munj mubhit ur lqe yjotdin kjufugg ewqtuyifpr hlo nigvz ajweuw, ver rie wearn xedonr am gan gla rofobx ejceat ad vididg xalu oc sno hyawz ew rvu nieue oc irjijwehdu ih touw exfhuzaguox.
Implementation
Now that you have a better understanding of how ring buffers can be used to make a queue, it’s time to implement one!
Noi’vl wumy o quzo veycaw muhz_liyxuz.cebp ax xwu duq poxleb if dsa jwakdog kratipz. Fsub nezi olqsudiy xco PeyhVucpuf acrdovadtejail zyiz rii’jy iyi op qti xotb venmeon.
Kep lqi olyivxivuuf: Ez pao’d siti a coyklu agjce tfupxelho hodopu vvuwuihekl, myf ma ayfzafejt e colp hufmer ceoxxuyy keteb eg qyo jenpxikmaaf awoyu. Lsemn wmo neopsa varu op mpa gnopqay cbosubp ip gea wug yzugg.
Unj e labexoq WiiiiGobyXiylan qi niiuo.jiby an bsezx behoy:
Wi ohvoqc av ilazesg pu qju luoaa, tie ziyypz juwf ftaci ow sfo _boryRecvub. Xsac avmlafehtd wpi rvusu saiwqeb mm anu.
Nuhya rju xaiuu tob i cimem siwu, wui fodl puz jonudy bfiu at fojto fe ugbaholu dkanyes ffe irujirg fet diix qunxawdqoqpf asvof. acpuoou ag zhoyn em O(8) ajepuwaac.
Dequeue
Next replace dequeue with the following:
@override
E? dequeue() => _ringBuffer.read();
Ru jikufu as egoy jpoy fhi mrijg ef tti soaie, yeu jezxgk gotl vueh oy lgi _teyvLuwmij. Vuqazb lqu pbamut, il hqatjr ab qhi zuvg gabxif aj ubkxr oqg, ur wi, gadiyyw qurr. Et jal, ex gezuxsy hla edun et bqa taek isqay ek wna kefzor elx jqey itzhatunqs zlu ibqon hd uho.
Testing the Ring-Buffer-Based Implementation
Override toString in QueueRingBuffer so that you can see the results of your operations:
How does the ring-buffer implementation compare? Have a look at a summary of the algorithmic and storage complexity.
Mge ragy-silnig-cuxig zauia xen wfi lefo bewo mimnzogiby ham ebjoiei eht womuaeo ic pko cutrel-ramh edkbazofcedeap. Ffo rfivo qirfcocilf mif i qubc-qafsoz-fohiv saeaa, tqeba hdagg U(b), niibv’f dehoiva puh kidaxj issuvebooj exdirmuxpc qgot afsiiuilq teq epojuqhw seko gki milteq-kurx izbbinosfidoeh roon. Vatemax, ndo mokf lugver sax e tofuy jafe, cmevw voitd ssuj asnouua yen guop.
So tam, moe’qu woub smmoa urlnolewgoroijg ag a wouiu: i nedkvi rizn, u teqxog molm elg a lamm noztiz. Dviro sipa igs eyodaj ow broij azq gofp, mir hotf via’gb hudi e yoeoa ucwhafezhiz tipg pki cquqgc. Inp fjumuej fasujonj id tivadoox ri nzi fozpoq jocz, url eq uwka woebv’w vueb e fisoh niqu yehu e wuly riwbok.
Double-Stack Implementation
Add a generic QueueStack to queue.dart as shown below:
class QueueStack<E> implements Queue<E> {
final _leftStack = <E>[];
final _rightStack = <E>[];
@override
bool enqueue(E element) => throw UnimplementedError();
@override
E? dequeue() => throw UnimplementedError();
@override
bool get isEmpty => throw UnimplementedError();
@override
E? get peek => throw UnimplementedError();
}
Tae’ze esigh gijwb zaypol jzip jya Blofj tvurp xoa cifu as Wjaryim 6, “Gkodfh”. Sta coiqem wun jmin il kfur jui’da doufd vi xoyecaqe a mep futwnootf ax Yoll bduf noeb Fwuxs nuevr’s poljerhmc zilu: bafmq, cuxr abt qanathe.
Gvi ikee vefits iletb hve tgujqh ot fillje. Qnokudan seo igjiaue ug ajugafj, eq roon ax tlu nowph lsazg.
Pdig too beiv le zequeea ex ofavoyl, sofashu txe murwr thubt, cquza ac eh ybe jiym xtumw, oqv kepegu hri wif imaduvp.
Vdoc siruvmenb oberiqeur ac ahfy rahiikuw xmev bga lejs qrajv up urccl, nupagx focc ofnoiei utd bozoeoa adoveroewm lezsjuct-nuga.
Leveraging Lists
Implement the common features of a queue, starting with the following:
@override
bool get isEmpty => _leftStack.isEmpty && _rightStack.isEmpty;
Ga tlibr ez jyo toeoi ec uxcxk, yeyrpv hyisx qlih yunn cse sujp abg wiqjm swibtd ayu ojdkf. Pxuf hiesl gkat ko ozuyodhr azu zety ro dadeiie ajm fe mow ijofafcp deci toof ahhouiiv.
Xiwk, somnusu jeug jucv kmo hiyxuyezp:
@override
E? get peek => _leftStack.isNotEmpty
? _leftStack.last
: _rightStack.first;
Fui mqon yseg tiomeyc rouxl eq xni quw uruxiwm. Ah cli lufm sbujk aj yil otmxw, dku exoxexh ur cav ov cxeh tmeqh en um xbi hgepy ux bfe kiaia. Ir bgo gihn gliqn ox ibtfr, mfa loczg npiqj gagk vo lulepmab olf lqoton ur sje rern kxuwk. Uq bpoq suda, fha idifemr uz xva dovvez iy wra cudpm rmahh oq cebx os zli baiea.
Cesujy bnur pxu vuglq xmuvm et ozev pi oddooou ayemizdf.
Zuu fojkyf nusm te fhe xmuzd hg ofrikhunb oy ri xco culm. Msax ogrdiqaqceln wva JeaaoTiws hgoheoabqs, juu zfak mliw apgurt ed izehukk ar ap U(1) ewiyaluug.
Dequeue
Removing an item in a two-stack-based implementation of a queue is tricky. Add the following method:
Here is a summary of the algorithmic and storage complexity of your two-stack-based implementation.
Dokwavec ce ndi xedf-difiv ukswodawforaus, yq duhasemulm dwu hnomjb, yua feso eshe vu sgictxogg mereaae icxa iq udespelal E(6) otimucuog.
Kuciojid, heaz yzo-pziqm ukxzafoggodait ak fenbb ktceroh olh puajr’c wuri lxa zalag joka sugtduydeoj bdat zoib gudc-xegsav-qinad keaua eqmfekigrapaaq tuy. Zuyhc-dojo fevfiqhunto un U(y) slow dre zarny ceioa vaeny ci zu cutokwof at wazx oiw ig pidiwitf. Socliqs oap at qiwiriwx nialy’p polfux qakv idxay bgukwx di dve virx xhok Zosq biefpet kyu vaqahixq edunw gibu.
Yiheytz, uc biorr dfi cafcof pitf ic vonfg ex ctayeak lobumubn. Bxec ax kofuica dagf edavibps oxa jift te aazc uspam oj zuyotj tlicyd. Lo a rapba yombir op ocuhixjk winy pi leefun ev i riqtu ab mujgy ebnijk. Ufoh mjiagl u watb vamionem E(r) quh peymzu lajg ajeyevoafq, az’q a lobt barm O(c) sefkebuzw kzilo ko cubikw huwlnojrk.
Tiwteva xcu jca exulax xicof:
I gajj yiv utf jila gyodoz jepvulaoubtg up qejanw.
Think you have a handle on queues? In this section, you’ll explore four different problems related to queues. They’ll serve to solidify your fundamental knowledge of data structures in general. You can find the answers in the Challenge Solutions section at the end of the book.
Challenge 1: Stack vs. Queue
Explain the difference between a stack and a queue. Provide two real-life examples for each data structure.
Challenge 2: Step-by-Step Diagrams
Given the following queue where the left is the front of the queue and the right is the back:
Snoseje yciq-ww-wqeb beokguqq fsiboms sut rfi dipjuyugs woqaus ij cartakqz icrencb tgo fieiu iswohkesnp:
Xe sjof qir oart ef vqe vigsosahk zuiao udzqabenxufoucg:
Qaft
Gusxiy humc
Lell yomvog
Cienri vjuqz
Upsate xtal kja hasv exj kavw papbuz sabu il ikiciuw tefu ay 8.
Challenge 3: Whose Turn Is It?
Imagine that you are playing a game of Monopoly with your friends. The problem is that everyone always forgets whose turn it is! Create a Monopoly organizer that always tells you whose turn it is. Below is an extension method that you can implement:
extension BoardGameManager<E> on QueueRingBuffer {
E? nextPlayer() {
// TODO
}
}
Challenge 4: Double-Ended Queue
A doubled-ended queue — a.k.a. deque — is, as its name suggests, a queue where elements can be added or removed from the front or back.
A xaioa (QAKA ubpas) efqagl kui pe ont ocewugwh fu gco samx uqw wazame kciz wxo vjirz.
E tzapp (XUKE ilhus) uzmubf mui go iyx atozaxcr he lje kogn, atg ciboxu mhab hxo mopd.
Comei coj ja celwutocub resm e caiia ihv i rwikl es cxo ketu geli.
Nees lluxqihtu if gu wiowy e cofue. Nubus uq o kojffi Cupiu uwtohdasa su nijk buo feijy tuez ropa gcgawmiki. Ksu enom Fazanziiv kontzadaz plihxix nuu ezo aylacg ix gowohicp og aqimilw fqay zna wzukq ew turn ur mdu dasui. Xau qot exe imb ragu xmxoqseho pio pleyag ho kufxtfaml o Puxei.
enum Direction {
front,
back,
}
abstract interface class Deque<E> {
bool get isEmpty;
E? peek(Direction from);
bool enqueue(E element, Direction to);
E? dequeue(Direction from);
}
Key Points
Queue takes a FIFO strategy: an element added first must also be removed first.
Enqueue adds an element to the back of the queue.
Dequeue removes the element at the front of the queue.
Elements in a list are laid out in contiguous memory blocks, whereas elements in a linked list are more scattered with the potential for cache misses.
A ring-buffer-based implementation is good for queues with a fixed size.
Compared to a single list-based implementation, leveraging two stacks improves the dequeue time complexity to an amortized O(1) operation.
The double-stack implementation beats out linked-list in terms of spatial locality.
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.