Data Structures - Stacks, Queues

- A good majority of computer programs involve performing operations on large amounts of data

- Recall that data structures are used to help manage and process large collections of data objects

- Different types of data structures organize, order, access, and process objects in different ways

- The types of data structures used depends on the specific requirements of the program

- Recall, for example, that linked lists were useful when the number of objects were unknown at compile time

- Two new types of data structures, stacks and queues, organize objects in special types of ordered lists

- Unlike linked lists, stacks and queues enforce special restrictions on how objects are added and removed
- Stacks and queues can store objects using either arrays or dynamically allocated memory

- Data structures “know” about data objects and “do” specific types of operations on the objects

- As such, data structures are ideally designed and implemented as C++ classes as in the following examples
 

 

 

Stack
- Ordered list of objects following the “LIFO” rule which is an acronym for Last In, First Out
- The last object added (in) to the list is also the first object accessed (out) from the list
- The ordering is similar to any type of structure containing a “stack” of elements

- Consider, for example, a "stack” of books, kitchen plates, or blocks

- The element at the top of a stack is the last element that was added to the stack

- The first (and only) element you can remove from the stack (without destroying it) is the one at the top

- Such an ordering scheme exists in many programming applications utilizing sets of nested tasks

- A word processor, for example, uses a stack to “undo” editing operations

- Each time a new operation is performed, it is stored on the stack

- Each time a user does an “undo”, the first operation to undo is the last one that was performed

- Functions use stacks to pass and return function arguments as they are called in a program

- The first argument retrieved from the stack is the last argument that was stored on the stack
- All additions and removals from a stack occur at one end - the "top" of the stack

- The terms, push and pop, are used to describe adding and removing elements from the stack

- We can design a C++ class that knows about the list and pushes and pops elements from this list

- Consider such a class to manage a stack of integers as in the following example

- Dynamically allocated memory is used to store the integers in the stack

- The size of the stack is passed as an argument to the class constructor

 

#include <iostream>

using namespace std;

 

#define DEFAULT_SIZE 10

 

class Stack

{

private:

  int *array;   // holds stack values

  int size;     // size of stack

  int topPos;   // top position on stack

 

public:

 

  // Construction

  Stack(int s=DEFAULT_SIZE);

  ~Stack();

 

  // Push on top of stack

  void Push(int);

 

  // Pop off top of stack

  int Pop();

 

  // Is stack empty?

  short IsEmpty() { return (topPos == -1) ? 1 : 0; }

 

  // Overloaded stream operator

  friend ostream &operator<<(ostream &,Stack &);

};

Stack::Stack(int s)

{

  size = s;

  array = size ? new int[size] : NULL;

  topPos = -1;

}

Stack::~Stack()

{

  if( array ) delete[] array;

}

 

// Overloaded output stream operator

ostream &operator<<(ostream &strm, Stack &t)

{

  int i;

  strm << "size: " << t.size;

  strm << "  top position: " << t.topPos << endl;

  strm << "  contents from top:";

  for( i = t.topPos; i >= 0; --i )

    strm << " " << t.array[i];

  strm << endl;

  return strm;

}

 

// Push integer on stack

void Stack::Push(int v)

{

  if( topPos < size-1 )

    {

      // Assign value to top position

      array[ ++topPos ] = v;

    }

  else

    {

      // Cannot push any more values

      cout << "  Warning: stack overflow!" << endl;

    }

}

 

// Pop integer from stack

int Stack::Pop()

{

  if( topPos >= 0 )

    {

      // Return top of stack, decrement current position

      return array[ topPos-- ];

    }

  else

    {

      // Cannot return any more values

      cout << "  Warning: stack underflow!" << endl;

      return 0;

    }

}

 

main()

{

  Stack is(3);

  int pv;

 

  pv = 77;

  cout << "Pushing value: " << pv << endl;

  is.Push(pv);

  cout << is;

 

  pv = 78;

  cout << "Pushing value: " << pv << endl;

  is.Push(pv);

  cout << is;

 

  pv = 79;

  cout << "Pushing value: " << pv << endl;

  is.Push(pv);

  cout << is;

 

  pv = 80;

  cout << "Pushing value: " << pv << endl;

  is.Push(pv);

  cout << is;

 

  pv = is.Pop();

  cout << "Popped off value: " << pv << endl;

 

  pv = is.Pop();

  cout << "Popped off value: " << pv << endl;

 

  pv = is.Pop();

  cout << "Popped off value: " << pv << endl;

 

  pv = is.Pop();

}

Output
Pushing value: 77
size: 3  top position: 0
  contents from top: 77
Pushing value: 78
size: 3  top position: 1
  contents from top: 78 77
Pushing value: 79
size: 3  top position: 2
  contents from top: 79 78 77
Pushing value: 80
  Warning: stack overflow!
Popped off value: 79
Popped off value: 78
Popped off value: 77
  Warning: stack underflow!

 

- Note how the integers are organized on the stack according to the last in, first out rule

- Note that the first integer “popped” off the stack is the last integer that was “pushed” on the stack

- Note the stack only needs to keep track of the index of the element at the top of the list (topPos)

- This index is used in the Push and Pop methods to add and retrieve the element at the top of the stack

- Note the error reported when the size of the stack is exceeded (overflow)

- Note the error reported when a popping elements from an empty stack (underflow)

- Note how the output stream operator is overloaded to print the state of the stack


 

 

Queue
- Ordered list of objects following the “FIFO” rule which is an acronym for First In, First Out
- The first object added (in) to the list is also the first object accessed (out) from the list
- The ordering is similar to any type of structure containing a “waiting line” of elements

- Consider, for example, people in line at a ticket booth or cars in line at a carwash

- The element waiting at the front of the line will be the first element out of the line

- Another element to such a list is added to the back or rear of the line

- Such an ordering scheme is useful in applications processing events in “fair” order

- A multi-processing operating system, for example, executes processes in a first in, first out order

- The first process to execute in a waiting line of processes is the first one in the list

- Each new requested process is added to the rear of the waiting list

- Additions and removals occur at both the front and rear of the queue

- The terms, enqueue and dequeue, are used to describe adding and removing elements from the queue

- We can design a C++ class that knows about the list and enqueues and dequeues elements from this list

- Consider such a class to manage a queue of integers as in the following example

- Dynamically allocated memory is used to store the integers in the queue

- The size of the queue is passed as an argument to the class constructor

- Consider how elements in a queue are re-arranged when an element leaves the queue

- When the front element leaves the queue, each successive element in line moves up one location

- This operation could be compute intensive when moving large objects from one memory location to another

- Instead, the class below implements this operation in what is known as a circular array

- With circular arrays, elements are never moved from one memory location to another

- Instead, indices representing the front and rear of the line change when an element leaves or enters the queue

- The front and rear indices change as they point to different elements upon operations on the queue

- The objects retain their original locations in memory

 

#include <iostream>

using namespace std;

 

#define DEFAULT_SIZE 5

 

class Queue

{

private:

  int *array;

  int Front, Rear;

  int ArraySize;

  int CurrentSize;

 

public:

  Queue(int qs = DEFAULT_SIZE);

  ~Queue();

 

  // Add element to the queue

  void Enqueue(int v); 

 

  // Remove element from the queue

  int Dequeue();

 

  // Clear the queue

  void Clear();

 

  // Is queue empty?

  short IsEmpty() { return (CurrentSize == 0) ? 1 : 0; }

 

  friend ostream &operator<<(ostream &, Queue &);

};

Queue::Queue(int qs)

{

  ArraySize = qs;

  array = ArraySize ? new int[ArraySize] : NULL;

  Clear();

}

Queue::~Queue()

{

  if( array ) delete[] array;

}

 

// Overloaded output stream operator

ostream &operator<<(ostream &strm, Queue &t)

{

  int i,j;

  strm << "total size, current size: " << t.ArraySize;

  strm << ", " << t.CurrentSize << endl;

  strm << "front, rear positions: " << t.Front << ", " << t.Rear << endl;

  strm << "  contents from front:";

  for( j = t.Front, i = 0; i < t.CurrentSize; ++i, j=(j+1)%t.ArraySize )

    strm << " (" << j << ")" << t.array[j];

  strm << endl;

  return strm;

}

void Queue::Clear()

{

  CurrentSize = 0;

  Front = 0;

  Rear = ArraySize-1;

}

 

// Add element to the list

void Queue::Enqueue(int v)

{

  if( CurrentSize >= ArraySize )

    {

      cout << "Warning: queue overflow" << endl;

      return;

    }

  if( Rear == ArraySize-1 )

    Rear = 0;

  else

    Rear++;

  array[ Rear ] = v;

  CurrentSize++;

}

 

// Remove element from the list

int Queue::Dequeue()

{

  int tmp;

  if( IsEmpty() )

    {

      cout << "Warning: queue underflow" << endl;

      return 0;

    }

  tmp = array[Front];

  if( Front == ArraySize-1 )

    Front = 0;

  else

    Front++;

  CurrentSize--;

  return tmp;

}

 

main()

{

  Queue line(3);

  int qv;

 

  qv = 77;

  cout << "Adding queue value: " << qv << endl;

  line.Enqueue(qv);

  cout << line;

 

  qv = 78;

  cout << "Adding queue value: " << qv << endl;

  line.Enqueue(qv);

  cout << line;

 

  qv = 79;

  cout << "Adding queue value: " << qv << endl;

  line.Enqueue(qv);

  cout << line;

 

  qv = 80;

  cout << "Adding queue value: " << qv << endl;

  line.Enqueue(qv);

 

  qv = line.Dequeue();

  cout << "Removed " << qv << " from the queue" << endl;

  cout << line;

 

  qv = line.Dequeue();

  cout << "Removed " << qv << " from the queue" << endl;

  cout << line;

 

  qv = line.Dequeue();

  cout << "Removed " << qv << " from the queue" << endl;

  cout << line;

 

  qv = line.Dequeue();

}

 

 

Output

Adding queue value: 77

total size, current size: 3, 1

front, rear positions: 0, 0

  contents from front: (0)77

Adding queue value: 78

total size, current size: 3, 2

front, rear positions: 0, 1

  contents from front: (0)77 (1)78

Adding queue value: 79

total size, current size: 3, 3

front, rear positions: 0, 2

  contents from front: (0)77 (1)78 (2)79

Adding queue value: 80

Warning: queue overflow

Removed 77 from the queue

total size, current size: 3, 2

front, rear positions: 1, 2

  contents from front: (1)78 (2)79

Removed 78 from the queue

total size, current size: 3, 1

front, rear positions: 2, 2

  contents from front: (2)79

Removed 79 from the queue

total size, current size: 3, 0

front, rear positions: 0, 2

  contents from front:

Warning: queue underflow

 

 

- Note how the integers are organized on the queue according to the first in, first out rule

- Note that the first integer “dequeued” off the queue is the first integer that was “enqueued” on the queue

- Note the queue needs to keep track of indices for the element at the front and rear of the list (Front, Rear)

- These indices are used in Enqueue and Dequeue to add and retrieve elements at the front and rear of the list

- Note the error reported when the size of the queue is exceeded (overflow)

- Note the error reported when a removing elements from an empty queue (underflow)

- Note how the output stream operator is overloaded to print the state of the stack