Stacks in C++ with O(1) Time Complexity for Push and Pop Operations


Understanding the Problem

Stacks are a fundamental data structure in computer science, characterized by their Last In, First Out (LIFO) behavior. The core challenge is to implement a stack that supports push and pop operations efficiently, ideally in constant time, O(1).

Stacks are widely used in various applications such as expression evaluation, backtracking algorithms, and function call management in programming languages. A common pitfall is not handling edge cases like popping from an empty stack, which can lead to runtime errors.

Approach

To solve this problem, we need to implement a stack with the following operations:

We will start with a naive approach using a dynamic array (vector) and then discuss an optimized approach using a linked list.

Naive Approach

The naive approach uses a dynamic array (vector) to store elements. While push and pop operations are generally O(1), resizing the array can lead to O(n) operations. This approach is not optimal for all scenarios.

Optimized Approach

An optimized approach uses a singly linked list, where each node points to the next node. This ensures that push and pop operations are always O(1) since we only need to update pointers.

Algorithm

Here is a step-by-step breakdown of the optimized approach using a linked list:

  1. Create a Node structure to represent each element in the stack.
  2. Maintain a pointer to the top node of the stack.
  3. For the push operation, create a new node and update the top pointer.
  4. For the pop operation, update the top pointer to the next node and delete the old top node.
  5. For the top operation, return the value of the top node.
  6. For the isEmpty operation, check if the top pointer is null.

Code Implementation

#include <iostream>
using namespace std;

// Node structure for the linked list
struct Node {
    int data;
    Node* next;
    Node(int val) : data(val), next(nullptr) {}
};

// Stack class using linked list
class Stack {
private:
    Node* top; // Pointer to the top of the stack

public:
    Stack() : top(nullptr) {}

    // Push operation
    void push(int val) {
        Node* newNode = new Node(val);
        newNode->next = top;
        top = newNode;
    }

    // Pop operation
    void pop() {
        if (isEmpty()) {
            cout << "Stack underflow" << endl;
            return;
        }
        Node* temp = top;
        top = top->next;
        delete temp;
    }

    // Top operation
    int peek() {
        if (isEmpty()) {
            cout << "Stack is empty" << endl;
            return -1;
        }
        return top->data;
    }

    // Check if the stack is empty
    bool isEmpty() {
        return top == nullptr;
    }

    // Destructor to clean up memory
    ~Stack() {
        while (!isEmpty()) {
            pop();
        }
    }
};

int main() {
    Stack s;
    s.push(10);
    s.push(20);
    s.push(30);
    cout << "Top element is " << s.peek() << endl; // Should print 30
    s.pop();
    cout << "Top element is " << s.peek() << endl; // Should print 20
    s.pop();
    s.pop();
    s.pop(); // Should print "Stack underflow"
    return 0;
}

Complexity Analysis

The time complexity for push, pop, and peek operations in the linked list implementation is O(1) because each operation involves a constant number of steps. The space complexity is O(n), where n is the number of elements in the stack.

Edge Cases

Potential edge cases include:

Testing

To test the solution comprehensively, consider the following test cases:

Using a testing framework like Google Test can help automate and manage these test cases effectively.

Thinking and Problem-Solving Tips

When approaching such problems, consider the following tips:

Practice by solving similar problems and studying different data structures and algorithms to improve problem-solving skills.

Conclusion

In this blog post, we discussed how to implement a stack using a linked list in C++ with O(1) time complexity for push and pop operations. We covered the problem definition, approach, algorithm, code implementation, complexity analysis, edge cases, and testing. Understanding and solving such problems is crucial for developing strong problem-solving skills in computer science.

Additional Resources

For further reading and practice, consider the following resources: