Stack Implementation Using Linked List.

Implementing a stack using a linked list involves utilizing the dynamic nature of linked lists to create a Last-in, First Out (LIFO) data structure. A linked list, comprising nodes where each node contains data and a reference to the next node, is ideal for stack implementation due to its efficient insertion and deletion at the beginning or to the top of a stack.

In this article, we will discuss how to perform push, pop, peek, isEmpty, and size operations of a stack using a linked list.

Stack Using LinkedList

To implement a stack using a linked list we need to create nodes that will contain a value and a reference to the next node and we also need a top pointer to point to the topmost node that will help us in further push and pop operation. Initially, our top pointer pointed to null as the stack is empty.

Stack Operations using Linked List.

Push operation: The push operation involves adding an element to the top of the stack. This process begins by creating a new node and linking it to the current top node. The newly created node becomes the new top of the stack, updating the top pointer to this newly added node. This operation has a constant time complexity of O(1), irrespective of the stack's size.

Pop operation: The pop operation removes the top element from the stack. It starts by checking if the stack is empty. If not, it removes the top node, reassigning the top pointer to the subsequent node in the linked list. This constant-time operation O(1) effectively removes the element from the stack, ensuring adherence to the LIFO principle.

Peek operation: The peek (or top) operation retrieves the top element without removing it. It simply returns the value of the top node, allowing users to view the element at the stack's top without altering the stack's structure.

IsEmpty operation: The isEmpty operation inspects whether the stack is empty by checking if the top pointer is null, promptly determining the stack's status without traversing the entire linked list.

Size operation: The size operation determines the number of elements in the stack. It traverses the entire linked list while incrementing a counter for each node encountered. This operation exhibits a linear time complexity O(n), directly proportional to the number of elements in the stack.

Below is the code implementation of the above stack operations using Linked List.
// C++ code implementation for stack using linked list
#include <iostream>
using namespace std;

// Node class for the linked list
class Node {
public:
    int data;
    Node* next;
    Node(int value) : data(value), next(nullptr) {}
};

// Stack class using linked list
class Stack {
private:
    Node* top;

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

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

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

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

    // Display the top element of the stack
    void peek() {
        if (isEmpty()) {
            cout << "Stack is empty." << endl;
            return;
        }
        cout << "Top element: " << top->data << endl;
    }
};

int main() {
    // Example usage of the stack
    Stack stack;

    // Push elements onto the stack
    stack.push(1);
    stack.push(2);
    stack.push(3);

    // Display the top element
    stack.peek();

    // Pop an element
    stack.pop();

    // Display the top element after pop
    stack.peek();

    return 0;
}
// Java code implementation for stack using Linked list
public class Main {
  // Node class for the linked list
  static class Node {
      int data;
      Node next;
      Node(int value) {
          data = value;
          next = null;
      }
  }

  // Stack class using linked list
  static class Stack {
      private Node top;

      // Push operation
      void push(int value) {
          Node newNode = new Node(value);
          newNode.next = top;
          top = newNode;
      }

      // Pop operation
      void pop() {
          if (isEmpty()) {
              System.out.println("Stack is empty. Cannot pop.");
              return;
          }
          top = top.next;
      }

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

      // Display the top element of the stack
      void peek() {
          if (isEmpty()) {
              System.out.println("Stack is empty.");
              return;
          }
          System.out.println("Top element: " + top.data);
      }
  }

  public static void main(String[] args) {
      // Example usage of the stack
      Stack stack = new Stack();

      // Push elements onto the stack
      stack.push(1);
      stack.push(2);
      stack.push(3);

      // Display the top element
      stack.peek();

      // Pop an element
      stack.pop();

      // Display the top element after pop
      stack.peek();
  }
}
# Python code implementation of stack using linked list
class Node:
def __init__(self, value):
    self.data = value
    self.next = None

class Stack:
def __init__(self):
    self.top = None

# Push operation
def push(self, value):
    new_node = Node(value)
    new_node.next = self.top
    self.top = new_node

# Pop operation
def pop(self):
    if self.is_empty():
        print("Stack is empty. Cannot pop.")
        return
    self.top = self.top.next

# Check if the stack is empty
def is_empty(self):
    return self.top is None

# Display the top element of the stack
def peek(self):
    if self.is_empty():
        print("Stack is empty.")
        return
    print("Top element:", self.top.data)

# Example usage of the stack
stack = Stack()

# Push elements onto the stack
stack.push(1)
stack.push(2)
stack.push(3)

# Display the top element
stack.peek()

# Pop an element
stack.pop()

# Display the top element after pop
stack.peek()
// C-Sharp  code implementation for stack using linked list
using System;

// Node class for the linked list
public class Node {
    public int Data;
    public Node Next;

    public Node(int value) {
        Data = value;
        Next = null;
    }
}

// Stack class using linked list
public class Stack {
    private Node top;

    // Push operation
    public void Push(int value) {
        Node newNode = new Node(value);
        newNode.Next = top;
        top = newNode;
    }

    // Pop operation
    public void Pop() {
        if (IsEmpty()) {
            Console.WriteLine("Stack is empty. Cannot pop.");
            return;
        }
        top = top.Next;
    }

    // Check if the stack is empty
    public bool IsEmpty() {
        return top == null;
    }

    // Display the top element of the stack
    public void Peek() {
        if (IsEmpty()) {
            Console.WriteLine("Stack is empty.");
            return;
        }
        Console.WriteLine("Top element: " + top.Data);
    }
}

class Program {
    static void Main() {
        // Example usage of the stack
        Stack stack = new Stack();

        // Push elements onto the stack
        stack.Push(1);
        stack.Push(2);
        stack.Push(3);

        // Display the top element
        stack.Peek();

        // Pop an element
        stack.Pop();

        // Display the top element after pop
        stack.Peek();
    }
}
Output:
Top element: 3
Top element: 2

Time Complexity: All stack operations like, push, pop, peek, and isEmpty take O(1) constant time as it doesn't rely on traversal. But for counting the number of nodes in the stack take O(n) time as we need to traverse the complete list.

Space Complexity: O(n) where n represents the number of elements in the stack. Each node in the linked list consumes space for value and a reference to the next node.

Benefits of Stack Implementation using LinkedList.

  • Dynamic Memory Allocation: Linked lists allow dynamic memory allocation, accommodating a varying number of elements without predefining a fixed size, providing flexibility.
  • Efficient Insertions and Deletions: Push and pop operations in linked lists execute in constant time, making them efficient for stack implementations.
  • Simplicity and Ease of Use: Implementing a stack using a linked list is relatively straightforward and easy to understand, aiding in code readability and maintenance.
  • No Memory Wastage: Linked lists utilize memory efficiently by allocating space only as needed for elements, preventing memory wastage.

Difference Between Procedural and Object Oriented Programming.

Software development encompasses various methodologies, among which Procedural Programming and Object-Oriented Programming (OOP) stand as fundamental paradigms. Each approach offers unique ways to structure code and solve problems.

In this article, we will discuss the difference between Procedural and Object Oriented Programming. 

Procedural Programming.

Procedural Programming is a programming paradigm that revolves around a step-by-step approach to solving problems by executing procedures or routines. In this paradigm, the emphasis is on procedures or functions that manipulate data, sequentially performing specific tasks. Languages that use procedural programming are C, Pascal, FORTRAN, BASIC, COBOL, and ALGOL.

Key Characteristics of Procedural Programming.

  • It focuses on breaking a program into smaller, reusable functions or procedures.
  • Procedural programming often utilizes global variables that can be accessed from anywhere in the program.
  • Problem-solving follows a top-down methodology, where the main task is broken into smaller sub-tasks.
  • Procedural programming code tends to be less reusable compared to Object-Oriented Programming.

Object-Oriented Programming.

Object-oriented programming (OOP) is programming that revolves around the concept of objects, which contain both data (attributes) and behaviors (methods). It organizes software design around real-world entities, allowing for these objects' creation, manipulation, and interaction to solve complex problems. Languages that use Object-oriented programming are C++, Java, C#, Python, Ruby, PHP, Swift, etc.

Key Characteristics of Object Oriented Programming.

  • Objects are instances of classes, which serve as blueprints defining the structure and behavior of objects.
  • In OOPs, encapsulation hides the internal state of an object and exposes only necessary functionalities through well-defined interfaces.
  • In OOPs, we have an inheritance property that enables new classes to inherit attributes and behaviors from existing classes, promoting code reuse and hierarchy.

Procedural Programming Vs Object Oriented Programming.

Below are the key differences between Procedural and Object-oriented programming.
Procedural Programming Object-Oriented Programming
Procedural Programming focuses on sequential execution via functions or procedures. Object-Oriented Programming (OOP) focuses on objects and their interactions.
Procedural Programming follows a top-down approach to code execution. Object-oriented follow bottom-up approach for code execution.
The program often relies on global data access. The program uses encapsulation to restrict data access.
Less emphasis on code reusability. Promotes code reusability through inheritance and abstraction.
Procedural Programming has no inherent concept of hierarchy. Object-Oriented uses inheritance to create hierarchies and relationships.
Code may become more complex as program size grows. OOP handles complexity better, suitable for larger and complex implementation details.
Example: C, COBOL, FORTRAN, BASIC Example: C++, Java, Python, C#, Ruby

Procedural programming revolves around functions sequentially manipulating data, while Object-Oriented Programming centers on objects containing both data and functions, promoting code reusability, modularity, and easier management of complexity. 

OOP's emphasis on encapsulation, inheritance, and abstraction makes it more suitable for larger and complex systems, whereas procedural programming is often used for simpler, smaller-scale tasks.

Monotonic Stack.

A monotonic stack is a stack-based data structure used in solving problems related to finding the next greater or smaller element, computing the nearest or farthest smaller or larger elements, or solving various range-based problems efficiently. It maintains a certain monotonic property, either non-increasing or non-decreasing while traversing through elements in the stack.

Note: In mathematics, "monotonic refers to the behavior or trend of a function or sequence that consistently moves in one direction without reversing or fluctuating. 

What is a Monotonic Stack?

The primary purpose of a monotonic stack is to facilitate efficient queries for elements that satisfy certain conditions within a sequence or an array. It allows constant-time access to specific elements by leveraging its monotonic property, making it ideal for scenarios where the order of elements is crucial.

A monotonic stack can either be non-increasing or non-decreasing. A non-increasing monotonic stack maintains elements in decreasing order from the stack's base to its top. Conversely, a non-decreasing monotonic stack arranges elements in increasing order within the stack.

Key features of Monotonic stack:
  • A normal stack but its elements are in monotonically decreasing/increasing order. 
  • The next element can only be pushed if it maintains the order. 
  • If pushing the next element violates the order, pop elements until it can be safely pushed.
Monotonic Stack Examples

Types of Monotonic Stack.

Monotonic stacks can be categorized into two types based on the order they maintain.
  • Non-Increasing monotonic stack.
  • Non-decreasing monotonic stack.

Non-increasing Monotonic stack.

This stack type maintains elements in decreasing order from the base to the top of the stack. To push an element onto the stack, elements greater than the incoming element are removed (popped) to sustain the decreasing order. 

Example:
Consider the input array [5, 3, 9, 6, 2, 8]

Step-by-step process:
  • Initially, the stack is empty.
  • Push 5 onto the stack: Stack contains [5].
  • Push 3 onto the stack: Stack contains [5, 3].
  • Push 9 onto the stack: Since 9 is greater than 5 and 3, we remove 5 and 3 and the stack becomes [9].
  • Push 6 onto the stack: Stack contains [9, 6].
  • Push 2 onto the stack: Stack contains [9, 6, 2].
  • Push 8 onto the stack: Since 8 is greater than 6 and 2, we remove 6 and 2 and the stack becomes [9, 8].
  • After processing all the elements, the resultant stack is [9, 8].

Code Implementation:
Below is the code implementation of the Non-Increasing Monotonic Stack:
#include <iostream>
#include <stack>
#include <vector>

using namespace std;

// Function to find a monotonic decreasing stack
vector<int> decreasingStack(const vector<int>& arr) {
    stack<int> stk;
    vector<int> result;

    for (int num : arr) {
        while (!stk.empty() && stk.top() < num) {
            stk.pop();
        }
        stk.push(num);
    }

    int n = stk.size();
    result.resize(n);
    int i = n - 1;

    // Empty stack
    while (!stk.empty()) {
        result[i--] = stk.top();
        stk.pop();
    }

    return result;
}

// Driver code
int main() {
    vector<int> arr = {12, 17, 11, 13, 14, 10};

    // Function Call
    vector<int> result = decreasingStack(arr);

    // Displaying the original array
    cout << "The Original Array: ";
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    // Displaying Monotonic Decreasing Stack
    cout << "The Monotonic Stack: ";
    for (int num : result) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}
import java.util.Stack;

public class MonotonicDecreasingStack {

  // Function to find a monotonic decreasing stack
  static int[] decreasingStack(int[] arr) {
      Stack<Integer> stack = new Stack<>();
      
      for (int num : arr) {
          while (!stack.isEmpty() && stack.peek() < num) {
              stack.pop();
          }
          stack.push(num);
      }

      int n = stack.size();
      int[] result = new int[n];
      int i = n - 1;

      // Empty stack
      while (!stack.isEmpty()) {
          result[i--] = stack.pop();
      }

      return result;
  }

  // Driver code
  public static void main(String[] args) {
      int[] arr = {12, 17, 11, 13, 14, 10};

      // Function Call
      int[] result = decreasingStack(arr);

      // Displaying the original array
      System.out.print("The Original Array: ");
      for (int num : arr) {
          System.out.print(num + " ");
      }
      System.out.println();

      // Displaying Monotonic Decreasing Stack
      System.out.print("The Monotonic Stack: ");
      for (int num : result) {
          System.out.print(num + " ");
      }
      System.out.println();
  }
}
def decreasing_stack(arr):
    stack = []
    result = []

    for num in arr:
        while stack and stack[-1] < num:
            stack.pop()
        stack.append(num)

    # Empty stack
    while stack:
        result.insert(0, stack.pop())

    return result

# Driver code
arr = [12, 17, 11, 13, 14, 10]

# Function Call
result = decreasing_stack(arr)

# Displaying the original array
print("The Original Array:", *arr)

# Displaying Monotonic Decreasing Stack
print("The Monotonic Stack:", *result)
using System;
using System.Collections.Generic;

class Program
{
    // Function to find a monotonic decreasing stack
    static int[] DecreasingStack(int[] arr)
    {
        Stack<int> stack = new Stack<int>();

        foreach (int num in arr)
        {
            while (stack.Count > 0 && stack.Peek() < num)
            {
                stack.Pop();
            }
            stack.Push(num);
        }

        int n = stack.Count;
        int[] result = new int[n];
        int i = n - 1;

        // Empty stack
        while (stack.Count > 0)
        {
            result[i--] = stack.Pop();
        }

        return result;
    }

    // Driver code
    static void Main()
    {
        int[] arr = { 12, 17, 11, 13, 14, 10 };

        // Function Call
        int[] result = DecreasingStack(arr);

        // Displaying the original array
        Console.Write("The Original Array: ");
        foreach (int num in arr)
        {
            Console.Write(num + " ");
        }
        Console.WriteLine();

        // Displaying Monotonic Decreasing Stack
        Console.Write("The Monotonic Stack: ");
        foreach (int num in result)
        {
            Console.Write(num + " ");
        }
        Console.WriteLine();
    }
}
Output:
The Original Array: 12 17 11 13 14 10 
The Monotonic Stack: 17 14 10

Time Complexity: The time complexity of the code is O(N), where N is the number of elements in the input array. This is because the code iterates through each element of the array exactly once.

Space Complexity: The space complexity is also O(N), where N is the number of elements in the input array. The space is used for the stack and the result array.


Non-Decreasing Monotonic Stack.

This stack type maintains elements in increasing order from the base to the top of the stack. When we want to push an element onto the stack, elements smaller than the incoming element are removed (popped) to maintain the increasing order.

Example:
Consider the input array [4, 7, 2, 9, 5, 1]
  • Initially, the stack is empty.
  • Push 4 onto the stack: Stack contains[4].
  • Push 7 onto the stack: Stack contains[4, 7].
  • Push 2 onto the stack: Since 2 is smaller than 7 and 4, we remove 7 and 4 and push 2. [2].
  • Push 9 onto the stack: Stack contains[2, 9].
  • Push 5 onto the stack: Since 5 is smaller than 9, we remove 9 and push 5. [2, 5].
  • Push 1 onto the stack: Since 1 is smaller than 2 and 5, we remove 2 and 5 and push 1 [1].
  • After processing all elements, the resultant stack is [1].

Code Implementation:
Below is the code implementation of the Non-Decreasing Monotonic Stack:
#include <iostream>
#include <stack>
#include <vector>

using namespace std;

// Function to build a monotonic increasing stack
vector<int> increasingStack(const vector<int>& arr) {
    stack<int> stk;
    vector<int> result;

    for (int num : arr) {
        while (!stk.empty() && stk.top() > num) {
            stk.pop();
        }
        stk.push(num);
    }

    while (!stk.empty()) {
        result.insert(result.begin(), stk.top());
        stk.pop();
    }

    return result;
}

// Driver code
int main() {
    vector<int> arr = {1, 4, 5, 3, 14, 17};

    // Function Call
    vector<int> result = increasingStack(arr);

    // Displaying the original array
    cout << "The Original Array: ";
    for (int num : arr) {
        cout << num << " ";
    }
    cout << endl;

    // Displaying Monotonic increasing stack
    cout << "The Monotonic Stack: ";
    for (int num : result) {
        cout << num << " ";
    }
    cout << endl;

    return 0;
}
import java.util.Stack;

public class MonotonicIncreasingStack {

  // Function to build a monotonic increasing stack
  static int[] increasingStack(int[] arr) {
      Stack<Integer> stack = new Stack<>();
      
      for (int num : arr) {
          while (!stack.isEmpty() && stack.peek() > num) {
              stack.pop();
          }
          stack.push(num);
      }

      int n = stack.size();
      int[] result = new int[n];
      int i = n - 1;

      // Empty Stack
      while (!stack.isEmpty()) {
          result[i--] = stack.pop();
      }

      return result;
  }

  // Driver code
  public static void main(String[] args) {
      int[] arr = {1, 4, 5, 3, 14, 17};

      // Function Call
      int[] result = increasingStack(arr);

      // Displaying the original array
      System.out.print("The Original Array: ");
      for (int num : arr) {
          System.out.print(num + " ");
      }
      System.out.println();

      // Displaying Monotonic increasing stack
      System.out.print("The Monotonic Stack: ");
      for (int num : result) {
          System.out.print(num + " ");
      }
      System.out.println();
  }
}
def increasing_stack(arr):
    stack = []
    result = []

    for num in arr:
        while stack and stack[-1] > num:
            stack.pop()
        stack.append(num)

    while stack:
        result.insert(0, stack.pop())

    return result

# Driver code
arr = [1, 4, 5, 3, 14, 17]

# Function Call
result = increasing_stack(arr)

# Displaying the original array
print("The Original Array:", *arr)

# Displaying Monotonic increasing stack
print("The Monotonic Stack:", *result)
using System;
using System.Collections.Generic;

class Program
{
    // Function to build a monotonic increasing stack
    static int[] IncreasingStack(int[] arr)
    {
        Stack<int> stack = new Stack<int>();

        foreach (int num in arr)
        {
            while (stack.Count > 0 && stack.Peek() > num)
            {
                stack.Pop();
            }
            stack.Push(num);
        }

        int n = stack.Count;
        int[] result = new int[n];
        int i = n - 1;

        // Empty Stack
        while (stack.Count > 0)
        {
            result[i--] = stack.Pop();
        }

        return result;
    }

    // Driver code
    static void Main()
    {
        int[] arr = { 1, 4, 5, 3, 14, 17 };

        // Function Call
        int[] result = IncreasingStack(arr);

        // Displaying the original array
        Console.Write("The Original Array: ");
        foreach (int num in arr)
        {
            Console.Write(num + " ");
        }
        Console.WriteLine();

        // Displaying Monotonic increasing stack
        Console.Write("The Monotonic Stack: ");
        foreach (int num in result)
        {
            Console.Write(num + " ");
        }
        Console.WriteLine();
    }
}
Output:
The Original Array: 1 4 5 3 14 17 
The Monotonic Stack: 1 3 14 17 

Time Complexity: O(n)
Space Complexity: O(n)

Advantages of Monotonic Stack.

  • It allows quick determination of immediate next or previous elements in a sequence, aiding in optimizing various algorithms.
  • It provides a straightforward approach to solving problems involving the next greater/smaller elements or histogram-related calculations.
  • It offers improved complexity in scenarios requiring element comparisons, reducing the overall computation time.

Disadvantages of Monotonic Stack.

  • It's specialized for problems requiring specific immediate next or previous element computations and might not be suitable for border algorithmic solutions.
  • It involves maintaining a stack, which can lead to additional memory overhead in scenarios with large input sizes.

DON'T MISS

Nature, Health, Fitness
© all rights reserved
made with by AlgoLesson