Command Palette

Search for a command to run...

MTH 4300

Lecture 16

Stacks

A stack is a last-in, first-out (LIFO) container. The three core operations are:

  • push(value) — add an element to the top
  • pop() — remove the top element
  • top() — read the top element without removing it

Think of a stack of plates: you can only add or remove from the top.

Stack<T> — array-backed
template <typename T>
class Stack {
public:
    Stack() : size_(0), capacity_(8) {
        data_ = new T[capacity_];
    }

    ~Stack() { delete[] data_; }

    void push(const T& value) {
        if (size_ == capacity_) grow();
        data_[size_++] = value;
    }

    void pop() {
        if (size_ > 0) --size_;
    }

    T& top() { return data_[size_ - 1]; }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    T*  data_;
    int size_;
    int capacity_;

    void grow() {
        capacity_ *= 2;
        T* next = new T[capacity_];
        for (int i = 0; i < size_; i++) next[i] = data_[i];
        delete[] data_;
        data_ = next;
    }
};

The top of the stack is data_[size_ - 1]. push appends at the end; pop decrements size_ — no data is moved. grow doubles capacity when the array is full, copying existing elements.

Calling top() or pop() on an empty stack accesses data_[-1] — undefined behavior. Always check empty() before calling them.

Queues

A queue is a first-in, first-out (FIFO) container. Elements enter at the back and leave from the front:

  • enqueue(value) — add an element to the back
  • dequeue() — remove the front element
  • front() — read the front element without removing it

Think of a line at a ticket counter: the first person in is the first person served.

Queue<T> — linked-list-backed
template <typename T>
class Queue {
public:
    Queue() : head_(nullptr), tail_(nullptr), size_(0) {}

    ~Queue() {
        while (head_) {
            Node* next = head_->next;
            delete head_;
            head_ = next;
        }
    }

    void enqueue(const T& value) {
        Node* node = new Node(value);
        if (tail_) tail_->next = node;
        else        head_ = node;
        tail_ = node;
        ++size_;
    }

    void dequeue() {
        if (!head_) return;
        Node* old = head_;
        head_ = head_->next;
        if (!head_) tail_ = nullptr; // queue is now empty
        delete old;
        --size_;
    }

    T& front() { return head_->value; }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    struct Node {
        T     value;
        Node* next;
        Node(const T& v) : value(v), next(nullptr) {}
    };

    Node* head_;
    Node* tail_;
    int   size_;
};

enqueue appends a new node at tail_. dequeue removes the node at head_. The tail_ pointer avoids walking the whole list to find the back — both operations are O(1).

When the last element is dequeued, both head_ and tail_ must be set to nullptr. Forgetting to reset tail_ leaves a dangling pointer that corrupts the next enqueue.

Stack vs. Queue

StackQueue
OrderLIFO — last in, first outFIFO — first in, first out
Addpush at topenqueue at back
Removepop from topdequeue from front
Typical useundo history, call stacktask scheduling, BFS

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    Stack<int> s;

    s.push(10);
    s.push(20);
    s.push(30);

    std::cout << s.top() << "\n"; // line A
    s.pop();
    std::cout << s.top() << "\n"; // line B
    s.push(99);
    std::cout << s.top() << "\n"; // line C
    std::cout << s.size() << "\n"; // line D

    return 0;
}

Q2.

Without running the code, trace the queue's front and back after each operation and predict the output.

#include <iostream>

int main() {
    Queue<int> q;

    q.enqueue(1);
    q.enqueue(2);
    q.enqueue(3);

    std::cout << q.front() << "\n"; // line A
    q.dequeue();
    std::cout << q.front() << "\n"; // line B
    q.enqueue(4);
    std::cout << q.front() << "\n"; // line C
    std::cout << q.size()  << "\n"; // line D

    return 0;
}

Q3.

Each snippet has a bug related to stacks or queues. Identify what's wrong.

// Snippet A — calling top on empty stack
Stack<int> s;
int x = s.top();

// Snippet B — dequeue forgets to reset tail_
void dequeue() {
    if (!head_) return;
    Node* old = head_;
    head_ = head_->next;
    delete old;
    --size_;
}

// Snippet C — pop implemented as decrement without bounds check
void pop() { --size_; }

// Snippet D — enqueue sets head_ unconditionally
void enqueue(const T& value) {
    Node* node = new Node(value);
    head_ = node; // always resets head
    if (tail_) tail_->next = node;
    tail_ = node;
    ++size_;
}

Q4.

Implement push, pop, and top for the array-backed stack below.

#include <iostream>

template <typename T>
class Stack {
public:
    Stack() : size_(0), capacity_(4) {
        data_ = new T[capacity_];
    }
    ~Stack() { delete[] data_; }

    void push(const T& value) {
        // TODO: grow if necessary, then append
    }

    void pop() {
        // TODO: decrement size if non-empty
    }

    T& top() {
        // TODO: return reference to top element
    }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    T*  data_;
    int size_;
    int capacity_;

    void grow() {
        capacity_ *= 2;
        T* next = new T[capacity_];
        for (int i = 0; i < size_; i++) next[i] = data_[i];
        delete[] data_;
        data_ = next;
    }
};

int main() {
    Stack<int> s;
    for (int i = 1; i <= 5; i++) s.push(i * 10);

    while (!s.empty()) {
        std::cout << s.top() << "\n";
        s.pop();
    }
    return 0;
}

Q5.

Implement enqueue, dequeue, and front for the linked-list queue below.

#include <iostream>

template <typename T>
class Queue {
public:
    Queue() : head_(nullptr), tail_(nullptr), size_(0) {}

    ~Queue() {
        while (head_) {
            Node* next = head_->next;
            delete head_;
            head_ = next;
        }
    }

    void enqueue(const T& value) {
        // TODO: append a new node at tail_
    }

    void dequeue() {
        // TODO: remove head_; reset tail_ if queue becomes empty
    }

    T& front() {
        // TODO: return reference to head_->value
    }

    bool empty() const { return size_ == 0; }
    int  size()  const { return size_; }

private:
    struct Node {
        T     value;
        Node* next;
        Node(const T& v) : value(v), next(nullptr) {}
    };

    Node* head_;
    Node* tail_;
    int   size_;
};

int main() {
    Queue<std::string> q;
    q.enqueue("first");
    q.enqueue("second");
    q.enqueue("third");

    while (!q.empty()) {
        std::cout << q.front() << "\n";
        q.dequeue();
    }
    return 0;
}

Practice Problems