Doubly linked list vs deque

There are at least two common ways to efficiently implement a deque: with a modified dynamic array or with a doubly linked list.

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:

  • Storing deque contents in a circular buffer, and only resizing when the buffer becomes full. This decreases the frequency of resizings.
  • 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.
  • 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.

Purely functional implementationEdit

Double-ended queues can also be implemented as a purely functional data structure.[3]: 115 Two versions of the implementation exist. The first one, called 'real-time deque, is presented below. It allows the queue to be persistent with operations in O(1) worst-case time, but requires lazy lists with memoization. The second one, with no lazy lists nor memoization is presented at the end of the sections. Its amortized time is O(1) if the persistency is not used; but the worst-time complexity of an operation is O(n) where n is the number of elements in the double-ended queue.

Let us recall that, for a list l, |l| denotes its length, that NIL represents an empty list and CONS(h, t) represents the list whose head is h and whose tail is t. The functions drop(i, l) and take(i, l) return the list l without its first i elements, and the first i elements of l, respectively. Or, if |l| < i, they return the empty list and l respectively.

Real-time deques via lazy rebuilding and schedulingEdit

A double-ended queue is represented as a sextuple (len_front, front, tail_front, len_rear, rear, tail_rear) where front is a linked list which contains the front of the queue of length len_front. Similarly, rear is a linked list which represents the reverse of the rear of the queue, of length len_rear. Furthermore, it is assured that |front| 2|rear|+1 and |rear| 2|front|+1 - 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, tail_front and tail_rear are tails of front and of rear, they allow scheduling the moment where some lazy operations are forced. Note that, when a double-ended queue contains n elements in the front list and n elements in the rear list, then the inequality invariant remains satisfied after i insertions and d deletions when (i+d) n/2. That is, at most n/2 operations can happen between each rebalancing.

Let us first give an implementation of the various operations that affect the front of the deque - cons, head and tail. Those implementation 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 satisfy 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.

empty = (0, NIL, NIL, 0, NIL, NIL) fun insert'(x, (len_front, front, tail_front, len_rear, rear, tail_rear)) = (len_front+1, CONS(x, front), drop(2, tail_front), len_rear, rear, drop(2, tail_rear)) fun head((_, CONS(h, _), _, _, _, _)) = h fun head((_, NIL, _, _, CONS(h, NIL), _)) = h fun tail'((len_front, CONS(head_front, front), tail_front, len_rear, rear, tail_rear)) = (len_front - 1, front, drop(2, tail_front), len_rear, rear, drop(2, tail_rear)) fun tail'((_, NIL, _, _, CONS(h, NIL), _)) = empty

It remains to explain how to define a method balance that rebalance the deque if insert' or tail broke the invariant. The method insert and tail can be defined by first applying insert' and tail' and then applying balance.

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

where rotateDrop(front, i, rear)) return the concatenation of front and of drop(i, rear). That isfront' = rotateDrop(front, ceil_half_len, rear) put into front' the content of front and the content of rear that is not already in rear'. Since dropping n elements takes O(n){\displaystyle O(n)} time, we use laziness to ensure that elements are dropped two by two, with two drops being done during each tail' and each insert' operation.

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)))

where rotateRev(front, middle, rear) 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 insert' and tail' and taking a constant time. This function uses the invariant that |rear|-2|front| is 2 or 3.

fun rotateRev(NIL, rear, a)= reverse(rear++a) fun rotateRev(CONS(x, front), rear, a)= CONS(x, rotateRev(front, drop(2, rear), reverse (take(2, rear))++a))

where ++ is the function concatenating two lists.

Implementation without lazinessEdit

Note that, without the lazy part of the implementation, this would be a non-persistent implementation of queue in O(1) amortized time. In this case, the lists tail_front and tail_rear could be removed from the representation of the double-ended queue.