Chapter: Generic Positional Containers and Double Ended Queues

Generic Positional Containers

A positional container is a generic container that stores elements by position at the discretion of a client. For example, a vector stores any value at any position and uses the bracket operator to access that element. A list also stores elements by position, although without random-access: a client may place en element at the front or back of a list using a push operation or anywhere else on the list using a list iterator as a pointer to the location where insertion is to occur.

More formally, a generic container is a template class C such that

A positional container ("pContainer" for short) is a generic container that is organized by position, which means

One test for whether a generic container is a pContainer is to check for a pair of push/pop operations. All pContainers have such a pair, either PushFront()/PopFront() or PushBack()/PopBack().

Clearly, fsu::Vector and fsu::List are pContainers. We will encounter a few others, notably Deque (in this chapter) and TBinaryTree (in a later chapter). We will also encounter associative containers, generic containers that store elements by value instead of position.

Deques - traditional

The C++ Standard Template Library (STL) contains a third pContainer in addition to the vector and list containers: the deque. The term "deque" is the traditional abbreviation associated with a traditional data structure called "double ended queue". ("Deque" is pronounced "deck", as in "a deck of cards" or "the deck of a ship".) The traditional concept is that of a linear pContainer with paired push/pop operations and data retrieval at both the front and back.

A quick review of List shows that List is a traditional deque. Moreover, Vector can be made into a traditional deque by adding PushFront() and PushBack() methods.

There is also a complexity constraint usually placed on the deque: The push/pop operations should be efficient, optimally running in time O(1). This last constraint makes the Vector incarnation of deque unacceptable.

Specifying Deque < T >

Emulating the STL, we introduce a modern version of the deque as the pContainer Deque. Deque should have the following attributes:

We will also avoid the use of a copy constructor for T.

If tradition were not involved, these attributes would perhaps be better described as a "double-ended vector". Either way, it is clear that Deque is a hybrid between modern vector and the classic deque. It can be thought of as a classic deque with random access iterators, or a modern vector with efficient push/pop/access at both the front and back.

Naming is perhaps the only way in which the STL is bound by tradition. Certainly tradition is not enough to get the modernized deque into the STL. It is there because it is useful, providing some of the functionality of the list while keeping the random access iterators of the vector.

Exercise: Write namespace-level template functions with prototypes

void InsertPosition (fsu::Deque<T>& X, size_t p, const T& t);
void InsertPosition (fsu::Vector<T>& X, size_t p, const T& t);

that insert the element t at position number p in X. What are the runtimes of these functions? How are these implementations different from the one for List assigned in the Lists chapter?

Deque<> Implementation Plan

Our implementation for Deque uses another item from classical data structures tradition: the circular array. (The STL version of deque uses a different implementation.) The essential ingredients are depicted in the slide:

Here is the actual declaration of protected data from the Deque class definition:

protected:
  T*           content_;
  unsigned int contentSize_, beg_, end_;

Consistency between content_ and contentSize_ must be maintained at all times: contentSize_ is the size of memory allocated to the array content_.

Immediately after creation of a Deque object d by the Deque default constructor, the array content_ will have size equal to contentSize_ which will in turn be the default content size. Assume that the defaultContentSize is 20. Then the protected array content_ would appear as follows:

which shows the array content_ with contentSize_ == 20, beg_ (denoted in the figure by "b") == 0, and end_ (denoted in the figure by "e") == 0. Note that while the size of the protected array content_ is 20, the size of the Deque object d is 0.

Suppose that we execute a series of d.PushBack(t) operations, with t = 'A', 'B', 'C', 'D', 'E'. After the first, content_ would appear as:

Note that 'A' resides at content_[0] and that beg_ == 0, signifying the front of d, and end_ == 1, signifying "one past the end" of d. After the other four PushBack operations, the picture has changed as follows:

again with beg and end indicating the front and one-past-the-end of d. None of what we have shown so far is at all suprising. To see the beauty of the circular array idea, consider one of the operations that is inefficient for vectors, PopFront()

PopFront() results in a modified internal structure as follows:

Unlike what would be necessary for Vector, no data has been moved or copied! To effect the removal of the front element of d, we only need to change the value of the beg datum to the next higher index. (This is reminiscent of the PopBack() operation for Vector.) In fact, we do not even need to erase the value in the now unused element content_[0], because it will be overwritten when it is used again in any case. (For clarity of the illustration, however, we show it erased.) After a second PopFront() operation, the picture becomes:

The trend should now be apparent, at least until end approaches the size of content_.

Now continue with d.PushBack(t) calls, for t = 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', and five d.PopFront() calls (interspersed anywhere within the PushBack operations). The picture is now updated as follows:

Now we can illustrate the strategy implied by the word "circular": maintain the illusion that the array content_ is arranged in a circle, with content_[contentSize_] == content_[0]. That is, we think of the array index modulo contentSize_. We will follow the data during three more PushBack operations and look at the picture after each. After d.PushBack('T'):

Note that end has been incremented modulo the size of content_. That is, instead of end = 20 = contentSize_ we have set end_ = 0. This is the beginning of the circular wrap of the data of d around the back of content_ to the beginning of content_: a circle.

After d.PushBack('U'):

the data has now followed end around and back to the front of content_.

After d.PushBack('V'):

the new data is going in to the unused elements of content_ in order of increasing index. Note that d.Size() is end_ - beg_ (modulo contentSize_) = e - b (mod 20) = 2 - 7 (mod 20) = -5 (mod 20) = 15. If now three d.PopFront() calls are made, we have:

and the new size of d is 2 - 10 (mod 20) = -8 (mod 20) = 12.

We conclude this illustrated explanation with some observations:


Deque<>::Iterator Implementation Plan

We use this iterator class to illustrate a way to implement random access iterators for an existing container class that has an internal bracket operator. This method works for both Deque and Vector. The implementations in the STL are somewhat different and somewhat more efficient, but only up to a constant multiple, which does not affect the runtime or runspace complexity of the implementation.

The protected data for an Iterator object is as follows:

protected:
  const Deque<T>* dequePtr_;
  unsigned int     indexBase_;

The pointer dequePtr_ names a particular Deque<T> object to which the iterator is associated, and the datum indexBase_ is interpreted as an index of the object *dequePtr_, a Deque object. It is important not to confuse indexBase_ with an index for the array that underlies the deque. The function of indexBase_ is to mark the starting *dequePtr_ index of the iterator. This is the element returned by operator *().

The combination of the two data items facilitates calls to the deque bracket operator, using either operator syntax or operator function syntax combined with pointer dereference. For example, either of the following returns a reference to the (indexBase_ + i) element of *dequePtr_:

(*dequePtr_)[indexBase_ + i];
dequePtr_ -> operator [] (indexBase_ + i);

This is the appropriate return value for the iterator at index i. An iterator that is initialized by i = d.Begin() has i.dequePtr_ = &D and i.indexBase_ = 0, so that *i is the same as the element D[0] and i[k] is the same element as D[k].

But the iterator can be incremented, which changes the value of its indexBase_ datum. For example, after either of the following equivalent actions:

++i;++i;++i;  // increment the iterator three times
i += 3;       // add 3 to the iterator

we would have i.indexBase_ = 3 and hence *i == D[3] and i[k] == D[3 + k]. Note that this is exactly equivalent to the way ordinary pointers interact with the ordinary array bracket operator.

These ideas are used to implement all of the methods of Deque<T>::Iterator.

Defining Deque<>

This slide gives the definition of the Deque template class:

template <typename T>
class Deque
{
public:
  // scope Deque<T>:: type definitions
  typedef T                 ValueType;
  typedef DequeIterator<T> Iterator;

  // constructors - specify size and an initial value
  Deque  ();
  Deque  (size_t, const T&);
  Deque  (const Deque<T>&);     

  // destructor
  virtual ~Deque();

  // member operators
  Deque<T>& operator =  (const Deque<T>&);
  T&         operator [] (size_t);
  const T&   operator [] (size_t) const;

  // Container class protocol
  bool      Empty       () const;
  size_t    Size        () const;
  bool      PushFront   (const T&);
  bool      PopFront    ();
  bool      PushBack    (const T&);
  bool      PopBack     ();
  void      Clear       ();
  T&        Front       ();
  const T&  Front       () const;
  T&        Back        ();
  const T&  Back        () const;

  // Iterator support
  friend class DequeIterator<T>;
  Iterator     Begin       ();
  Iterator     End         ();
  Iterator     rBegin      ();
  Iterator     rEnd        ();

  // Generic display methods 
  void Display    (std::ostream& os, char ofc = '\0') const;
  void Dump       (std::ostream& os) const;

protected:
  // classic circular array implementation
  T*     content_;
  size_t contentSize_, beg_, end_;
} ;

// operator overloads (friend status not required)

template < class T >
std::ostream& operator << (std::ostream& os, const Deque<T>& a);

template < class T >
bool     operator == (const Deque<T>&, const Deque<T>&); 

template < class T >
bool     operator != (const Deque<T>&, const Deque<T>&); 

A few items in the definition of Deque shown in the slide have yet to be mentioned. Display() is intended for client use and has the same second parameter seen in previous Display() methods, an output formatting character that is placed between every pair of T objects in the ostream, defaulting to '\0', which produces no character in the stream. Also, as in previous classes, the Dump() method is not intended for client use but is useful for inspection of internal object structure, something that is handy for the class developer as well as in assessment of software. Iterator support is the same as that of List: bidirectional.

Defining DequeIterator<>

This slide gives the definition of the Deque::Iterator template class:

template <typename T>
class DequeIterator
{
friend class Deque<T>;

public:
  // terminology support
  typedef T                 ValueType;
  typedef DequeIterator<T> Iterator;

  // constructors
  DequeIterator      ();
  DequeIterator      (const Deque<T>& Q);
  DequeIterator      (const Iterator& I);

  // information/access
  T&   Retrieve        () const; // Return ptr to current Tval
  bool Valid           () const; // cursor is valid element

  // Initializers
  void  Initialize    (const Deque<T>& Q);
  void  rInitialize   (const Deque<T>& Q);

  // various operators
  bool      operator == (const Iterator& I2) const;
  bool      operator != (const Iterator& I2) const;
  T&        operator *  () const; // Return reference to current Tval
  T&        operator [] (size_t i) const; // Return reference to Tval at index i
  Iterator& operator =  (const Iterator & I);
  Iterator& operator ++ ();    // prefix
  Iterator  operator ++ (int); // postfix
  Iterator& operator -- ();    // prefix
  Iterator  operator -- (int); // postfix

  // "pointer" arithmetic

  long      operator -  (const Iterator & I2) const;

  // these are the new template pointer arithmetic operators

  template <typename N>
  Iterator& operator += (N n);

  template <typename N>
  Iterator& operator -= (N n);

  template <typename N>
  Iterator  operator +  (N n) const;

protected:
  const Deque<T>* dequePtr_;
  size_t indexBase_;
} ;

The DequeIterator<> specification is similar to that for ListIterator<>, with the addition of the random access capability. Random access iterators are characterized by

For a brief review of classical pointer arithmetic, see Appendix 5. For more details, refer to Chapter 5 of [Deitel].

Implementing Deque<>

namespace deque
{
  static const size_t defaultContentSize = 10;
}

// operator overloads

template <typename T>
std::ostream& operator << (std::ostream& os, const Deque<T>& Q)
{
  Q.Display(os);
  return os;
}

template <typename T>
bool operator == (const Deque<T>& Q1, const Deque<T>& Q2)
{
  if (Q1.Size() != Q2.Size())
    return 0;
  for (size_t i = 0; i < Q1.Size(); ++i)
    if (Q1[i] != Q2[i])
      return 0;
  return 1;
}

template <typename T>
bool operator != (const Deque<T>& Q1, const Deque<T>& Q2)
{
  return !(Q1 == Q2);
}

// public methods

template <typename T>
Deque<T>::Deque() : content_(0), contentSize_(0), beg_(0), end_(0)
// Construct a deque of zero size and default contentSize_
{
  content_ = new T [deque::defaultContentSize];
  if (content_ == 0)
    {
      std::cerr << "** Deque error: unable to allocate memory in default constructor\n";
      exit(EXIT_FAILURE);
    }
  contentSize_ = deque::defaultContentSize;
}

template <typename T>
Deque<T>::~Deque()
{
  delete[] content_;
  content_=0;
}

template <typename T>
Deque<T>::Deque(const Deque<T>& Q) 
  :  contentSize_(Q.contentSize_), beg_(Q.beg_), end_(Q.end_)
// copy constructor      
{
  content_ = new T [contentSize_];
  if (content_ == 0)
    {
      std::cerr << "** Deque error: unable to allocate memory in copy constructor\n";
      exit(EXIT_FAILURE);
    }
  for (size_t i = 0; i < contentSize_; ++i)
    content_[i] = Q.content_[i];
}

template <typename T>
Deque<T>& Deque<T>::operator = (const Deque<T>& Q) 
// assignment operator
{
  if (this != &Q)
  {
    T* newContent = new T [Q.contentSize_];
    if (newContent == 0)
      {
	std::cerr << "** Deque error: unable to allocate memory in assignment operator\n";
        return *this;  // unchanged
      }
    delete [] content_;
    content_ = newContent;
    contentSize_ = Q.contentSize_;
    beg_ = Q.beg_;
    end_ = Q.end_;
    for (size_t i = 0; i < contentSize_; ++i)
      content_[i] = Q.content_[i];
  }
  return *this;
}

template <typename T>
T& Deque<T>::operator [] (size_t i) const
// element operator
{
  if (Size() <= i)
    {
      std::cerr << "** Deque error: index out of range\n";
      exit (EXIT_FAILURE);
    }
  i += beg_;
  if (i >= contentSize_)
    i -= contentSize_;  
  return content_[i];
}

// Container class protocol implementation

template <typename T>
bool Deque<T>::Empty() const
{
  return beg_ == end_;
}

template <typename T>
size_t Deque<T>::Size() const
{
  if (beg_ <= end_)
    return end_ - beg_;
  return contentSize_ + end_ - beg_;
}

template <typename T>
bool Deque<T>::PushFront(const T& Tval)
{
  if (Size() + 1 >= contentSize_)
  // deque is full -- grow by doubling content size
  {
    size_t newContentSize,i;
    newContentSize = 2 * contentSize_;
    if (contentSize_ == 0) newContentSize = 2;
    T* newContent = new T [newContentSize];
    if (newContent == 0)
    {
      std::cerr << "** Deque error: unable to allocate memory for PushFront()\n";
      return 0; // unchanged
    }
    // copy data to new content array
    if (beg_ <= end_)
    {
      for (i = beg_; i < end_; ++i) newContent[i] = content_[i];
    }
    else
    {
      for (i = beg_; i < contentSize_; ++i) newContent[i] = content_[i];
      for (i = 0; i < end_; ++i) newContent[i + contentSize_] = content_[i];
      end_ += contentSize_;
    }
    // switch to new content
    delete [] content_;
    content_ = newContent;
    contentSize_ = newContentSize;
  }
  // now there is extra capacity
  if (beg_ == 0)
    beg_ = contentSize_;
  --beg_;
  content_[beg_] = Tval;
  return 1;
}

template <typename T>
bool Deque<T>::PushBack(const T& Tval)
{
  if (Size() + 1 >= contentSize_)
  // deque is full -- grow by doubling content size
  {
    size_t newContentSize,i;
    newContentSize = 2 * contentSize_;
    if (contentSize_ == 0) newContentSize = 2;
    T* newContent = new T [newContentSize];
    if (newContent == 0)
    {
      std::cerr << "** Deque error: unable to allocate memory for PushBack()\n";
      return 0; // unchanged
    }
    // copy data to new content array
    if (beg_ <= end_)
    {
      for (i = beg_; i < end_; ++i) newContent[i] = content_[i];
    }
    else
    {
      for (i = beg_; i < contentSize_; ++i) newContent[i] = content_[i];
      for (i = 0; i < end_; ++i) newContent[i + contentSize_] = content_[i];
      end_ += contentSize_;
    }
    // switch to new content
    delete [] content_;
    content_ = newContent;
    contentSize_ = newContentSize;
  }
  // now there is extra capacity
  content_[end_] = Tval;
  ++end_;
  if (end_ == contentSize_)
    end_ = 0;
  return 1;
}

template <typename T>
bool Deque<T>::PopFront()
{
  if (beg_ == end_)
    return 0;
  ++beg_;
  if (beg_ == contentSize_)
    beg_ = 0;
  return 1;
}

template <typename T>
bool Deque<T>::PopBack()
{
  if (beg_ == end_)
    return 0;
  if (end_ == 0)
    end_ = contentSize_;
  --end_;
  return 1;
}

template <typename T>
void Deque<T>::Clear()
{
  beg_ = 0;
  end_ = 0;
}

template <typename T>
T&  Deque<T>::Front() const
{
  if (beg_ == end_)
  {
    std::cerr << "** Deque error: Front() called on empty deque\n";
    if (contentSize_ == 0)
      exit (EXIT_FAILURE);
  }
  return content_[beg_];
}

template <typename T>
T&  Deque<T>::Back() const
{
  if (beg_ == end_)
  {
    std::cerr << "** Deque error: Back() called on empty deque\n";
    if (contentSize_ == 0)
      exit (EXIT_FAILURE);
  }
  if (end_ == 0)
    return content_[contentSize_ - 1];
  return content_[end_ - 1];
}

// Iterator support

template <typename T>
DequeIterator<T> Deque<T>::Begin () const
{
  Iterator I;
  I.dequePtr_ = this;
  I.indexBase_ = 0;
  return I;
}

template <typename T>
DequeIterator<T> Deque<T>::End() const
{
  Iterator I;
  I.dequePtr_ = this;
  I.indexBase_ = Size();
  return I;
}

template <typename T>
DequeIterator<T> Deque<T>::rBegin() const
{
  Iterator I;
  I.dequePtr_ = this;
  I.indexBase_ = Size() - 1;
  return I;
}

template <typename T>
DequeIterator<T> Deque<T>::rEnd() const
{
  Iterator I;
  I.dequePtr_ = this;
  I.indexBase_ = 0 - 1; // note: this will be 1111111111111111 (binary)
  return I;
}

template <typename T>
void Deque<T>::Display(std::ostream& os, char ofc) const
{
  size_t i;
  if (ofc == '\0')
    for (i = 0; i < Size(); ++i)
      os << operator[](i);
  else
    for (i = 0; i < Size(); ++i)
      os << operator[](i) << ofc;
}  // end Display()

template <typename T>
void Deque<T>::Dump(std::ostream& os) const
{
  for (size_t i = 0; i < contentSize_; ++i)
  {
    if (i < 10)
      os << "    content_[" << i << "] == " << content_[i];
    else if (i < 100)
      os << "   content_[" << i << "] == " << content_[i];
    else if (i < 1000)
      os << "  content_[" << i << "] == " << content_[i];
    else if (i < 10000)
      os << " content_[" << i << "] == " << content_[i];
    else 
      os << "content_[" << i << "] == " << content_[i];
    if (i == beg_) 
      os << " <- beg";
    if (i == end_)
      os << " <- end";
    os << '\n';
  }
}


Implementing DequeIterator<>

template <typename T>
DequeIterator<T>::DequeIterator () : dequePtr_(0), indexBase_(0)
{}

template <typename T>
DequeIterator<T>::DequeIterator (const Deque<T>& Q)
 : dequePtr_(&Q), indexBase_(0)
{}

template <typename T>
DequeIterator<T>::DequeIterator (const DequeIterator<T>& i)
  :  dequePtr_(i.dequePtr_), indexBase_(i.indexBase_)
{}

template <typename T>
void  DequeIterator<T>::Initialize (const Deque<T>& Q)
{
  dequePtr_ = &Q;
  indexBase_ = 0;
}

template <typename T>
void  DequeIterator<T>::rInitialize (const Deque<T>& Q)
{
  dequePtr_ = &Q;
  indexBase_ = Q.Size() - 1;
}

template <typename T>
T&  DequeIterator<T>::Retrieve () const
{
  if (!dequePtr_)
  {
    std::cerr << "** DequeIterator error: invalid call to Retrieve()\n";
    exit (EXIT_FAILURE);
  }
  return dequePtr_->operator[](indexBase_);
}

template <typename T>
bool DequeIterator<T>::Valid () const
{
  if (0 == dequePtr_)
    return 0;
  if (indexBase_ >= dequePtr_->Size())
    return 0;
  return 1;
}

template <typename T>
bool DequeIterator<T>::operator == (const Iterator& i2) const
{
  if (dequePtr_ != i2.dequePtr_)
    return 0;
  if (indexBase_ != i2.indexBase_)
    return 0;
  return 1;
}

template <typename T>
bool DequeIterator<T>::operator != (const Iterator& i2) const
{
  return !(*this == i2);
}

template <typename T>
T&  DequeIterator<T>::operator *  () const
{
  if (0 == dequePtr_)
  {  
    std::cerr << "** DequeIterator error: invalid dereference\n";
    exit (EXIT_FAILURE);
  }
  if (dequePtr_->Size() == 0)
    std::cerr << "** DequeIterator error: invalid dereference\n";
  return dequePtr_->operator[](indexBase_);
}

template <typename T>
T&  DequeIterator<T>::operator [] (size_t index) const
{
  if (!dequePtr_)
  {
    std::cerr << "** DequeIterator error: invalid dereference\n";
    exit (EXIT_FAILURE);
  }
  return dequePtr_->operator[](indexBase_ + index);
}

template <typename T>
DequeIterator<T>& DequeIterator<T>::operator = (const Iterator & i)
{
  if (this != &i)
  {
    dequePtr_ = i.dequePtr_;
    indexBase_ = i.indexBase_;
  }
  return *this;
}

template <typename T>
DequeIterator<T>& DequeIterator<T>::operator ++ ()
{
  ++indexBase_;
  return *this;
}

template <typename T>
DequeIterator<T>  DequeIterator<T>::operator ++ (int)
{
  Iterator i(*this);
  operator ++();
  return i;
}

template <typename T>
DequeIterator<T>& DequeIterator<T>::operator -- ()
{
  --indexBase_;
  return *this;
}

template <typename T>
DequeIterator<T>  DequeIterator<T>::operator -- (int)
{
  Iterator i(*this);
  operator --();
  return i;
}

template <typename T>
long DequeIterator<T>::operator -  (const Iterator & i2) const
{
  return indexBase_ -  i2.indexBase_;
}

// these are the pointer arithmetic operator implementations are templates
// the template parameter represents the numerical argument type

template <typename T>
template <typename N>
DequeIterator<T>& DequeIterator<T>::operator += (N n)
{
  indexBase_ += n;
  return *this;
}

template <typename T>
template <typename N>
DequeIterator<T>& DequeIterator<T>::operator -= (N n)
{
  indexBase_ -= n;
  return *this;
}

template <typename T>
template <typename N>
DequeIterator<T>  DequeIterator<T>::operator +  (N n) const
{
  Iterator i(*this);
  return i += n;
}


Deque Complexity Requirements

The asymptotic runtime requirements mimic those of Vector, and the reasoning behind the verification is the same as for Vector.