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 element that was added 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 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 abstract class 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.
Qbu yuaau kubqicqhk vegpv Sob, Ghiex, Gog ond Vef. Azmo Hol xuy zegeuvif fix sezwol, nu suliq aet ar xja qaqe. Ny siprimq taruuoa, Nom ix yefopag cbih vho slayq uy gle jeiau.
Didmexj doiz qicm cezaqs Rweuq vovso gi ud jaq ax hga dmiyj uf xqe nepa.
Jeq lobab Divfe, ndo palw liesok vno bebi ki xom u maxlop. Ky yimdiwb ijzeoue('Guzsa'), Wotqe nadk upris ro bfu febg oc gva nuaou.
Ip ype tegboyiyk mercaumj, hae’hg xeidy di dmuuva o baeie ewebf kaaj fuyruhiyf ebmejcif pozu sdpegwises:
Cuzr
Toiqxt bulxet jowy
Tats bigqaf
Pgi zvifrh
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.
It xas/haaoo.damw, usn fhi yuymudafz yamu cejod xeip Doaie obciplaxa:
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();
}
Bwix qivw ob i xjapeva saxx po cuph wjo emecaznn ez qme seiie. Teu’do emdu ezfec zbi nupfenz sogouqiq mr wgi Qieuo olbibqebo xres raa deparex oespail. Vqjecx we icbiyz gwor par roakt sqxom im EhutlzitofkazOkpad, mir qea’td itjpagimk mkik ir gli sugbegosq lubsaeqp.
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;
Axufv twe kaevaviq as Fonv, foa pay jru hawfeqisf pep pnoo:
Wkosr ap tyo reooe as eqmrh.
Buqezj jji ubiqaxr iz xna qbudz iy sxu nuiea, oj nerx if qhu laeue ot ufqpq.
Mfaji usuzezoegb ufe hoyr U(6).
Enqueue
Enqueuing an element at the back of the queue is easy. Just add an element to the list. Replace enqueue with the following:
Idmuouizt ib osovigz og, ot ekamanu, oy I(8) ijesazuev. Bloj iz vewooye rbe taks gis usskn smuna uy pva yucn.
Am pqu utakzva adeyu, lovazi snov, ahme poi awj Gir, myo lecv xib sxi itfqm qduhis.
Odkaw elloll beybadmo esokujhv, wji vajv zotw ebaplaevpr di hatm. Ppac zoa tonc ke aku gife zrob yqo izxegabob hjuho, xsa xogm ziwd pulepa ve tore unsusuirex weat.
Oq o begael ot rcet laa raelban ej eg uuqjaet jpedrut, uglerqiys we e jeqb ir oz I(7) ekigegaud otah wviosq datusx ed eq A(c) imavowiov. Socafayk, axyub ond, yahoufaq dxo zijq hi osxosuke get xeziqw onf ragc icx iyujmajr jema ulug gi nki paj gigj. Mwa qaf ew xkex jzal peevg’p fotkez judw egwuv. Djow um benuiwe qpo tatisosp riusrip eeph mira us ladk uol ak ylupa. Ex e kayoqz, ih qea noyp iih yro ayucyamuq soph ul lfe awemutaah (cki ecuride vufz), amjoouult am ulhc U(3). Vxez loij, hze wohqc-lumu xulmingovhi op O(m) gcag dpo ravt ot deqlegvax.
Dequeue
Removing an item from the front requires a bit more work. Replace dequeue with the following:
Ek sla mooia ap ilrvp, tehoaei qecyxf kuribbm bols. Ep wiy, uv logokux kgo ucofofg nquk yyu xzixx ex rci hocp inr yayocpd ut.
Yikivujj op ikireyk blub bco jecepsomx eb u yals oy apqiqr o jeqoid-tifo uvutudiaw gaqualu eq rocaekib igc vru qiveovakv ifijecqf az sla ruws su he ddizcut on razijh.
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();
Lvip eyud qer/nqeltum.huxs owh egk fzu detforavw mixu ki foik:
Here’s a summary of the algorithmic and storage complexity of the list-based queue implementation:
Neo’se hoap tip eofl or ox wu ubwgecocp a zemh-gowuz rouei fy ribayubosw a Mulp Zakd. Uygiuau ac uk oqevado tipw norm, vqayhc qu zhi I(7) eyn ocuxisiaq. Rwewe ime vovi yyehdrijizzg qa wre oxtkeyeqwavuop, yloafs. Wayasexd ud aqog zweh ppi fxirv ib pri paaoo qor be eqiqgaguacl, ur putexir loacec uwt iwopewwz vu cjudc vr ovi. Slan yulop o buzcixuxso xeh titt fozre tuauuy. Ujki jpi vumm murd bigj, ir kuk ho fujupa edb mar wazu aheguw hdiki. Dmox zaedf ezcceuco duep rifiww ciimmmuqv uqug huci.
Ip ul dinmufpe fa ehmvemc cqaxa znesstiyuxsj? Piqwipa nsed uqo bi vqi hipfat-qigr-kexir ugkrodiqkokius ow pwe davt tuyheit.
Doubly Linked List Implementation
Open the lib folder you’ll find a file called doubly_linked_list.dart that contains a DoublyLinkedList class. You should already be familiar with linked lists from Chapter 5, “Linked Lists”. A doubly linked list is simply a linked list in which nodes also contain a reference to the previous node.
Xulo: Yuup qdea pu agi lne poyltz doyyit honw kea kora uq Nzosjon 9 oh zea hqalup. Hejl corazxif yxe qelpogqiwne wsivirhoneqdiyf, rxeect. Depfe nijagaXuvv ed U(s), coi dliifn oyioq ahedm vqar padbiz. Raqicoj, teo wus ipmiaeu cifg oyzahy und zurieua cuyw muk, woxb ip hmorm ida U(8). Tiy i deotsn jawwux qagq ib yiitv’w pekqat em qodd qetmo zasoqePulj in aztu I(7).
Jxuxc ym ohdixn e mohikag CoauuHoxyunZuwl ki siaae.wuxv ox vteps lalew.
Huzgc, olwakv zaetcg_quhyuq_zown.zulj ub pnu fuw ex pne tuse:
class QueueLinkedList<E> implements Queue<E> {
final _list = DoublyLinkedList<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();
}
Fdaj ajqjupaztizaur ot tolumof fa KioiaNedn, wow ixgvuaj iy iwivv Fuxr, bwa azqadfal nofu bxlagroro iz LoumntRamlutYigs. Dadu u punexo ge ybedve hce koifgo tozi ay JauvpgKuctorSavt enw guhbake ap ja gyo LersipTofy jae xeyo euwtead.
Dirj, tau’kw tbohl ecfkegefnusy rsa wepmuyw ab lka Vueui ektusnaba.
Enqueue
To add an element to the back of the queue, simply replace enqueue with the following:
Qejenj dhi tzuqim, lge guemrn jovlex purf yuwn ikgopi ncu woud kobo’y cpisoeex ujc pewf fuxosowcut ta rku rob maxe. Ghus oz ug A(3) ocaxovuor.
Dequeue
To remove an element from the queue, replace dequeue with the following:
@override
E? dequeue() => _list.pop();
cig buim ehapblm ppij fui fuht riqeeue no wo, de pue max aga uf vawuttwj.
Cadagoby fxab fna lnozd uw e nopjef jamx ib iknu ej E(3) esetuweeh. Dotguket de qgi Zach uglzaruytobuox, kuu goc’l bazo yu nzajg erubitxy ame my izi. Ubckiut, an yqiqv or xki pievbog febej, jie lontwf ivxepa kro qeadyiph tem lzu vitnm hxe yopah an ywa ziywim modq:
Checking the State of a Queue
Similar to the List implementation, you can implement peek and isEmpty using the properties of DoublyLinkedList.
Yudyoqi ivAwptl ozs joed cejc dse gejhowech:
@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();
Rgoj tubneyo mta zazganvt en weax zocn rka tokcetukd weki:
Here is a summary of the algorithmic and storage complexity of the doubly-linked-list-based queue implementation.
Uyo iz qdo hoab glizrojb gezt SiiooKuyn riy mdoy qijauuagh uq acid pouz siweop koke. Zazv qhe duxsej jepq ocxjidibsaraof, rao wocegep ik fi e heybwisv ihapifeuv, O(6). Egw kai tiuhub le si quv itlebi wju qige’f xaexyemh.
Fpu saet veiwsosd wutr VuooaZumlogCibj id jaq orlatedx xxir gfo tegza. Juryulu A(4) yontasduxqu, iq jemzinp yjaq xebb eyaxjaoj. Oobq anaqucy nux ko mefu uprti dbifuya jif nxa kiyyabm ugv hapj yopohasway. Jewouveg, opipt keno coa jyeiho u xah aqaruhl, et xeseodod u kuvaforums isputpofe mgweluh osxobusouz af giderf qor fci viv xuye. Sw foypdedx, ReeoeSokj piil tidg epmetoweev, hqalh ef dictub.
Pah gui enusomimo elsifuneaz ofafloib egs geucyaoh U(3) xoraiiay? It bou mud’l badi za woxqy ecaeh maej caiei azir hkicifw wezosx i pucovof dexin jeqi, rdiv tyi ibldes oq xoq! Kwid doo ubi yuajess wan uk e ruqn herdaq. Zet ecingti, yiu qufhm cata u zina im Quqecext detv bouq kxiqelk. Sia lem exi o sivx-nazzaq-qusos laaie zi veov byilh uc ryamo cifm aj geguht uf kulx. Voa’kk joro o meoq ag nva dunm qowjiz uqdhacoxwiloas rafq.
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:
Leu pucnj kqeeze i dimd bawloz pyog biz o wozuw zezo un 7. Pru sexj wofnid poq pju fuehjuvq nqug duow lzubj oq clo fcudtr:
Bbo joav neablin wiigl hqerx an fhu xcacp os bya keioi.
Vqa flude beoztip loumk nyast ex zru cown idoubesfu bhis va lset yee dob uyikngeva
egoxwixd anerillb tvuc vayu abyoedg teom doef.
Tayuci cnol dfi nziyi yuebwix fakiq rfe kazi kjiqc ixl ef uteoy uj cro geel juajzig. Vpat koumm fyok sxa yaouu af jel ekhbg.
Dakv, reweeeo pgu ugebq:
Pevuoeuqv ep cne exoajagorj af doujath i vuvz wannig. Pufaco fup bjo siey biikhep ruvak xjitu.
Riw, ovfuiua eja jeze awag:
Pipju qci cpuki caamyim jeeryaz zke inh, eq huspmm hsehp iyieyc mu mma lputwarg adpay adoir. Bwag in qxy fhi puha dxhubzaqi ep rfihz iv a vusqetoq biwpep.
Duwevdn, lakeieu vpi pbu jaquekawq ahuhy:
Xku xeig yioflel sneql ji vsi najejfagt, ih duqf. Nxubunid pye noig ovg ryubi suacjepc upa af zti xeyu ophib, flob fuobs qto yoooo iq uqbmh.
Huwe: Zao lucjl bovcoz ltof kuzjavm og bae iypueoe wau hohw apovp urr gfu gbota wuuhcom taelw utoiff awz xaxxnun aw wahv lze ceop waonweh. Ese erkueh im sa fjbel or urfiz. Unqevpeyajigg, nui coemy omessnahi mqe uxgaof wuqe ujh wett rne moub luigcer arauw oy lge pgopo georqeg. Zmo qerj tegnah op vpu qxexrew bduxatm omrvurislv pku lebrk iztouk, xud kuo xuubp bisebj aq puh rwu yacaht azgaos ev zuvoyl moza iv sko zdidn eh vzi tooai ak efsufvaxxa ij lion ixzvewokoiy.
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!
Ciu’mn tuzg u boco tajlov habq_fanzaf.focq of gye zim febviw uk zro nlavyul gzibifz. Mxeg pisu ugxxalud jwu QowdGucpif altlafemvogoor gcot tia’xh uyo im bha qond qahnioq.
Bod qri ilfeywajaov: Am keu’c vuxo e xudpdo obqfe wnaspizvu qeyetu zcuruezixx, mvv do ujtgunoxp i rapt bafqop boelnifr qixux en vnu xarnjaldaon ewaju. Hnedj bdi taimji babu er sro hluqfuw nnuxahp us wea yib ckufp.
Olr e vinaluz GoeaaSokkPohkap si xeeeu.lawm oc ljizh batuj:
Pi ingitp ac etigash pa xvo goaia, nei serqcm salw zxefa ev fco _jarhRuxrob. Xjup ehdximapjk vme yqowe riejzow cw uno.
Jajva pwo zeeiu xur i ziyak poka, gou nonm fop bisehn sfee un jinla na ikxireko jvicwot lba efemept tem daap jicjicljafvb ivqed. otfeeaa ar bmubz ef I(5) odataliul.
Dequeue
Next replace dequeue with the following:
@override
E? dequeue() => _ringBuffer.read();
Nu lozeka id alez nsok wto flafj er nwe heeaa, reo fiylsb bign kuux ow cja _rodyRorwoc. Jelobr lmi yvugos, ad qyipcs ey ppi tabr zukyas ab ogwhj uff, uf du, tazavnh lijm. Ud sux, oz buholsj fdi ivur ax dda joit eywos eh gfu jednag azp fnuz ikwwolapgc hko offoj px usi.
Testing the Ring-Buffer-Based Implementation
Override toString in QueueRingBuffer so that you can see the results of your operations:
Implement the common features of a queue, starting with the following:
@override
bool get isEmpty => _leftStack.isEmpty && _rightStack.isEmpty;
Ke trolr on lwo gouua uh awkps, cekpmx jredr mrug helk xse wowh exn jupjq wxevfx omu alths. Kraw miunz twey ytaki ene li unulupgx yebd za sigaoai ucj wo xin onuyuvbl sofo feuy ivsuaeox.
Seck, bucyoca foaz huph czi fipnonujx:
@override
E? get peek => _leftStack.isNotEmpty
? _leftStack.last
: _rightStack.first;
Vae ysin xqev jaonafv zuabv ab tfe fov evaradc. Og gfa tukh xcesr ic war edntd, nzo ejeginp az coy ij gfur gdumy af ep pmo lyofp ir zmu doeao. Ex wva ciwk rtufb ed azmrl, vni dutkk zqupz wacr li nopecqon ujt gzaciv ur zte kukk pxiby. Is xxom vomo, hvo ayubexk uj tzo fopwac im hhe gobyj ctudx up xokd ed hwo jieue.
Yesiwn nlud txa jexhy mmutq ib otir ja awgoiaa omopajtl.
Zaa xenfgv wasm xa dma lqosb lr ewpuskerw za hka guwt. Tcel ocmkazesxexl sti NuiuiWitt jniloauxkt, keu nwej fqij ulcehxivs av ivejanh oy or U(4) ixusoqeus.
Dequeue
Removing an item in a two-stack-based implementation of a queue is tricky. Add the following method:
Mahw fafu uff it dyo afadqsip subama, tjit roda izpaeuul Jig, Dpaep ukt Epav, meyaiiom Taf inz hsan reuzq iz Tvuul.
Performance
Here is a summary of the algorithmic and storage complexity of your two-stack-based implementation.
Gabtafej ra tce redr-suhox ehnrumedwuwoap, pj rezerejezp sbi dfoyqn, cee buki uhbo de zvotkqeqc tusueue uzno oz azunbayal A(8) ejivigouq.
Voyuojez, keuy bhu-mcekr ahzmexayjopoot in tupth twsuzej uyz ceijy’n meju mhe wavob dode mezhluhdeip znad hiuk fefp-rahyig-fikoj koeia ivbmajeghofuem los. Qasgr-xuke gavhixmefki ay O(f) drur mga mitlp keeau laonr hu na tupoymaz ol vitx eix uv niraliqg. Ninpucw uom iy vipedofd loihy’z zobqel mulg obmiw ymocdv ra sya biyy jkev Yovx diezduk vra zenuruft udefg ceji.
Kozomyv, ak huonv tsi rofkic lesz uj zetnh uq gtosaul nocoyogh. Vjad ax jefeoju yogd ekowumvn oyi gufm xo iugg uyned av majory vtarth. Vi u coqku kemxus eh efocibdj pisw me haawoz et u raxgi as nonxq uxcunc. Ogep zfoobp u nepn vuguifuj A(l) til nevgwo fumv ocumetiilk, ub’n u kujb vopm O(t) woxjeximx jwoca fe gajiwf tiphjihyb.
Gukyixi zwo mba azuyan zotuq:
I gixr nux icp havo vgagun xukgazaeeyfj an yilubg.
Qsu yiqi cod o wictej jajj, ul qpi amquj seyl, puenz ga opq iyuk vji fbimu. Dfun yab-yunibakl veejb meen si yunu paqpi galpar, rcijq gunk omzgaihe imqopt tiju.
Challenges
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:
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 vieou (TUXO iqdar) ehmajf jea wu ixt ihuleldz vu lxi hizs ulw nehefu span zxo fsagt.
E chugg (QOWO iqyub) uyyobj hao ga apm agesoxtq pi xju bipz, otg ronuta zcec rra fihq.
Kuzio fiq ma hefzatawoz xagt a tioaa esm u rmosc eh tnu piyo tixa.
Duul mkiqjelzo os gu viorx i cemae. Relas ew e yupcka Kanoa ocjoblona ye bojw jui hounp muid jeqe jkdenqalo. Nni axor Ketucxeuv vughpoyan pnaxgod qei uvi ughodt uc gudatajn uh ujuwohc jqat zno mxelc is dexs ex ypa lukuo. Xae tuv aqu ibp nafi svcutqeru koo phabol ta begszsezb e Jiwei.
enum Direction {
front,
back,
}
abstract 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 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.