The Stack ADT

Stack is a last in first out datastructure. We can define the stack operations abstractly as an Interface as follows

public interface Stack<T> {

    int size();

    public boolean isEmpty();

    public T peek() throws StackEmptyException;

    public void push(T element);

    public T pop() throws StackEmptyException;

    public void print();

}

Now let us consider an application. We would like to reverse the elements of an array. How to use a Stack to achieve this? Let a be an array and let s be a Stack. Then, we can leverage the LIFO nature of s as follows:

	for (int i = 0; i < a.length; i++) {
	    s.push(a[i]);
	}

	for (int i = 0; i < a.length; i++) {
	    a[i] = s.pop();
	}


Edit Get your hands dirty!

In the following you are given the first version of the Stack, which is implemented using an ArrayList.

public class ArrayListStack<T> implements Stack<T> {
    protected List<T> stackData;
    protected int top = -1;

    public ArrayListStack() {
	this.stackData = new ArrayList<T>();
    }

    public int size() {

    }

    public boolean isEmpty() {
	
    }

    public T peek() throws StackEmptyException {

    }

    public void push(T element) {

    }

    public T pop() throws StackEmptyException {

    }

    public void print() {
	StringBuilder sb = new StringBuilder();
	sb.append("[");
	for (int i = top; i > -1; --i) {
	    sb.append((stackData.get(i)).toString());
	    if (i > 0) {
		sb.append(" : ");
	    }
	}
	sb.append("]");
	System.out.println("Printing the stack contents with top being leftmost");
	System.out.println(sb);
    }

    public static <T> void reverseArray(T[] a) throws StackEmptyException {

    }


We will now do the following exercise in CodeRunner.

  1. The code snippet above is provided. You implement the size and isEmpty methods.
  2. Now implement the push method.
  3. Next implement the peek method.
  4. Now implement the pop method.
  5. Perform the following operations on a Stack and then print the stack contents: push(-10),push(-20), push(-30), pop(), pop(), push(0), push(100), pop(). What do you expect the output to be?
  6. Implement the reverseArray method.
  7. In main create first an array of Integer objects, which is sorted in ascending order and now store the elements in descending order.
  8. Likewise, create an array of String objects, which are in descending order. Store the elements in ascending order.
  9. Print the contents of both arrays after the operations.


HOMEWORK

You need to develop a NodeStack that implements the Stack interface. Using the same operations in the main method ensure that you have identical behaviour for both stacks.

Consider the class Node we created before:

public class Node<T> {
    private T val;
    private Node<T> next;

    // constructor

    public Node() {

    }

    public Node(T v) {
	val = v;
	next = null;
    }

    // getters and setters

    public void setNext(Node<T> n) {
	next = n;
    }

    public Node<T> getNext() {
	return next;
    }

    public T getValue() {
	return val;
    }
}


The Queue ADT

Queue is a first in first out (FIFO) data-structure with wide applicability.

public interface Queue<T> {

    public int size();

    public boolean isEmpty();

    public T front() throws QueueEmptyException;

    public void enqueue(T element) throws QueueFullException;

    public T dqueue() throws QueueEmptyException;

    public void print();

}


Edit Get your hands dirty!

In the following you are given the first version of the Queue, which is implemented using an Array.

public class SimpleQueue<T> implements Queue<T> {
	protected int front;
	protected int rear;
	protected int count;
	protected int capacity;
	protected T[] queueData;

	public SimpleQueue(int capacity) {
		this.front = 0;
		this.rear = 0;
		this.count = 0;
		this.size = size;
		this.queueData = (T[]) new Object[capacity];

	}

	@Override
	public int size() {
		// TODO Auto-generated method stub
		return count;
	}

	@Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public T front() throws QueueEmptyException {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void enqueue(T element) throws QueueFullException {
		// TODO Auto-generated method stub
		
	}

	@Override
	public T dqueue() throws QueueEmptyException {
		// TODO Auto-generated method stub
	
	}

	@Override
	public void print() {
		StringBuilder sb = new StringBuilder();
		sb.append("[");
		for (int i = front; i < rear; ++i) {
			sb.append((queueData[i]).toString());
			if (i < rear - 1) {
				sb.append(" : ");
			}
		}
		sb.append("]");
		System.out.println("Printing the queue contents with front item being the left most");
		System.out.println(sb);
	}


We will now do the following exercise in coderunner

  1. The code snippet above is provided. You implement the size and isEmpty methods.
  2. Now implement the front method.
  3. Next implement the enqueue method.
  4. Now implement the dequeue method.
  5. Perform the following operations on a Queue of size=5 and then print the Queue contents: enqueue("a"), enqueue("b"), enqueue("c"), enqueue("d"), enqueue("e"). What do you expect the output to be when you invoke print?
  6. Now perform two dequeue operations.
  7. Now check the size.
  8. Finally try to enqueue("f"). What happens?


Limitations of the SimpleQueue implementation

  • While there is space available in the array i.e. the size of the Queue is 3. Yet, we are unable to reclaim the available space to enqueue.
  • How can this be fixed? Can we wrap around F and R when they reach capacity?

A CircularQueue

  • Here we can reclaim the available space
  • Key idea is that when F or R reach the value of capacity-1 the next value upon increment makes them point again to the start of the queue i.e. 0. This is very simple to achieve using the mod operator.


Edit Get your hands dirty!

In the following you are given the skeleton code for the CircularQueue.

public class CircularQueue<T> extends SimpleQueue<T> {

	public CircularQueue(int size) {
		super(size);
		// TODO Auto-generated constructor stub
	}

	@Override
	public void enqueue(T element) throws QueueFullException {
		// TODO Auto-generated method stub

	}

	@Override
	public T dqueue() throws QueueEmptyException {
		// TODO Auto-generated method stub
	}

	@Override
	public void print() {
		// TODO Auto-generated method stub
	}


We will now do the following exercise in coderunner. You need to override three main methods as follows and test the behaviour.

  1. First implement the enqueue method.
  2. Next implement the dequeue method.
  3. Next implement the print method.
  4. Finally in main Perform the following operations on a Queue of size=5 and then print the Queue contents: enqueue("a"), enqueue("b"), enqueue("c"), enqueue("d"), enqueue("e"). What do you expect the output to be when you invoke print?
  5. Now perform two dequeue operations.
  6. Now check the size.
  7. Finally try to enqueue("f"). What happens?


TLDR

Concepts reused from previous lectures

  • Polymorphism
  • Interfaces
  • Generics
  • Inheritance

Recap of the Stack

  • A Stack is a LIFO data-structure with wide applicability.
  • The key operations are push, which adds an item to the top of the stack; pop, which removes an item from the top of the stack.
  • You can use a stack to apply the B-Brackets, O-Orders (powers/indices or roots), D-Division, M-Multiplication, A-Addition, S-Subtraction (BODMAS) rule to solve an arithmetic expression. First thing you need to check if the expression is well-formed i.e. all the brackets are in the correct place and for every opening bracket there is a matching closing bracket.
  • Consider: \(\{(a+b)*((c+d)/(e-f))\}\), which is well formed, while \(\{(a+b)*(c+d)/(e-f))\}\}\) is not well formed. ** HOMEWORK: Add a method to the ArrayListStack class to check if a given arithmetic expression is well formed.

Recap of the Queue

  • A Queue is a FIFO data-structure, again with wide applicability.
  • The key operations are enqueue that adds the next item to the end of the queue and dqueue that removes the next item at the front of the queue.
  • A very good application of a Queue is when we want to perform scheduling. One typical scheduling is called round-robin, where the scheduler selects an entry from the front of the queue to provide service. This is, for example, how processes are scheduled by the operating system to allocate the CPU to a process for it’s timeslice.

  • Examine how we explored a simple array-based queue, called a SimpleQueue, which has limitations.
  • We then inherited from this SimpleQueue to create a CircularQueue