Stacks, Data Structures

Harsh Mishra - Jun 2 - - Dev Community

Stacks

A stack is a fundamental data structure in computer science that operates on a Last In, First Out (LIFO) principle. This means that the last element added to the stack is the first one to be removed. Stacks are analogous to a pile of plates where you can only add or remove the top plate. This simplicity and the constraint on how elements are added and removed make stacks particularly useful for certain types of problems and algorithms.

Basic Concepts of Stacks

  1. Push Operation: This operation adds an element to the top of the stack. If the stack is implemented using an array and the array is full, a stack overflow error may occur.

  2. Pop Operation: This operation removes the top element from the stack. If the stack is empty, a stack underflow error may occur.

  3. Peek (or Top) Operation: This operation returns the top element of the stack without removing it. It is useful for accessing the top element without modifying the stack.

  4. isEmpty Operation: This operation checks whether the stack is empty. It returns true if the stack has no elements, otherwise false.

  5. Size Operation: This operation returns the number of elements currently in the stack.

Stacks are widely used in various applications such as parsing expressions, backtracking algorithms, function call management in programming languages, and many others. Understanding the basic operations and concepts of stacks is essential for solving problems that involve this data structure.

Characteristics of Stacks

  1. LIFO Structure: The defining characteristic of a stack is its Last In, First Out (LIFO) nature. This means that the most recently added element is the first one to be removed. This characteristic is crucial for scenarios where the most recent items need to be processed first.

  2. Operations are Performed at One End: All operations (push, pop, peek) are performed at the top end of the stack. This makes the stack operations very efficient in terms of time complexity, typically O(1) for these operations.

  3. Limited Access: In a stack, elements are only accessible from the top. This restricted access is what differentiates a stack from other data structures like arrays or linked lists, where elements can be accessed at any position.

  4. Dynamic Nature: When implemented using linked lists, stacks can grow and shrink dynamically as elements are added or removed. This flexibility allows stacks to handle varying sizes of data efficiently.

Understanding these characteristics helps in leveraging stacks effectively for various computational problems and in recognizing situations where a stack is the appropriate data structure to use.

Implementing Stacks

Implementing stacks can be done in several ways, with the most common methods being through arrays and linked lists. Each implementation has its own advantages and limitations, making them suitable for different scenarios.

1. Array-Based Implementation

In an array-based stack, a fixed-size array is used to store the stack elements. An index (often called the 'top' index) keeps track of the position of the last element added.

  • Advantages:

    • Simple to implement.
    • Provides fast access to elements (O(1) for push and pop operations).
    • Memory is contiguous, leading to better cache performance.
  • Limitations:

    • Fixed size, which means the stack can overflow if it exceeds the array's capacity.
    • Resizing the array (to handle overflow) can be time-consuming and memory-intensive.

2. Linked List-Based Implementation

In a linked list-based stack, each element is stored in a node, with each node pointing to the next node in the stack. The top of the stack is represented by the head of the linked list.

  • Advantages:

    • Dynamic size, which means it can grow and shrink as needed without worrying about overflow (as long as memory is available).
    • No need to predefine the stack size.
  • Limitations:

    • Slightly more complex to implement compared to array-based stacks.
    • Generally has higher memory overhead due to storing pointers/references.
    • Access time might be slower due to non-contiguous memory allocation.

By understanding these different methods of implementing stacks, you can choose the one that best fits the requirements of your application, balancing between simplicity, performance, and flexibility.

Stack Implementation Using Arrays in C++

Implementing a stack using arrays in C++ is a straightforward approach that leverages the array's contiguous memory allocation, which allows for efficient access and manipulation of elements. In this method, we use a fixed-size array to hold the stack elements and manage the stack operations using simple indexing.

Creation of the Stack

To implement a stack using arrays in C++, we first define a class Stack with its basic attributes and a constructor. Below is a part of the code for the Stack class with its fundamental attributes and constructor:

#include <iostream>
using namespace std;

class Stack {
private:
    int* arr;
    int top;
    int capacity;

public:
    // Constructor to initialize stack
    Stack(int size) {
        arr = new int[size];
        capacity = size;
        top = -1;
    }

    // Destructor to free memory allocated to the array
    ~Stack() {
        delete[] arr;
    }
};
Enter fullscreen mode Exit fullscreen mode

Attributes Explanation:

  1. arr: This is a pointer to an integer array that will store the elements of the stack. The array is dynamically allocated based on the capacity provided during the stack's initialization.

  2. top: This integer variable keeps track of the index of the top element in the stack. Initially, it is set to -1, indicating that the stack is empty.

  3. capacity: This integer variable defines the maximum number of elements that the stack can hold. It is set when the stack is initialized and does not change during the stack's lifetime.

Constructor Explanation:

The constructor Stack(int size) initializes the stack with a specified capacity:

  • arr = new int[size]: Allocates memory for the stack's array based on the given size.
  • capacity = size: Sets the capacity of the stack.
  • top = -1: Initializes the top index to -1, indicating that the stack is currently empty.

This setup provides the basic framework for the stack, allowing us to build upon it with the necessary operations such as push, pop, and peek. The constructor ensures that the stack is properly initialized with the specified capacity and is ready for use.

Operations on Stacks (Array Implementation)

To effectively use a stack, we need to implement several fundamental operations. These include pushing an element onto the stack, popping an element from the stack, peeking at the top element, checking if the stack is empty, and checking if the stack is full. Each of these operations can be efficiently implemented using arrays.

Push Operation

The push operation adds an element to the top of the stack. Before adding, it checks if the stack is full to avoid overflow. If the stack is full, an error message is displayed; otherwise, the element is added, and the top index is incremented.

void push(int x) {
    if (isFull()) {
        cout << "Overflow: Stack is full.\n";
        return;
    }
    arr[++top] = x;
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

Pop Operation

The pop operation removes the top element from the stack. Before removing, it checks if the stack is empty to avoid underflow. If the stack is empty, an error message is displayed; otherwise, the element is removed, and the top index is decremented.

int pop() {
    if (isEmpty()) {
        cout << "Underflow: Stack is empty.\n";
        return -1;
    }
    return arr[top--];
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

Peek Operation

The peek operation returns the top element of the stack without removing it. It checks if the stack is empty before accessing the top element.

int peek() {
    if (!isEmpty()) {
        return arr[top];
    } else {
        cout << "Stack is empty.\n";
        return -1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

isEmpty Operation

The isEmpty operation checks whether the stack is empty by verifying if the top index is -1.

bool isEmpty() {
    return top == -1;
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

isFull Operation

The isFull operation checks whether the stack is full by comparing the top index with the maximum capacity minus one.

bool isFull() {
    return top == capacity - 1;
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

By implementing these operations, we ensure that the stack can be efficiently used for its intended purposes, such as managing data in a LIFO order. Each operation is designed to run in constant time, ensuring quick and predictable performance.

Full Code Implementation of Stacks Using Arrays

Below is the full implementation of a stack using arrays in C++. This implementation encapsulates the stack operations within a class, providing a clean and efficient way to manage stack data.

#include <iostream>
using namespace std;

class Stack {
private:
    int* arr;
    int top;
    int capacity;

public:
    // Constructor to initialize stack
    Stack(int size) {
        arr = new int[size];
        capacity = size;
        top = -1;
    }

    // Destructor to free memory allocated to the array
    ~Stack() {
        delete[] arr;
    }

    // Utility function to add an element `x` to the stack
    void push(int x) {
        if (isFull()) {
            cout << "Overflow: Stack is full.\n";
            return;
        }
        arr[++top] = x;
    }

    // Utility function to pop the top element from the stack
    int pop() {
        if (isEmpty()) {
            cout << "Underflow: Stack is empty.\n";
            return -1;
        }
        return arr[top--];
    }

    // Utility function to return the top element of the stack
    int peek() {
        if (!isEmpty()) {
            return arr[top];
        } else {
            cout << "Stack is empty.\n";
            return -1;
        }
    }

    // Utility function to check if the stack is empty
    bool isEmpty() {
        return top == -1;
    }

    // Utility function to check if the stack is full
    bool isFull() {
        return top == capacity - 1;
    }

    // Utility function to return the size of the stack
    int size() {
        return top + 1;
    }
};

int main() {
    Stack stack(3);

    stack.push(1);
    stack.push(2);
    stack.push(3);

    cout << "Top element is: " << stack.peek() << endl;

    cout << "Stack size is " << stack.size() << endl;

    stack.pop();
    stack.pop();
    stack.pop();

    if (stack.isEmpty()) {
        cout << "Stack is empty\n";
    } else {
        cout << "Stack is not empty\n";
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Stack Implementation Using Linked List in C++

Implementing a stack using a linked list in C++ allows for dynamic memory allocation, enabling the stack to grow and shrink as needed. In this method, each element of the stack is represented as a node in the linked list, with each node containing the data and a pointer to the next node.

Creation of the Stack

To implement a stack using a linked list in C++, we encapsulate the stack within a class. Below is a class-based implementation of a stack using a linked list, including its attributes and constructor.

#include <iostream>
using namespace std;

class Stack {
private:
    struct Node {
        int data;
        Node* next;
        Node(int val) : data(val), next(nullptr) {}
    };

    Node* top;

public:
    // Constructor to initialize stack
    Stack() : top(nullptr) {}

    // Destructor to free memory allocated to the linked list
    ~Stack() {
        while (!isEmpty()) {
            pop();
        }
    }
};
Enter fullscreen mode Exit fullscreen mode

Attributes Explanation:

  1. top: This is a pointer to the top node of the stack, representing the last element pushed onto the stack.

Constructor Explanation:

The constructor Stack() initializes the stack by setting the top pointer to nullptr, indicating an empty stack.

  • top = nullptr: Initializes the top pointer to nullptr, indicating that the stack is empty.

This setup provides the basic framework for the stack, allowing us to build upon it with the necessary operations such as push, pop, and peek. The constructor ensures that the stack is properly initialized and ready for use.

Operations on Stacks (Linked List Implementation)

Implementing stack operations using a linked list allows for dynamic memory allocation and efficient manipulation of elements. Below are the fundamental operations of a stack - push, pop, peek, and isEmpty - along with their corresponding implementations using a linked list.

Push Operation

The push operation adds an element to the top of the stack. This operation involves creating a new node and updating the top pointer to point to this new node.

// Utility function to add an element `x` to the stack
void push(int x) {
    Node* newNode = new Node(x);
    newNode->next = top;
    top = newNode;
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

Pop Operation

The pop operation removes the top element from the stack. This operation involves updating the top pointer to point to the next node and deleting the removed node.

// Utility function to pop the top element from the stack
int pop() {
    if (isEmpty()) {
        cout << "Underflow: Stack is empty.\n";
        return -1;
    }
    Node* temp = top;
    int poppedValue = temp->data;
    top = top->next;
    delete temp;
    return poppedValue;
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

Peek Operation

The peek operation returns the top element of the stack without removing it. This operation involves accessing the data of the top node.

// Utility function to return the top element of the stack
int peek() {
    if (isEmpty()) {
        cout << "Stack is empty.\n";
        return -1;
    }
    return top->data;
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

isEmpty Operation

The isEmpty operation checks whether the stack is empty by verifying if the top pointer is nullptr.

// Utility function to check if the stack is empty
bool isEmpty() {
    return top == nullptr;
}
Enter fullscreen mode Exit fullscreen mode

Time Complexity: O(1)

In a linked list implementation of a stack, there is typically no need for an isFull operation. This is because a linked list-based stack can theoretically grow to utilize all available memory, as long as the system has memory available for allocation.

By implementing these operations, we ensure that the stack can be efficiently used for its intended purposes, such as managing data in a Last-In-First-Out (LIFO) order. Each operation is designed to run in constant time, ensuring quick and predictable performance.

Full Code Implementation of Stacks Using Linked List

Implementing a stack using a linked list in C++ allows for dynamic memory allocation, enabling the stack to grow and shrink as needed. In this method, each element of the stack is represented as a node in the linked list, providing flexibility in managing stack operations efficiently.

#include <iostream>
using namespace std;

class Stack {
private:
    struct Node {
        int data;
        Node* next;
        Node(int val) : data(val), next(nullptr) {}
    };

    Node* top;

public:
    // Constructor to initialize stack
    Stack() : top(nullptr) {}

    // Destructor to free memory allocated to the linked list
    ~Stack() {
        while (!isEmpty()) {
            pop();
        }
    }

    // Utility function to add an element `x` to the stack
    void push(int x) {
        Node* newNode = new Node(x);
        newNode->next = top;
        top = newNode;
    }

    // Utility function to pop the top element from the stack
    int pop() {
        if (isEmpty()) {
            cout << "Underflow: Stack is empty.\n";
            return -1;
        }
        Node* temp = top;
        int poppedValue = temp->data;
        top = top->next;
        delete temp;
        return poppedValue;
    }

    // Utility function to return the top element of the stack
    int peek() {
        if (isEmpty()) {
            cout << "Stack is empty.\n";
            return -1;
        }
        return top->data;
    }

    // Utility function to check if the stack is empty
    bool isEmpty() {
        return top == nullptr;
    }
};

int main() {
    Stack stack;

    stack.push(1);
    stack.push(2);
    stack.push(3);

    cout << "Top element is: " << stack.peek() << endl;

    cout << "Popping elements from the stack:\n";
    cout << stack.pop() << " ";
    cout << stack.pop() << " ";
    cout << stack.pop() << endl;

    if (stack.isEmpty()) {
        cout << "Stack is empty\n";
    } else {
        cout << "Stack is not empty\n";
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

This implementation provides a complete and efficient stack data structure using a linked list, encapsulated within a class in C++. The main function demonstrates the usage of the stack class by performing various operations such as pushing, popping, peeking, and checking for emptiness.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .