Abstract Data Types

History of Abstractions

All computer systems are based on layers of abstraction Programming languages are abstractions High-level languages came about by identifying the necessary constructs (abstractions): These are the basic building-blocks of all programs.

Data Structure Abstractions

From the Sedgewick book:

An abstract data type (ADT) is a data type (a set of values and a collection of operations on those values) that is accessed only through an interface. We refer to a program that uses an ADT as a client, and a program that specifies the data type as an implementation.
Why use an ADT? A linked list abstraction using an array to implement a linked list. The client has no knowledge of the underlying data structures.

Collections of Data

Once we have the fundamental operations implemented, we can create specific ADTs (concrete types) from the more general ADT.

Pushdown Stack ADT

From the book:
A pushdown stack is an ADT that comprises two basic operations: Insert (push) a new item, and remove (pop) the item that was most recently inserted.
The stack is a LIFO (last-in, first-out) paradigm.

Q: What data structure employs the FIFO (first-in, first-out) paradigm?
A: a queue

The interface to our stack looks like this:

Stack1(int capacity) // constructor
void Push(char item) // add an item to the top
char Pop()           // remove the top item
bool IsEmpty()       // check if empty

Our first implementation (array) of a Stack: (notice the capacity in the constructor)

Stack Implementation #1

class Stack1
{
  private:
    char *items;
    int size;
  public:
    Stack1(int capacity)
    {
      items = new char[capacity];
      size = 0;
    }

    ~Stack1()
    {
      delete[] items;
    }

    void Push(char item)
    {
      items[size++] = item;
    }

    char Pop()
    {
      return items[--size];
    }

    bool IsEmpty()
    {
      return (size == 0);
    }
};
Using the first Stack class:
int main()
{
  const int SIZE = 10;
  Stack1 stack(SIZE);

  char *p = "ABCDEFG";

  for (unsigned int i = 0; i < strlen(p); i++)
    stack.Push(p[i]);

  while (!stack.IsEmpty())
    cout << stack.Pop();

  cout << endl;

  return 0;
}

The output:
GFEDCBA
There are some limitations of this Stack class:

Stack Implementation #2

A second (linked-list) version of the Stack class will require a Node structure of some sort:
struct CharItem
{
  CharItem *next;
  char data;
};
The second implementation of a Stack class:
class Stack2
{
  private:
    CharItem *head;
    int size;
    int capacity;
  public:
    Stack2(int capacity)
    {
      head = 0;
      this->capacity = capacity;
      size = 0;
    }

    ~Stack2()
    {
      while (head)
      {
        CharItem *t = head->next;
        Free(head);
        head = t;
      }
    }

    void Push(char c)
    {
      if (size >= capacity)
        return;

      CharItem *item = Allocate();

      item->data = c;
      item->next = head;
      head = item;
      size++;
    }

    char Pop()
    {
      char c = head->data;
      CharItem *temp = head;
      head = head->next;
      Free(temp);
      return c;
    }

    bool IsEmpty()
    {
      return (head == 0);
    }
};
In this implementation: Here's an implementation of the generic allocation/deallocation that do nothing special. You can easily replace these with your own memory manager.
CharItem *Allocate()
{
  return new CharItem;
}

void Free(CharItem *item)
{
  delete item;
}
We still have some limitations:

Stack Implementation #3

A third version (array) using a template class: (almost identical to the first implementation)

template <typename Item>
class Stack3
{
  private:
    Item *items;
    int size;
  public:
    Stack3(int capacity)
    {
      items = new Item[capacity];
      size = 0;
    }

    ~Stack3()
    {
      delete[] items;
    }

    void Push(Item item)
    {
      items[size++] = item;
    }

    Item Pop()
    {
      return items[--size];
    }

    bool IsEmpty()
    {
      return (size == 0);
    }
};

class Stack1
{
  private:
    char *items;
    int size;
  public:
    Stack1(int capacity)
    {
      items = new char[capacity];
      size = 0;
    }

    ~Stack1()
    {
      delete[] items;
    }

    void Push(char item)
    {
      items[size++] = item;
    }

    char Pop()
    {
      return items[--size];
    }

    bool IsEmpty()
    {
      return (size == 0);
    }
};
Client code is almost identical:

int main()
{
  const int SIZE = 10;
  Stack3<char> stack(SIZE); // This is the only change

  char *p = "ABCDEFG";

  for (unsigned int i = 0; i < strlen(p); i++)
    stack.Push(p[i]);

  while (!stack.IsEmpty())
    cout << stack.Pop();

  cout << endl;

  return 0;
}

The output:
GFEDCBA
An advantage of this implementation:

int main()
{
  const int SIZE = 5;
  Stack3<int> stack(SIZE);

  for (unsigned int i = 1; i <= SIZE; i++)
  {
    cout << 1000 * i << endl;
    stack.Push(1000 * i);
  }

  cout << endl;

  while (!stack.IsEmpty())
    cout << stack.Pop() << endl;

  return 0;   
}
The output:
1000
2000
3000
4000
5000

5000
4000
3000
2000
1000

In this implementation:

Stack Implementation #4

A fourth implementation using linked-lists of generic pointers.

We use this Node structure:

struct Item
{
  Item *next;
  void *data;
};
The interface/implementation:
class Stack4
{
  private:
    Item *head;
    int size;
    int capacity;
  public:
    Stack4(int capacity)
    {
      head = 0;
      size = 0;
      this->capacity = capacity;
    }

    ~Stack4()
    {
      // walk the list and delete each item
      while (head)
      {
        Item *t = head->next;
        Free(head);
        head = t;
      }
    }

    void Push(void *data)
    {
      if (size >= capacity)         // stack is full
        return;                     //   do nothing

      Item *item = Allocate();// allocate new item

      item->data = data;            // insert new item at head
      item->next = head;
      head = item;
      size++;
    }

    void *Pop()
    {
      void *p = head->data; // get top item
      Item *temp = head;    // update head
      head = head->next;
      Free(temp);     // deallocate
      return p;
    }

    bool IsEmpty()
    {
      return (head == 0);
    }
};
Client using the fourth implementation:
int main()
{
  const int SIZE = 10;
  Stack4 stack(SIZE);

  char *p = "ABCDEFG";

  for (unsigned int i = 0; i < strlen(p); i++)
    stack.Push(&p[i]); // push address of data;

  while (!stack.IsEmpty())
  {
    char *c = (char *) stack.Pop();
    cout << *c; // dereference data;
  }

  cout << endl;

  return 0;
}

The output:
GFEDCBA
A less trivial example:
struct TStudent
{
  float GPA;
  int ID;
  int Year;
};

int main()
{
  const int SIZE = 5;
  Stack4 stack(SIZE);

  for (int i = 0; i < SIZE; i++)
  {
    TStudent *ps = new TStudent;
    ps->GPA = GetRandom(100, 400) / 100.0;
    ps->ID = GetRandom(1, 1000);
    ps->Year = GetRandom(1, 4);
    cout << "Student ID: " << ps->ID << ", Year: " << ps->Year << ", GPA: " << ps->GPA << endl;
    stack.Push(ps);
  }

  cout << endl;

  while (!stack.IsEmpty())
  {
    TStudent *ps = (TStudent *) stack.Pop();
    cout << "Student ID: " << ps->ID << ", Year: " << ps->Year << ", GPA: " << ps->GPA << endl;
  }

  return 0;
}

The output:
Student ID: 468, Year: 3, GPA: 1.41
Student ID: 170, Year: 1, GPA: 1.12
Student ID: 359, Year: 3, GPA: 1.4
Student ID: 706, Year: 2, GPA: 1.83
Student ID: 828, Year: 2, GPA: 2.04

Student ID: 828, Year: 2, GPA: 2.04
Student ID: 706, Year: 2, GPA: 1.83
Student ID: 359, Year: 3, GPA: 1.4
Student ID: 170, Year: 1, GPA: 1.12
Student ID: 468, Year: 3, GPA: 1.41
Considerations with this implementation:

A Somewhat Useful Stack Example

A stack is the perfect data structure to implement this paradigm. Suppose we have a stream of tokens: (24 x 17 = 408)

5 9 8 + 4 6 * * 7 + *

and we want to evaluate it. The algorithm is as follows:

Self-check Evaluate the expression: 5 9 8 + 4 6 * * 7 + * using a stack where you push and pop from the top.

A very simple Evaluate function: (Supports only single-digits for input)

// postfix is something like: "598+46**7+*"
int Evaluate(const char *postfix)
{
  Stack1 stack(strlen(postfix));
  while (*postfix)
  {
    char token = *postfix;

    if (token == '+')
      stack.Push(stack.Pop() + stack.Pop());
    else if (token == '*')
      stack.Push(stack.Pop() * stack.Pop());
    else if (token >= '0' && token <= '9')
      stack.Push(token - '0');

    postfix++;
  }
  return stack.Pop();
}
Client code:

int main()
{
  char *postfix = "598+46**7+*";
  cout << postfix << " = " << Evaluate(postfix) << endl;

  return 0;
}

Some examples:
598+46**7+* = 2075
34+ = 7
34+7* = 49
12*3*4*5*6* = 720

Self-check Modify the Evaluate function above to support subtraction and division as well. (Note: You'll need to pay attention to the order of operands.) Try it with

"2 * 8 / 4 + 5 * 6 - 8" which is "2 8 * 4 / 5 6 * + 8 -" in postfix.

Converting Infix to Postfix

Input: An infix expression.
Output: A postfix expression.

Examples from above:

The algorithm is as follows: Scan the input expression from left to right until there are no more symbols. Depending on what the symbol is, you need to perform these actions:
  1. Operand - send to the output
  2. Left parenthesis - push onto the stack
  3. Right parenthesis - operators are popped off the stack and sent to the output until a left parenthesis is found (and then discarded).
  4. Operator
  5. If the input stream is empty and there are still operators on the stack, pop all of them and add them to the output.

Note that the only symbols that exist on the stack are operators and left parentheses. Operands and right parentheses are never pushed onto the stack.

Self-check - Implement a function that converts an infix expression into a postfix expression. (Hint: You will want to use a stack class. Duh.) Use your implementation to convert this infix expression to postfix: (7 + 5) * (3 + 4) - (4 * (9 - 2))

Stack Implementations: Array vs. Linked-List

Queues

Similar to stacks, but more general: Implementing Queues:

Self-check Evaluate the expression: 5 9 8 + 4 6 * * 7 + * using a queue (instead of a stack like above) where you add to the back, but remove from the front.

Implementing a FIFO Queue using a circular array

We will use a circular array of SIZE elements The Queue after construction and adding 3 items: (Note that the shaded blocks indicate unused slots in the Queue)

Removing one item, adding 3, then removing 4 more:

Adding until full, removing until empty:

The implementation is left as an exercise for the student.

Self-check Using the class interface below, implement the Queue as a circular array.

class Queue
{
  public:
    Queue(int MaxItems);
    ~Queue();
    void Add(int Item); // Push
    int Remove();   // Pop
    bool IsFull() const;
    bool IsEmpty() const;
};

Priority Queue Example

Here's a C++ class for an abstract interface of a PriorityQueue:
class PriorityQueue
{
  private:
    // private data

  public:
    PriorityQueue(int capacity);
    ~PriorityQueue();
    void Add(int Item);
    int Remove();
    bool IsEmpty() const;
    bool IsFull() const;
    void Dump() const;
};
We could implement this with either a linked list or an array. Only the private data would change.
Linked list Array
struct PQNode
{
  PQNode *next;
  int data;
};


class PQList
{
  private:
    PQNode *list_;
    int capacity_;
    int count_;
  public:
    // public interface (same as the array)
};
class PQArray
{
  private:
    int *array_;
    int capacity_;
    int count_;
  public:
    // public interface (same as the list)
};

However, the complexity of the algorithms depends on how the list/array is implemented. (Sorted vs. unsorted).

Here's a sample application using the Priority Queue. (Assume that PQList keeps the list sorted.)

And the associated output:

8  7  5  4  2  1

Removing: 8
7  5  4  2  1

Removing: 7
5  4  2  1

Removing: 5
4  2  1
If we replace this line:
PQList pq(10); // Sorted linked list implementation
with this one:
PQArray pq(10); // Array implementation (assume unsorted array)
still inserting the items in the same order as before:
pq.Add(4);  pq.Add(7);  pq.Add(2);
pq.Add(5);  pq.Add(8);  pq.Add(1);
we get this:
4  7  2  5  8  1

Removing: 8

4  7  2  5  1

Removing: 7



4  1  2  5

Removing: 5

4  1  2
The result is the same, but the implementations (and complexities) are different.

Self-check Using the class interface above, implement two priority queues. One using an array and one using a linked list. You can decide whether or not to keep it sorted.

What is the complexity when adding items to the queue when implemented as an unsorted array? Sorted array?
What is the complexity when adding items to the queue when implemented as an unsorted linked-list? Sorted List?
What is the complexity when removing items from the queue when implemented as an unsorted array? Sorted array?
What is the complexity when removing items from the queue when implemented as an unsorted linked-list? Sorted List?

Realize that adding and removing requires two actions. The first is locating the item.