Deques

Steven J. Zeil

Last modified: Oct 26, 2023
Contents:

1 What is a Deque?

A queue is a sequence of data such that

A DEqueue (double-ended queue, pronounced “dee-cue”) is a sequence of data such that

Both the queue and the dequeue can grow to arbitrary size.

The C++ standard deque (pronounced “deck”) is a generalized dequeue:

2 The deque Interface

The deque interface is identical to that of vector, except for

which are guaranteed to be O(1) time.

are not available.

So a deque is a vector that can grow/shrink at both ends.

3 Implementing Deques

Key items influencing the implementation: According to the C++ standard, we

Most implementations of deque are based on a scheme that technically meets the O(1) requirements for the push and pop operations only in an amortized sense, but does guarantee that each push or pop will only do O(1) copies of the actual heap elements.

This implementation is based upon the idea of allocating “blocks” (fixed-sized arrays) of elements and then using a “map” array (an array of pointers to these blocks) to keep track of the blocks.

3.1 Blocks and BlockMaps

 

The diagram shows a typical deque of strings under this scheme. To simplify the presentation, I have placed the strings in the deque in alphabetic order: Adams, Baker, Cox, … . You can clearly see the block and map structure.

Note that the blocks can occur essentially anywhere on the heap. The order of the blocks within the deque is determined by the map array.

3.2 The deque header

 

The deque itself has several data members.

3.3 expandMap and indexAt

Here is the code for the private section of this deque implementation.

template <class T>
class deque
{
public:
  ⋮
private:
  size_type theSize;

  T** blockmap;
  size_type mapSize;

  size_type firstBlock;
  size_type firstElement;
                         


  const static size_type BlockSize = 4096;
  static size_type numElementsInBlock;

  void deque<T>::expandMap ();  // Double the map size

  struct dqPosition {
    size_type blockNum;
    size_type elementNum;
  };

  dqPosition indexAt (deque<T>::size_type n) const;
      // where do we find element # n?
};

In addition to the items already described, a few other things are worth taking note of:

Both of these functions are “utilities” intended to simplify the implementation of the “real” deque member functions. They are private because they are only supposed to be called by the deque member functions, not by application code that uses the deque.

We can now start to describe how we actually accomplish some of the typical deque operations.

3.4 begin() & front()

 

For example, begin() and front() are pretty straightforward. To find the first element in the deque, we simply look in blockmap[firstElement]. That points us to the block containing the first element. Then within that block, we look at the firstElement position.

Here is the code for front. As is often the case, this operation comes in a pair: one version for const deques and one for non-const deques. The non-const version returns a reference to the front element, allowing applications to change it:

myNonConstDeque.front() = someValue;

The const version returns a const reference, and so changes to the front element of a const deque would result in a compilation error.

template <class T>
T& 	deque<T>::front ()
{
  return blockmap[firstBlock][firstElement];
}

template <class T>
const T& 	deque<T>::front () const
{
  return blockmap[firstBlock][firstElement];
}

begin is similar in theory, but slightly more complicated because we need to use that same reasoning to construct an iterator. We haven’t discussed the structure of the deque iterators yet, and I don’t intend to do so, because it’s not as instructive as the deque itself.

3.5 indexing

 

Next, let’s consider the indexing operation, e.g., myDeque[i], which is provided by operator[ ].

We can split the problem of finding the \( i^{\mbox{th}} \) element into cases:

Use the resulting quotient to index into the map and find the desired block. Then use the remainder of that division as the index within the block.

This calculation is one that we need to do quite often in this ADT implementation, so it’s worthwhile breaking that out into a separate utility function.

template <class T>
deque<T>::dqPosition  
    deque<T>::indexAt (deque<T>::size_type n) const
{
  dqPosition pos;
  pos.blockNum = firstBlock;
  if (n < numElementsInBlock - firstElement)
    {
      pos.elementNum = n + firstElement;
    }
  else
    {
      n -= numElementsInBlock - firstElement;
      ++pos.blockNum;
      int k = n / numElementsInBlock;
      pos.blockNum += k;
      pos.elementNum = n - k*numElementsInBlock;
    }
  return pos;
}

Here you see that utility, indexAt. It carries out the calculation we have just described, and stores the result into a simple structure with two fields, blockNum and elementNum.

And, once we have that utility function, the implementation of the indexing operators becomes trivial.

template <class T>
T&  deque<T>::operator [ ] (deque<T>::size_type n)
{
  dqPosition<T> pos = indexAt(n);
  return blockmap[pos.blockNum][pos.elementNum];
}


template <class T>
const T&  deque<T>::operator [ ] (deque<T>::size_type n) const
{
  dqPosition<T> pos = indexAt(n);
  return blockmap[pos.blockNum][pos.elementNum];
}

3.6 back() and end()

The same utility makes for a simple implementation of the back() function and would also help greatly in implementing end().

template <class T>
T& 	deque<T>::back ()
{
  dqPosition pos = indexAt(theSize-1);
  return blockmap[pos.blockNum][pos.elementNum];
}


template <class T>
const T& 	deque<T>::back () const
{
  dqPosition pos = indexAt(theSize-1);
  return blockmap[pos.blockNum][pos.elementNum];
}

It’s worth pointing out that, while there is a fair amount of calculation involved in getting to the \( i^{\mbox{th}} \) element in a deque, it’s all O(1). This is rather typical in comparing deques to arrays and vectors. deques tend to have more overhead, but it’s in the constant-multipliers. In a big-O sense, deques are always no slower than arrays and vectors, and may be faster for some operations (e.g., inserting at the front of the deque).

In addition, there are practical benefits to having at least one sequential container that does not keep all of its data in one huge contiguous area. Some operating systems place rather stringent limits on the largest amount of contiguous space you can allocate on the heap. For example, Windows 3.1 would not allow you to allocate more than 64K in a single block of memory. That may sound like a lot, but suppose you had an array or vector of PersonnelRecords, and each PersonnelRecord occupied 1024 bytes (which is not at all difficult to achieve). Then an array or vector could hold no more than 64 elements, total. deques, on the other hand, because they break their storage into blocks, could grow to quite large size (up to the total limit of memory) as long as each individual block contained no more than 64 elements.

3.7 Inserting Data into a deque

 

That brings us to the question of just how the data gets inserted into the deque in the first place.

Looking at the structure shown here, you can see that pushing a new element onto either the front or the back would be no problem for this particular deque. We have room in both the first and the last block for additional elements.

What if we want to push something onto the end/front of the deque and the last/first block is full? Then we would need to allocate space for a new block, putting the pointer to the new block in the map just after/before the currently occupied slots, and placing the new element at the beginning/end of the newly created block.

Which leaves us with only one problem: what if the map is full, and there’s no room in the map for any more pointers?

3.7.1 Expanding the map

In that case we need to create a new, larger map, as shown here.

template <class T>
void deque<T>::expandMap ()
  // double the size of the block map, copying the
  // current set of pointers into the middle of the 
  // new map array
{
  T** newMap = new T*[2*mapSize];
  fill_n(newMap, 2*mapSize, (T*)0);
  copy (blockmap+firstBlock, blockmap+mapSize, 
        newMap+mapSize/2);
  delete [] blockmap;
  blockmap = newMap;
  firstBlock = mapSize/2;
  mapSize *= 2;
}

Note that the fill and copy operations are O(mapSize), and mapSize could be as large as theSize/numElementsInBlock, so this means that a push_back or push_front on a deque with $N$ elements is really $O(N)$.

This worst case cost does amortize to $O(1)$ if we start with an empty deque and push $N$ successive elements into it. Furthermore, and this may be important if we are dealing with elements of a large or complicated data type, expandMap only copies pointers, not actual elements. The number of elements copied by a push call remains $O(1)$.

The remainder of the details for implementing the push operations is left as an exercise for the reader.