Double-ended queue: Difference between revisions

From Wikipedia, the free encyclopedia
Jump to navigation Jump to search
 
imported>OAbot
m Open access bot: url-access=subscription updated in citation with #oabot.
 
Line 1: Line 1:
{{Short description|Abstract data type}}
{{Short description|Abstract data type}}
{{redirect-distinguish2|Deque|dequeueing, a [[queue (abstract data type)|queue]] operation}}
{{redirect-distinguish-text|Deque|dequeueing, a [[queue (abstract data type)|queue]] operation}}
{{Distinguish|Double-ended priority queue}}
{{Distinguish|Double-ended priority queue}}
{{tooshort|date=April 2022}}
{{Multiple issues|
{{lead too short|date=April 2022}}
{{technical|date=April 2022}}
{{technical|date=April 2022}}
}}


In [[computer science]], a '''double-ended queue''' (abbreviated to '''deque''', {{IPAc-en|d|ɛ|k}} {{Respell|DEK}}<ref>Jesse Liberty; Siddhartha Rao; Bradley Jones. ''C++ in One Hour a Day, Sams Teach Yourself'', Sixth Edition. Sams Publishing, 2009. {{ISBN|0-672-32941-7}}. Lesson 18: STL Dynamic Array Classes, pp. 486.</ref>) is an [[abstract data type]] that generalizes a [[queue (data structure)|queue]], for which elements can be added to or removed from either the front (head) or back (tail).<ref>[[Donald Knuth]]. ''[[The Art of Computer Programming]]'', Volume 1: ''Fundamental Algorithms'', Third Edition. Addison-Wesley, 1997. {{ISBN|0-201-89683-4}}. Section 2.2.1: Stacks, Queues, and Deques, pp. 238&ndash;243.</ref> It is also often called a '''head-tail linked list''', though properly this refers to a specific [[data structure]] ''[[#Implementations|implementation]]'' of a deque (see below).
{{Infobox data structure
|name=Double-ended queue
|image=
{{image label begin |image=Deque-01.svg |width=250 }}
<div style="color:blue">
<div style="position:absolute;top:24%;left:18%;">Input</div>
<div style="position:absolute;top:67%;right:18%;">Input</div>
</div>
<div style="color:red">
<div style="position:absolute;top:24%;right:16%;">Output</div>
<div style="position:absolute;top:67%;left:15%;">Output</div>
</div>
<div style="text-align:right">
<div style="position:absolute;top:6%;right:4%;font-size:110%"><i>as a queue</i></div>
<div style="position:absolute;bottom:6.5%;right:68%;font-size:110%"><i>as a stack</i></div>
</div>
{{image label end}}


==Naming conventions==
|caption=Representation of a double-ended queue
''Deque'' is sometimes written ''dequeue'', but this use is generally deprecated in technical literature or technical writing because ''dequeue'' is also a verb meaning "to remove from a queue". Nevertheless, several [[Library (computing)|libraries]] and some writers, such as [[Alfred Aho|Aho]], [[John Hopcroft|Hopcroft]], and [[Jeffrey Ullman|Ullman]] in their textbook ''Data Structures and Algorithms'', spell it ''dequeue''. [[John C. Mitchell|John Mitchell]], author of ''Concepts in Programming Languages,'' also uses this terminology.
|space_avg={{math|O(''n'')}}
|space_worst={{math|O(''n'')}}
|insert_avg={{math|O(1)}}
|insert_worst={{math|O(1)}}
|delete_avg={{math|O(1)}}
|delete_worst={{math|O(1)}}
}}


==Distinctions and sub-types==
In [[computer science]], a '''double-ended queue''' (abbreviated to '''deque'''),  is an [[abstract data type]] that serves as an ordered [[collection (abstract data type)|collection]] of entities.
This differs from the queue abstract data type or ''first in first out'' list ([[FIFO (computing and electronics)|FIFO]]), where elements can only be added to one end and removed from the other. This general data class has some possible sub-types:
It generalizes both a [[queue (data structure)|queue]] and a [[stack (data structure)|stack]] : elements can be added (''enqueue'') to or removed (''dequeue'') from either end.<ref name="knuth">[[Donald Knuth]]. ''[[The Art of Computer Programming]]'', Volume 1: ''Fundamental Algorithms'', Third Edition. Addison-Wesley, 1997. {{ISBN|0-201-89683-4}}. Section 2.2.1: Stacks, Queues, and Deques, pp. 238&ndash;243.</ref> It provides similar services in [[computer science]], [[transport]], and [[operations research]] where various entities such as data, objects, persons, or events are stored and held to be processed later. In these contexts, the deque performs the function of a [[buffer (computer science)|buffer]]. The additional flexibility in managing the order of elements or the properties of some implementations may allow the improvement and optimization of certain algorithms.


*An input-restricted deque is one where deletion can be made from both ends, but insertion can be made at one end only.
As a general data class, the deque has some possible sub-types:
*An output-restricted deque is one where insertion can be made at both ends, but deletion can be made from one end only.
*An input-restricted deque is one where deletion can be made from either end, but insertion can be made at one end only.
*An output-restricted deque is one where insertion can be made at either end, but deletion can be made from one end only.
Despite their limitations these sub-types have many [[#Applications|applications]] and their implementations may be simpler.


Both the basic and most common list types in computing, [[queue (data structure)|queue]]s and [[stack (data structure)|stack]]s can be considered specializations of deques, and can be implemented using deques. A deque is a data structure that allows users to perform push and pop operations at both ends, providing flexibility in managing the order of elements.
==Terminology==
The term ''deque'' ({{IPAc-en|d|ɛ|k}} {{Respell|DEK}}) is used as an [[acronym]] and in analogy to a [[deck of cards]] with which the structure shares some properties and the pronunciation.<ref name="knuth"/>{{rp|239}}<ref>Jesse Liberty; Siddhartha Rao; Bradley Jones. ''C++ in One Hour a Day, Sams Teach Yourself'', Sixth Edition. Sams Publishing, 2009. {{ISBN|0-672-32941-7}}. Lesson 18: STL Dynamic Array Classes, pp. 486.</ref>
''Dequeue'' is sometimes used but can be confusing as the term is also used for the operation of removing an element of the deque.
A deque may also be called a ''head-tail linked list'', though properly this refers to a [[#Implementations|specific implementation]].
<br>An input-restricted deque is often called a ''steque'' (for stack-ended queue).<ref name="knuth"/>{{rp|240}}<ref name=okasaki_PFDS>{{cite thesis |url=https://www.cs.cmu.edu/~rwh/students/okasaki.pdf |first=Chris |last=Okasaki |title=Purely Functional Data Structures |type=Ph.D. thesis |date=September 1996 |publisher=Carnegie Mellon University |id=CMU-CS-96-177}}</ref>{{rp|53}}


== Operations ==
There is no standard vocabulary associated with deques. The terms ''enqueue'' and ''dequeue'', borrowed from the queue structure, generally denote the basic operations on a deque, on either end. But papers and actual implementations often use different names.
[[File:UML deque.svg|right|UML class diagram of a double-ended queue]]
The names of the operations vary depending on the context, the author, the implementation or the programming language:
The basic operations on a deque are ''enqueue'' and ''dequeue'' on either end. Also generally implemented are ''[[Peek (data type operation)|peek]]'' operations, which return the value at that end without dequeuing it.
*in analogy to a queue: ''enqueue'' and ''dequeue'', or ''push'' and ''pull'';
*in analogy to a stack: ''push'' and ''pop'' on one side, and possibly ''inject'' and ''eject'' on the other side;
*in analogy to a list: ''cons'' and ''uncons'' on one side, ''snoc'' and ''unsnoc'' on the other side;
*in analogy to an array: ''append'' on one side, ''prepend'' on the other side, or ''shift'' and ''unshift''.
Unlike the associated data structures the deque is symmetrical. Its sides may be freely named according to the context:
*in analogy to a queue: ''front'' and ''back'';
*in analogy to a stack: ''top'' and ''bottom'';
*in analogy to a list: ''head'' or ''last'';
*in analogy to an array: ''first'' and ''end'', or ''first'' and ''last'';
*finally ''left'' and ''right'' preserves the original symmetry of the structure.
The full name of an operation may be a combination of the name of the basic operation and the name of the side: e.g. ''push_front'' and ''pop_back''. Terms from different analogies may be associated in a single context: ''append'' and ''pop'', ''push'' and ''shift'', or ''front'' and ''tail''. Finally some programming languages use even different names based on the underlying data structure.


Names vary between languages; major implementations include:
Also generally implemented are ''"peek"'' operations, which return the value at one end without dequeuing it. It is often named after the target side: e.g. ''top'' is the value of the element at the top of the deque. In the context of the functional programming, the ''dequeue'' operation (that returns two values: the removed element and the new deque) is not used. It is replaced by a ''peek'' function (i.e. ''head'' and ''last'') and a function that returns the deque minus the end, i.e. ''tail'' and ''init''.
{| class="wikitable"
! operation !! common name(s) !! [[Ada (programming language)|Ada]] !! [[C++]]!![[Java (programming language)|Java]] !! [[Perl]] !! [[PHP]] !! [[Python (programming language)|Python]] !! [[Ruby (programming language)|Ruby]]
![[Rust (programming language)|Rust]]
![[JavaScript]]
|-
| insert element at back || inject, snoc, push ||<code>Append</code> || <code>push_back</code> || <code>offerLast</code> || <code>push</code> || <code>array_push</code> || <code>append</code> || <code>push</code>
|<code>push_back</code>||<code>push</code>
|-
| insert element at front || push, cons || <code>Prepend</code> || <code>push_front</code> || <code>offerFirst</code> || <code>unshift</code> || <code>array_unshift</code> || <code>appendleft</code> || <code>unshift</code>
|<code>push_front</code>||<code>unshift</code>
|-
| remove last element || eject || <code>Delete_Last</code> || <code>pop_back</code> || <code>pollLast</code> || <code>pop</code> || <code>array_pop</code> || <code>pop</code> || <code>pop</code>
|<code>pop_back</code>||<code>pop</code>
|-
| remove first element || pop || <code>Delete_First</code> || <code>pop_front</code> || <code>pollFirst</code> || <code>shift</code> || <code>array_shift</code> || <code>popleft</code> || <code>shift</code>
|<code>pop_front</code>||<code>shift</code>
|-
| examine last element || peek
| <code>Last_Element</code> || <code>back</code> || <code>peekLast</code> || <code>$array[-1]</code> || <code>end</code> || <code><obj>[-1]</code> || <code>last</code>
|<code>back</code>||<code><obj>.at(-1)</code>
|-
| examine first element || || <code>First_Element</code> || <code>front</code> || <code>peekFirst</code> || <code>$array[0]</code> || <code>reset</code> || <code><obj>[0]</code> || <code>first</code>
|<code>front</code>||<code><obj>[0]</code>
|}


== Implementations ==
== Implementations ==
There are at least two common ways to efficiently implement a deque: with a modified [[dynamic array]] or with a [[doubly linked list]].
There are at least two common ways to efficiently implement a deque, [[Linked list#Linked lists vs. dynamic arrays|that are often pitted against one another]]: with a doubly linked list or with a modified dynamic array. There are many variations and actual implementations are often hybrid solutions.
Additionally, several [[Purely functional data structure|purely functional]] implementations of the double-ended queue exist.


The dynamic array approach uses a variant of a [[dynamic array]] that can grow from both ends, sometimes called '''array deques'''. These array deques have all the properties of a dynamic array, such as constant-time [[random access]], good [[locality of reference]], and inefficient insertion/removal in the middle, with the addition of amortized constant-time insertion/removal at both ends, instead of just one end. Three common implementations include:
===Doubly linked list===
* Storing deque contents in a [[circular buffer]], and only resizing when the buffer becomes full. This decreases the frequency of resizings.
While a simple list may implement a deque, a [[doubly-linked list]] is more adequate for its symmetry to achieve a fast access to both ends of the list (''head'' and ''tail'', hence the name ''head-tail linked list''). The obvious solution is to manage two references; alternatively the deque can be build as a circular list.
* Allocating deque contents from the center of the underlying array, and resizing the underlying array when either end is reached. This approach may require more frequent resizings and waste more space, particularly when elements are only inserted at one end.
[[File:doubly-linked-list.svg|frame|none|alt=A doubly linked list whose nodes contain three fields: an integer value, the link to the next node, and the link to the previous node.|A doubly linked list whose nodes contain three fields: the link to the previous node, an element value, and the link to the next node.]]
* Storing contents in multiple smaller arrays, allocating additional arrays at the beginning or end as needed. Indexing is implemented by keeping a dynamic array containing pointers to each of the smaller arrays.
In a doubly-linked list implementation, and assuming no allocation/deallocation overhead, the [[Computational complexity theory|time complexity]] of all deque operations is [[Big O notation|{{math|O(1)}}]]. Additionally, insertion or deletion in the middle, given an iterator, can also be achieved in constant time; however, the time taken for random access by index is {{math|O(n)}}. As well finding a specific element normally [[Linked list#Speeding up search|requires {{math|O(n)}} time]].
Generally linked data structures have poor locality of reference.


===Purely functional implementation===
===Dynamic array===
Double-ended queues can also be implemented as a [[purely functional data structure]].<ref name="functional">{{cite thesis |url=https://www.cs.cmu.edu/~rwh/students/okasaki.pdf |first=Chris |last=Okasaki |title=Purely Functional Data Structures |type=Ph.D. thesis |date=September 1996 |publisher=Carnegie Mellon University |id=CMU-CS-96-177}}</ref>{{rp|115}} Two versions of the implementation exist. The first one, called '''real-time deque'', is presented below. It allows the queue to be [[persistent data structure|persistent]] with operations in {{math|''O''(1)}} worst-case time, but requires [[lazy evaluation|lazy]] lists with [[memoization]]. The second one, with no lazy lists nor memoization is presented at the end of the sections. Its [[amortized analysis|amortized]] time is {{math|''O''(1)}} if the persistency is not used; but the worst-time complexity of an operation is {{math|''O''(''n'')}} where {{mvar|n}} is the number of elements in the double-ended queue.
[[File:Dynamic array.svg|thumb|Several values are inserted at the end of a dynamic array using geometric expansion. Grey cells indicate space reserved for expansion. Most insertions are fast ([[constant time]]), while some are slow due to the need for [[Memory management|reallocation]] ({{math|Θ(''n'')}} time, labelled with turtles).]]In this case as well, while a [[dynamic array]] can be used to implement a deque, a variant that can grow from both ends is more appropriate. This is sometimes called ''array deques''. This can be achieved in various ways, e.g.:
*by offsetting the position of the first element of the array in the reserved memory: the unused space is distributed on both side of the data;
*with a [[Circular buffer|circular array]].


Let us recall that, for a list <code>l</code>, <code>|l|</code> denotes its length, that <code>NIL</code> represents an empty list and <code>CONS(h, t)</code> represents the list whose head is <code>h</code> and whose tail is <code>t</code>. The functions <code>drop(i, l)</code> and <code>take(i, l)</code> return the list <code>l</code> without its first <code>i</code> elements, and the first <code>i</code> elements of <code>l</code>, respectively. Or, if <code>|l| < i</code>, they return the empty list and <code>l</code> respectively.
The ''[[Dynamic array#Geometric expansion and amortized cost|amortized]]'' time complexity of all deque operations with an ''array deque'' is [[Big O notation|{{math|O(1)}}]], thanks to the geometric expansion of the back-end buffer. Additionally, random access by index takes constant time ; but the average time taken for insertion or deletion in the middle are {{math|O(n)}}. Thank to the fast random access, finding an element in an ordered array is time {{math|O(log n)}} ([[binary search]]).
Each time the array is resized, the whole content is moved: the memory usage is then momentarily doubled (or more), and all direct (external) references to the content of the array are lost.


==== Real-time deques via lazy rebuilding and scheduling ====
===Purely functional implementation===
A double-ended queue is represented as a sextuple <code>(len_front, front, tail_front, len_rear, rear, tail_rear)</code> where <code>front</code> is a [[linked list]] which contains the front of the queue of length <code>len_front</code>. Similarly, <code>rear</code> is a linked list which represents the reverse of the rear of the queue, of length <code>len_rear</code>. Furthermore, it is assured that <code>|front| ≤ 2|rear|+1</code> and <code>|rear| ≤ 2|front|+1</code> - intuitively, it means that both the front and the rear contains between a third minus one and two thirds plus one of the elements. Finally, <code>tail_front</code> and <code>tail_rear</code> are tails of <code>front</code> and of <code>rear</code>, they allow scheduling the moment where some lazy operations are forced. Note that, when a double-ended queue contains <code>n</code> elements in the front list and <code>n</code> elements in the rear list, then the inequality invariant remains satisfied after <code>i</code> insertions and <code>d</code> deletions when <code>(i+d) &leq; n/2</code>. That is, at most <code>n/2</code> operations can happen between each rebalancing.
Doubly linked lists cannot be used as [[Immutable_object|immutable data structures]]. And an immutable array would be highly inefficient (an array is often simulated by [[Tree_(abstract_data_type)|a tree]]). A purely functional implementation of the deque can be based on stack, that is easily implemented with a [[Linked lists#Doubly linked vs. singly linked|singly linked list]] as an immutable and persistent structure.
 
{{Blockquote
Let us first give an implementation of the various operations that affect the front of the deque - cons, head and tail. Those implementations do not necessarily respect the invariant. In a second time we'll explain how to modify a deque which does not satisfy the invariant into one which satisfies it. However, they use the invariant, in that if the front is empty then the rear has at most one element. The operations affecting the rear of the list are defined similarly by symmetry.  
|text=There are several works in the literature dealing with this problem. All of these use two key ideas. The first is that a deque can be represented by a pair of stacks, one representing the front part of the deque and the other representing the reversal of the rear part. When one side becomes empty because of too many ''pop'' or ''eject'' operations, the deque, now all on one stack, is copied into two stacks each containing half of the deque elements. This fifty-fifty split guarantees that such copying, even though expensive, happens infrequently. A simple amortization argument shows that this gives a linear-time simulation of a deque by a constant number of stacks: ''k'' deque operations starting from an empty deque are simulated by O(k) stack operations.
 
<br>[…]<br>The second idea is to use incremental copying to convert this linear-time simulation into a real-time simulation: as soon as the two stack become sufficiently unbalanced, recopying to create two balanced stacks begins.
<syntaxhighlight lang="sml">
|style=font-style:italic;
empty = (0, NIL, NIL, 0, NIL, NIL)
|source=
fun insert'(x, (len_front, front, tail_front, len_rear, rear, tail_rear)) =
{{cite conference | first1=Haim |last1=Kaplan |first2=Robert E. |last2=Tarjan |author-link2=Robert Tarjan |title=Persistent lists with catenation via recursive slow-down |book-title=Proc. of the 27th annual ACM symposium on Theory of computing |pages=93–102 |date=1995 |location=Las Vegas, Nevada |url=https://dl.acm.org/doi/pdf/10.1145/225058.225090 |doi=10.1145/225058.225090 |url-access=subscription }}
  (len_front+1, CONS(x, front), drop(2, tail_front), len_rear, rear, drop(2, tail_rear))
(preliminary version of <ref name=KT1999/>)}}
fun head((_, CONS(h, _), _, _, _, _)) = h
This last process could be rather complicated as it needs to be executed concurrently with other operations and completed before the next one, to achieve an amortized real-time complexity. The next step is to support the operations in {{math|O(1)}} ''worst-case'' time. Another challenge is the real-time catenation of deques.
fun head((_, NIL, _, _, CONS(h, NIL), _)) = h
[[Chris Okasaki|Okasaki]] gives a simple solution that uses [[lazy evaluation|lazy lists]] combined with [[memoization]]. The stack to stack balancing is then partially automatic by means of a precise scheduling of incremental functions. <ref name=okasaki_PFDS>{{cite thesis |url=https://www.cs.cmu.edu/~rwh/students/okasaki.pdf |first=Chris |last=Okasaki |title=Purely Functional Data Structures |type=Ph.D. thesis |date=September 1996 |publisher=Carnegie Mellon University |id=CMU-CS-96-177}}</ref>{{rp|52−59}}{{rp|115}} However some authors deem such algorithm as not purely functional since memoization is considered as a [[Side effect (computer science)|side effect]].<ref name=KT1999/>{{rp|581}} Kaplan and Tarjan gives their own version of the purely functional deque (non-catenable), based on three ideas:<ref name=KT1999>{{cite journal |first1=Haim |last1=Kaplan |first2=Robert E. |last2=Tarjan |author-link2=Robert Tarjan |title=Purely Functional, Real-Time Deques with Catenation |journal=Journal of the ACM |volume=46 |issue=5 |year=1999 |pages=577–603 |url=https://dl.acm.org/doi/pdf/10.1145/324133.324139 |doi=10.1145/324133.324139 }}</ref>
fun tail'((len_front, CONS(head_front, front), tail_front, len_rear, rear, tail_rear)) =
*''data-structural bootstrapping'', resulting in a recursive structure that foresees the [[finger tree]]: a deque is a triple consisting of a sub-deque flanked by two size-bounded buffers. ''Enqueue'' and ''dequeue'' basically operate on the buffers (in real-time because of the bounded size), and move forward one step in the balancing process;
  (len_front - 1, front, drop(2, tail_front), len_rear, rear, drop(2, tail_rear))
*''recursive slow down'', inspired by [[Redundant binary representation]] (RBR), where an additional <code>2</code> digit stands for a suspended carry: the sub-deque contains pairs of elements from the parent deque, and the propagation of the balancing process to the sub-deque is delayed like is the carry propagation after the increment or decrement of a RBR number;<ref name=okasaki_PFDS/>{{rp|105}}
fun tail'((_, NIL, _, _, CONS(h, NIL), _)) = empty 
*and a modification of the spine of the finger tree-like structure (a stack) into a stack of stacks that can be thought of as a 2-level [[skip list]]. This allows a real-time access to the unbalanced sub-deques. In analogy to a RBR, the sub-stacks stand for contiguous blocks of <code>1</code> digits, that can be skipped to access the next <code>2</code>, i.e. a suspended carry.
</syntaxhighlight>
Kaplan and Tarjan also present an even more complex version that achieves catenation in real-time. More generally, real-time catenation requires a deque is a tuple consisting mainly in two sub-structures, that themselves contain deques or compounds of deques. The linear spine of the non-catenable deque is then replaced by a ''binary skeleton''.
 
It remains to explain how to define a method <code>balance</code> that rebalance the deque if <code>insert'</code> or <code>tail</code> broke the invariant. The method <code>insert</code> and <code>tail</code> can be defined by first applying <code>insert'</code> and <code>tail'</code> and then applying <code>balance</code>.
 
<syntaxhighlight lang="sml">
fun balance(q as (len_front, front, tail_front, len_rear, rear, tail_rear)) =
  let floor_half_len = (len_front + len_rear) / 2 in
  let ceil_half_len = len_front + len_rear - floor_half_len in
  if len_front > 2*len_rear+1 then
    let val front' = take(ceil_half_len, front)
        val rear' = rotateDrop(rear, floor_half_len, front)
    in (ceil_half_len, front', front', floor_half_len, rear', rear')
  else if len_front > 2*len_rear+1 then
    let val rear' = take(floor_half_len, rear)
        val front' = rotateDrop(front, ceil_half_len, rear)
    in (ceil_half_len, front', front', floor_half_len, rear', rear')
  else q
</syntaxhighlight>
where <code>rotateDrop(front, i, rear))</code> return the concatenation of <code>front</code> and of <code>drop(i, rear)</code>. That is<code>front' = rotateDrop(front, ceil_half_len, rear)</code> put into <code>front'</code> the content of <code>front</code> and the content of <code>rear</code> that is not already in <code>rear'</code>. Since dropping <code>n</code> elements takes <math>O(n)</math> time, we use laziness to ensure that elements are dropped two by two, with two drops being done during each <code>tail'</code> and each <code>insert'</code> operation.
 
<syntaxhighlight lang="sml">
fun rotateDrop(front, i, rear) =
  if i < 2 then rotateRev(front, drop(i, rear), NIL)
  else let CONS(x, front') = front in
    CONS(x, rotateDrop(front', j-2, drop(2, rear)))
</syntaxhighlight>
where <code>rotateRev(front, middle, rear)</code> is a function that returns the front, followed by the middle reversed, followed by the rear. This function is also defined using laziness to ensure that it can be computed step by step, with one step executed during each <code>insert'</code> and <code>tail'</code> and taking a constant time. This function uses the invariant that <code>|rear|-2|front|</code> is 2 or 3.  


<syntaxhighlight lang="sml">
Kaplan, Okasaki, and Tarjan produced a simpler, amortized version that can be implemented either using lazy evaluation or more efficiently using mutation in a broader but still restricted fashion.<ref>{{cite journal |first1=Haim |last1=Kaplan |first2=Chris |last2=Okasaki |first3=Robert E. |last3=Tarjan
fun rotateRev(NIL, rear, a) =
|date=2000 |url=https://epubs.siam.org/doi/10.1137/S0097539798339430 |title=Simple Confluently Persistent Catenable Lists
  reverse(rear)++a
|journal=SIAM Journal on Computing |volume=30 |issue=3 |doi=10.1137/S0097539798339430 |url-access=subscription }}</ref>
fun rotateRev(CONS(x, front), rear, a) =
Mihaescu and Tarjan created a simpler (but still highly complex) strictly purely functional implementation of catenable deques, and also a much simpler implementation of strictly purely functional non catenable deques, both of which have optimal worst-case bounds (not officially published).<ref name=MT2003>{{citation
  CONS(x, rotateRev(front, drop(2, rear), reverse(take(2, rear))++a))
|last1=Mihaescu |first1=Radu |last2=Tarjan |first2=Robert
</syntaxhighlight>
|date=August 2003
where <code>++</code> is the function concatenating two lists.
|title=Computer Science 528 Data Structures and Graph Algorithms
 
|chapter=Notes on Catenable Deques in Pure Lisp |type=course handout
==== Implementation without laziness ====
|publisher=Princeton University |department=Computer science
Note that, without the lazy part of the implementation, this would be a non-persistent implementation of queue in {{math|''O''(1)}} [[amortized analysis|amortized time]]. In this case, the lists <code>tail_front</code> and <code>tail_rear</code> could be removed from the representation of the double-ended queue.
|url=https://www.cs.princeton.edu/courses/archive/fall03/cs528/
|chapter-url=https://www.cs.princeton.edu/courses/archive/fall03/cs528/handouts/Notes%20on%20Catenable%20Deques.doc
|chapter-format=DOC |archive-url=https://web.archive.org/web/20060917064618/https://www.cs.princeton.edu/courses/archive/fall03/cs528/handouts/Notes%20on%20Catenable%20Deques.doc |archive-date=2006-09-17 }}</ref>


== Language support ==
== Language support ==
[[Ada (programming language)|Ada]]'s containers provides the generic packages <code>Ada.Containers.Vectors</code> and <code>Ada.Containers.Doubly_Linked_Lists</code>, for the dynamic array and linked list implementations, respectively.
[[Ada (programming language)|Ada]]'s containers provides the generic packages <code>Ada.Containers.Vectors</code> and <code>Ada.Containers.Doubly_Linked_Lists</code>, for the dynamic array and linked list implementations, respectively.


C++'s [[Standard Template Library]] provides the class templates <code>std::deque</code> and <code>std::list</code>, for the multiple array and linked list implementations, respectively.
[[File:UML deque.svg|right|UML class diagram of a double-ended queue]]C++'s [[Standard Template Library]] provides the class templates <code>std::deque</code> and <code>std::list</code>, for the multiple array and linked list implementations, respectively.


As of Java 6, Java's Collections Framework provides a new {{Javadoc:SE|java/util|Deque}} interface that provides the functionality of insertion and removal at both ends. It is implemented by classes such as {{Javadoc:SE|java/util|ArrayDeque}} (also new in Java 6) and {{Javadoc:SE|java/util|LinkedList}}, providing the dynamic array and linked list implementations, respectively. However, the <code>ArrayDeque</code>, contrary to its name, does not support random access.
As of Java 6, Java's Collections Framework provides a new {{Javadoc:SE|java/util|Deque}} interface that provides the functionality of insertion and removal at both ends. It is implemented by classes such as {{Javadoc:SE|java/util|ArrayDeque}} (also new in Java 6) and {{Javadoc:SE|java/util|LinkedList}}, providing the dynamic array and linked list implementations, respectively. However, the <code>ArrayDeque</code>, contrary to its name, does not support random access.
Line 127: Line 124:
As of PHP 5.3, PHP's SPL extension contains the 'SplDoublyLinkedList' class that can be used to implement Deque datastructures. Previously to make a Deque structure the array functions array_shift/unshift/pop/push had to be used instead.
As of PHP 5.3, PHP's SPL extension contains the 'SplDoublyLinkedList' class that can be used to implement Deque datastructures. Previously to make a Deque structure the array functions array_shift/unshift/pop/push had to be used instead.


[[Glasgow Haskell Compiler|GHC]]'s [http://hackage.haskell.org/package/containers-0.6.0.1/docs/Data-Sequence.html Data.Sequence] module implements an efficient, functional deque structure in [[Haskell (programming language)|Haskell]]. The implementation uses [[2–3 tree|2–3 finger trees]] annotated with sizes. There are other (fast) possibilities to implement purely functional (thus also [[persistent data structure|persistent]]) double queues (most using heavily [[lazy evaluation]]).<ref name="functional"/><ref>Adam L. Buchsbaum and Robert E. Tarjan. Confluently persistent deques via data structural bootstrapping. Journal of Algorithms, 18(3):513–547, May 1995. (pp. 58, 101, 125)</ref> Kaplan and Tarjan were the first to implement optimal confluently persistent catenable deques.<ref>Haim Kaplan and Robert E. Tarjan. Purely functional representations of catenable sorted lists. In ACM Symposium on Theory of Computing, pages 202–211, May  1996. (pp. 4, 82, 84, 124)</ref> Their implementation was strictly purely functional in the sense that it did not use lazy evaluation. Okasaki simplified the data structure by using lazy evaluation with a bootstrapped data structure and degrading the performance bounds from worst-case to amortized.<ref>Chris Okasaki (Aug. 1997), [https://dl.acm.org/doi/10.1145/258949.258956 Catenable double-ended queues], ACM SIGPLAN Notices Volume 32 Issue 8</ref> Kaplan, Okasaki, and Tarjan produced a simpler, non-bootstrapped, amortized version that can be implemented either using lazy evaluation or more efficiently using mutation in a broader but still restricted fashion.<ref> Haim Kaplan, Chris Okasaki, and Robert E. Tarjan (2000), [https://epubs.siam.org/doi/10.1137/S0097539798339430 Simple Confluently Persistent Catenable Lists], SIAM Journal on Computing Vol. 30, Iss. 3</ref> Mihaescu and Tarjan created a simpler (but still highly complex) strictly purely functional implementation of catenable deques, and also a much simpler implementation of strictly purely functional non-catenable deques, both of which have optimal worst-case bounds.<ref>Radu Mihaescu and Robert Tarjan (Aug. 2003), [https://www.cs.princeton.edu/courses/archive/fall03/cs528/handouts/Notes%20on%20Catenable%20Deques.doc Notes on Catenable Deques in Pure Lisp], Princetown University, COS 528, Fall 03</ref>
[[Glasgow Haskell Compiler|GHC]]'s [http://hackage.haskell.org/package/containers-0.6.0.1/docs/Data-Sequence.html Data.Sequence] module implements an efficient, functional deque structure in [[Haskell (programming language)|Haskell]]. The implementation uses [[2–3 tree|2–3 finger trees]] annotated with sizes.


Rust's <code>std::collections</code> includes [https://doc.rust-lang.org/std/collections/struct.VecDeque.html VecDeque] which implements a double-ended queue using a growable ring buffer.
Rust's <code>std::collections</code> includes [https://doc.rust-lang.org/std/collections/struct.VecDeque.html VecDeque] which implements a double-ended queue using a growable ring buffer.


== Complexity ==
* In a doubly-linked list implementation and assuming no allocation/deallocation overhead, the [[Computational complexity theory|time complexity]] of all deque operations is [[Big O notation|O(1)]]. Additionally, the time complexity of insertion or deletion in the middle, given an iterator, is O(1); however, the time complexity of random access by index is O(n).
* In a growing array, the [[Amortized analysis|amortized]] time complexity of all deque operations is [[Big O notation|O(1)]]. Additionally, the time complexity of random access by index is O(1); but the time complexity of insertion or deletion in the middle is [[Big O notation|O(n)]].
== Applications ==
== Applications ==
[[File:Back button history (Ecosia browser - Google Pixel 4a) - Google Android 12 (cropped).png|thumb|A double-ended queue can be used to store the [[Web browsing history|browsing history]]: new websites are added to the end of the queue, while the oldest entries will be deleted when the history is too large. When a user asks to clear the browsing history for the past hour, the most recently added entries are removed.]]
A double-ended queue can always be substituted for a queue or a stack structure. Thus real-world applications of the deque are often extended versions of stack- or queue-based algorithms. Actually many applications only need an output- or (more rarely) input-restricted deque. Only real-world applications that are optimally based on strict deque, i.e. witch only need access to the elements at both ends and one by one, are listed here.
One example where a deque can be used is the [[Work stealing|work stealing algorithm]].<ref name="jacm">{{cite journal |last1=Blumofe |first1=Robert D. |first2=Charles E. |last2=Leiserson |author-link2=Charles E. Leiserson |title=Scheduling multithreaded computations by work stealing |journal=J ACM |volume=46 |issue=5 |year=1999 |pages=720–748 |url=http://supertech.csail.mit.edu/papers/ft_gateway.pdf |doi=10.1145/324133.324234|s2cid=5428476 }}</ref> This algorithm implements task scheduling for several processors. A separate deque with threads to be executed is maintained for each processor. To execute the next thread, the processor gets the first element from the deque (using the "remove first element" deque operation). If the current thread forks, it is put back to the front of the deque ("insert element at front") and a new thread is executed. When one of the processors finishes execution of its own threads (i.e. its deque is empty), it can "steal" a thread from another processor: it gets the last element from the deque of another processor ("remove last element") and executes it. The work stealing algorithm is used by Intel's Threading Building Blocks (TBB) library for parallel programming.
 
===Simple priority queue===
Stacks and queues can be seen as a particular kinds of [[priority queue]]s, with the priority determined by the order in which the elements are inserted. Similarly a deque can implement a priority queue with two levels of priority: high priority elements are added to the front side of the deque while low priority ones are added to the rear. Unless an element could be canceled or stolen and then ejected from the bottom, an output-restricted deque is adequate.
 
Thus it's possible to modify the standard [[Breadth-First Search|Breadth-First Search algorithm]] to find [[Shortest_path_problem#Single-source shortest paths|single source shortest path]] in a graph with 0-cost and 1-cost edges. 0-cost elements are enqueued in front of the deque (high-priority) and then are always processed before the higher cost elements (low-priority).
 
A deque is used in the [[Work stealing|work stealing algorithm]].<ref name="jacm">{{cite journal |last1=Blumofe |first1=Robert D. |first2=Charles E. |last2=Leiserson |author-link2=Charles E. Leiserson |title=Scheduling multithreaded computations by work stealing |journal=J ACM |volume=46 |issue=5 |year=1999 |pages=720–748 |url=http://supertech.csail.mit.edu/papers/ft_gateway.pdf |doi=10.1145/324133.324234|s2cid=5428476 }}</ref> This algorithm implements task scheduling for several processors. A separate deque with threads to be executed is maintained for each processor. To execute the next thread, the processor gets the first element from the deque (using the "remove first element" deque operation). If the current thread forks, it is put back to the front of the deque ("insert element at front") and a new thread is executed. When one of the processors finishes execution of its own threads (i.e. its deque is empty), it can "steal" a thread from another processor: it gets the last element from the deque of another processor ("remove last element") and executes it. The work stealing algorithm is used by Intel's Threading Building Blocks (TBB) library for parallel programming.
 
===Deque automaton===
A [[Deque automaton]] (DA) is a finite-state machine equipped with a deque auxiliary memory. It generalizes [[Pushdown automaton]] (PDA) (stack automaton) and [[Queue automaton]] (Pull up automaton,  PUA). As such it is equivalent to a [[Turing machine]], and therefore it can process the same class of [[formal languages]]. But unlike the PDA and the PUA which impose serialization, a deque automaton permits parallel or interleaved execution of some operations.<ref>{{cite journal |last1=Crespi-Reghizzi |first1=Stefano |last2=San Pietro |first2=Pierluigi |title=Deque automata, languages, and planar graph representations |journal=Theoretical Computer Science |volume=834 |year=2020 |pages=43-59 |url=https://www.sciencedirect.com/science/article/pii/S0304397520301274 |doi=10.1016/j.tcs.2020.02.029 |url-access=subscription }}</ref>


== See also ==
== See also ==

Latest revision as of 05:11, 17 November 2025

Template:Short description Template:Redirect-distinguish-text Script error: No such module "Distinguish". Template:Multiple issues

Template:Infobox data structure

In computer science, a double-ended queue (abbreviated to deque), is an abstract data type that serves as an ordered collection of entities. It generalizes both a queue and a stack : elements can be added (enqueue) to or removed (dequeue) from either end.[1] It provides similar services in computer science, transport, and operations research where various entities such as data, objects, persons, or events are stored and held to be processed later. In these contexts, the deque performs the function of a buffer. The additional flexibility in managing the order of elements or the properties of some implementations may allow the improvement and optimization of certain algorithms.

As a general data class, the deque has some possible sub-types:

  • An input-restricted deque is one where deletion can be made from either end, but insertion can be made at one end only.
  • An output-restricted deque is one where insertion can be made at either end, but deletion can be made from one end only.

Despite their limitations these sub-types have many applications and their implementations may be simpler.

Terminology

The term deque (Template:IPAc-en Template:Respell) is used as an acronym and in analogy to a deck of cards with which the structure shares some properties and the pronunciation.[1]Template:Rp[2] Dequeue is sometimes used but can be confusing as the term is also used for the operation of removing an element of the deque. A deque may also be called a head-tail linked list, though properly this refers to a specific implementation.
An input-restricted deque is often called a steque (for stack-ended queue).[1]Template:Rp[3]Template:Rp

There is no standard vocabulary associated with deques. The terms enqueue and dequeue, borrowed from the queue structure, generally denote the basic operations on a deque, on either end. But papers and actual implementations often use different names. The names of the operations vary depending on the context, the author, the implementation or the programming language:

  • in analogy to a queue: enqueue and dequeue, or push and pull;
  • in analogy to a stack: push and pop on one side, and possibly inject and eject on the other side;
  • in analogy to a list: cons and uncons on one side, snoc and unsnoc on the other side;
  • in analogy to an array: append on one side, prepend on the other side, or shift and unshift.

Unlike the associated data structures the deque is symmetrical. Its sides may be freely named according to the context:

  • in analogy to a queue: front and back;
  • in analogy to a stack: top and bottom;
  • in analogy to a list: head or last;
  • in analogy to an array: first and end, or first and last;
  • finally left and right preserves the original symmetry of the structure.

The full name of an operation may be a combination of the name of the basic operation and the name of the side: e.g. push_front and pop_back. Terms from different analogies may be associated in a single context: append and pop, push and shift, or front and tail. Finally some programming languages use even different names based on the underlying data structure.

Also generally implemented are "peek" operations, which return the value at one end without dequeuing it. It is often named after the target side: e.g. top is the value of the element at the top of the deque. In the context of the functional programming, the dequeue operation (that returns two values: the removed element and the new deque) is not used. It is replaced by a peek function (i.e. head and last) and a function that returns the deque minus the end, i.e. tail and init.

Implementations

There are at least two common ways to efficiently implement a deque, that are often pitted against one another: with a doubly linked list or with a modified dynamic array. There are many variations and actual implementations are often hybrid solutions. Additionally, several purely functional implementations of the double-ended queue exist.

Doubly linked list

While a simple list may implement a deque, a doubly-linked list is more adequate for its symmetry to achieve a fast access to both ends of the list (head and tail, hence the name head-tail linked list). The obvious solution is to manage two references; alternatively the deque can be build as a circular list.

A doubly linked list whose nodes contain three fields: an integer value, the link to the next node, and the link to the previous node.
A doubly linked list whose nodes contain three fields: the link to the previous node, an element value, and the link to the next node.

In a doubly-linked list implementation, and assuming no allocation/deallocation overhead, the time complexity of all deque operations is [[Big O notation|Template:Math]]. Additionally, insertion or deletion in the middle, given an iterator, can also be achieved in constant time; however, the time taken for random access by index is Template:Math. As well finding a specific element normally [[Linked list#Speeding up search|requires Template:Math time]]. Generally linked data structures have poor locality of reference.

Dynamic array

File:Dynamic array.svg
Several values are inserted at the end of a dynamic array using geometric expansion. Grey cells indicate space reserved for expansion. Most insertions are fast (constant time), while some are slow due to the need for reallocation (Template:Math time, labelled with turtles).

In this case as well, while a dynamic array can be used to implement a deque, a variant that can grow from both ends is more appropriate. This is sometimes called array deques. This can be achieved in various ways, e.g.:

  • by offsetting the position of the first element of the array in the reserved memory: the unused space is distributed on both side of the data;
  • with a circular array.

The amortized time complexity of all deque operations with an array deque is [[Big O notation|Template:Math]], thanks to the geometric expansion of the back-end buffer. Additionally, random access by index takes constant time ; but the average time taken for insertion or deletion in the middle are Template:Math. Thank to the fast random access, finding an element in an ordered array is time Template:Math (binary search). Each time the array is resized, the whole content is moved: the memory usage is then momentarily doubled (or more), and all direct (external) references to the content of the array are lost.

Purely functional implementation

Doubly linked lists cannot be used as immutable data structures. And an immutable array would be highly inefficient (an array is often simulated by a tree). A purely functional implementation of the deque can be based on stack, that is easily implemented with a singly linked list as an immutable and persistent structure.

<templatestyles src="Template:Blockquote/styles.css" />

There are several works in the literature dealing with this problem. All of these use two key ideas. The first is that a deque can be represented by a pair of stacks, one representing the front part of the deque and the other representing the reversal of the rear part. When one side becomes empty because of too many pop or eject operations, the deque, now all on one stack, is copied into two stacks each containing half of the deque elements. This fifty-fifty split guarantees that such copying, even though expensive, happens infrequently. A simple amortization argument shows that this gives a linear-time simulation of a deque by a constant number of stacks: k deque operations starting from an empty deque are simulated by O(k) stack operations.
[…]
The second idea is to use incremental copying to convert this linear-time simulation into a real-time simulation: as soon as the two stack become sufficiently unbalanced, recopying to create two balanced stacks begins.

Script error: No such module "Check for unknown parameters".

This last process could be rather complicated as it needs to be executed concurrently with other operations and completed before the next one, to achieve an amortized real-time complexity. The next step is to support the operations in Template:Math worst-case time. Another challenge is the real-time catenation of deques. Okasaki gives a simple solution that uses lazy lists combined with memoization. The stack to stack balancing is then partially automatic by means of a precise scheduling of incremental functions. [3]Template:RpTemplate:Rp However some authors deem such algorithm as not purely functional since memoization is considered as a side effect.[4]Template:Rp Kaplan and Tarjan gives their own version of the purely functional deque (non-catenable), based on three ideas:[4]

  • data-structural bootstrapping, resulting in a recursive structure that foresees the finger tree: a deque is a triple consisting of a sub-deque flanked by two size-bounded buffers. Enqueue and dequeue basically operate on the buffers (in real-time because of the bounded size), and move forward one step in the balancing process;
  • recursive slow down, inspired by Redundant binary representation (RBR), where an additional 2 digit stands for a suspended carry: the sub-deque contains pairs of elements from the parent deque, and the propagation of the balancing process to the sub-deque is delayed like is the carry propagation after the increment or decrement of a RBR number;[3]Template:Rp
  • and a modification of the spine of the finger tree-like structure (a stack) into a stack of stacks that can be thought of as a 2-level skip list. This allows a real-time access to the unbalanced sub-deques. In analogy to a RBR, the sub-stacks stand for contiguous blocks of 1 digits, that can be skipped to access the next 2, i.e. a suspended carry.

Kaplan and Tarjan also present an even more complex version that achieves catenation in real-time. More generally, real-time catenation requires a deque is a tuple consisting mainly in two sub-structures, that themselves contain deques or compounds of deques. The linear spine of the non-catenable deque is then replaced by a binary skeleton.

Kaplan, Okasaki, and Tarjan produced a simpler, amortized version that can be implemented either using lazy evaluation or more efficiently using mutation in a broader but still restricted fashion.[5] Mihaescu and Tarjan created a simpler (but still highly complex) strictly purely functional implementation of catenable deques, and also a much simpler implementation of strictly purely functional non catenable deques, both of which have optimal worst-case bounds (not officially published).[6]

Language support

Ada's containers provides the generic packages Ada.Containers.Vectors and Ada.Containers.Doubly_Linked_Lists, for the dynamic array and linked list implementations, respectively.

UML class diagram of a double-ended queue
UML class diagram of a double-ended queue

C++'s Standard Template Library provides the class templates std::deque and std::list, for the multiple array and linked list implementations, respectively.

As of Java 6, Java's Collections Framework provides a new Deque interface that provides the functionality of insertion and removal at both ends. It is implemented by classes such as ArrayDeque (also new in Java 6) and LinkedList, providing the dynamic array and linked list implementations, respectively. However, the ArrayDeque, contrary to its name, does not support random access.

Javascript's Array prototype & Perl's arrays have native support for both removing (shift and pop) and adding (unshift and push) elements on both ends.

Python 2.4 introduced the collections module with support for deque objects. It is implemented using a doubly linked list of fixed-length subarrays.

As of PHP 5.3, PHP's SPL extension contains the 'SplDoublyLinkedList' class that can be used to implement Deque datastructures. Previously to make a Deque structure the array functions array_shift/unshift/pop/push had to be used instead.

GHC's Data.Sequence module implements an efficient, functional deque structure in Haskell. The implementation uses 2–3 finger trees annotated with sizes.

Rust's std::collections includes VecDeque which implements a double-ended queue using a growable ring buffer.

Applications

A double-ended queue can always be substituted for a queue or a stack structure. Thus real-world applications of the deque are often extended versions of stack- or queue-based algorithms. Actually many applications only need an output- or (more rarely) input-restricted deque. Only real-world applications that are optimally based on strict deque, i.e. witch only need access to the elements at both ends and one by one, are listed here.

Simple priority queue

Stacks and queues can be seen as a particular kinds of priority queues, with the priority determined by the order in which the elements are inserted. Similarly a deque can implement a priority queue with two levels of priority: high priority elements are added to the front side of the deque while low priority ones are added to the rear. Unless an element could be canceled or stolen and then ejected from the bottom, an output-restricted deque is adequate.

Thus it's possible to modify the standard Breadth-First Search algorithm to find single source shortest path in a graph with 0-cost and 1-cost edges. 0-cost elements are enqueued in front of the deque (high-priority) and then are always processed before the higher cost elements (low-priority).

A deque is used in the work stealing algorithm.[7] This algorithm implements task scheduling for several processors. A separate deque with threads to be executed is maintained for each processor. To execute the next thread, the processor gets the first element from the deque (using the "remove first element" deque operation). If the current thread forks, it is put back to the front of the deque ("insert element at front") and a new thread is executed. When one of the processors finishes execution of its own threads (i.e. its deque is empty), it can "steal" a thread from another processor: it gets the last element from the deque of another processor ("remove last element") and executes it. The work stealing algorithm is used by Intel's Threading Building Blocks (TBB) library for parallel programming.

Deque automaton

A Deque automaton (DA) is a finite-state machine equipped with a deque auxiliary memory. It generalizes Pushdown automaton (PDA) (stack automaton) and Queue automaton (Pull up automaton, PUA). As such it is equivalent to a Turing machine, and therefore it can process the same class of formal languages. But unlike the PDA and the PUA which impose serialization, a deque automaton permits parallel or interleaved execution of some operations.[8]

See also

References

Template:Reflist

External links

Template:Data structures

  1. a b c Donald Knuth. The Art of Computer Programming, Volume 1: Fundamental Algorithms, Third Edition. Addison-Wesley, 1997. Template:ISBN. Section 2.2.1: Stacks, Queues, and Deques, pp. 238–243.
  2. Jesse Liberty; Siddhartha Rao; Bradley Jones. C++ in One Hour a Day, Sams Teach Yourself, Sixth Edition. Sams Publishing, 2009. Template:ISBN. Lesson 18: STL Dynamic Array Classes, pp. 486.
  3. a b c Template:Cite thesis
  4. a b c Script error: No such module "Citation/CS1".
  5. Script error: No such module "Citation/CS1".
  6. Script error: No such module "citation/CS1".
  7. Script error: No such module "Citation/CS1".
  8. Script error: No such module "Citation/CS1".