Stack Data Structure

A stack is a linear data structure that follows the Last In, First Out (LIFO) principle. The last item added to the stack is the first one removed. It supports the following primary operations:

  1. Push: Add an element to the top of the stack.

  2. Pop: Remove the top element from the stack.

  3. Peek: View the top element without removing it.

  4. IsEmpty: Check if the stack is empty.


Nigerian Banking Story for Context:

Consider a Nigerian bank managing customer transaction reversals. When customers make complaints about failed or incorrect transactions, the bank logs each reversal request into a stack. The most recent reversal request is processed first to prioritize recent issues while ensuring fairness in handling complaints.

For example:

  1. A customer complains about an ATM withdrawal failure.

  2. Another customer complains about a wrong POS debit.

  3. The bank logs these complaints as tasks into a stack.

The bank processes the latest issue first (LIFO), ensuring efficient issue resolution for all customers.


Real-World Applications of Stacks in Banking:

  1. Transaction Reversals: Handling customer complaints in the order of urgency, where the most recent issue is prioritized.

  2. Undo/Redo in Online Banking: Allowing customers to revert or redo recent actions (e.g., undoing fund transfers).

  3. Audit Logs: Tracking recent activities and reversing them if errors are detected.


Problem Example:

Scenario:

A Nigerian bank needs to manage transaction complaints. Write a program to:

  1. Add new complaints to the stack.

  2. Remove the most recent complaint when resolved.

  3. Display the current top complaint without removing it.

Input:

  • A series of complaints: "Failed ATM withdrawal," "POS double debit," "Wrong account transfer."

Output:

  1. The stack after all complaints are added.

  2. The top complaint after resolving the most recent one.

  3. The stack after resolving another complaint.


Code Example:

class BankComplaintStack:
    def __init__(self):
        self.stack = []

    # Add a complaint to the stack
    def push(self, complaint):
        self.stack.append(complaint)
        print(f"Complaint logged: {complaint}")

    # Remove the top complaint from the stack
    def pop(self):
        if self.is_empty():
            return "No complaints to resolve."
        complaint = self.stack.pop()
        print(f"Complaint resolved: {complaint}")
        return complaint

    # View the top complaint
    def peek(self):
        if self.is_empty():
            return "No complaints in the system."
        return f"Current complaint: {self.stack[-1]}"

    # Check if the stack is empty
    def is_empty(self):
        return len(self.stack) == 0

    # Display all complaints
    def display(self):
        return f"Current stack: {self.stack}" if not self.is_empty() else "No complaints in the system."

# Initialize the complaint stack
bank_complaints = BankComplaintStack()

# Add complaints
bank_complaints.push("Failed ATM withdrawal")
bank_complaints.push("POS double debit")
bank_complaints.push("Wrong account transfer")

# Display current stack
print(bank_complaints.display())

# View the top complaint
print(bank_complaints.peek())

# Resolve the most recent complaint
bank_complaints.pop()

# Display updated stack
print(bank_complaints.display())

# Resolve another complaint
bank_complaints.pop()

# Display final stack
print(bank_complaints.display())

Output:

Complaint logged: Failed ATM withdrawal
Complaint logged: POS double debit
Complaint logged: Wrong account transfer
Current stack: ['Failed ATM withdrawal', 'POS double debit', 'Wrong account transfer']
Current complaint: Wrong account transfer
Complaint resolved: Wrong account transfer
Current stack: ['Failed ATM withdrawal', 'POS double debit']
Complaint resolved: POS double debit
Current stack: ['Failed ATM withdrawal']

Practice Problem:

Scenario:

The bank now wants to extend this system to allow branch managers to review pending complaints and undo resolved ones if needed.

Task:

  1. Implement a function to allow undoing resolved complaints by storing resolved complaints in a separate stack.

  2. Add a function to redo an undone complaint.


Solution for Practice Problem:

class UndoRedoComplaintSystem:
    def __init__(self):
        self.complaint_stack = []
        self.resolved_stack = []

    # Log a new complaint
    def log_complaint(self, complaint):
        self.complaint_stack.append(complaint)
        print(f"Complaint logged: {complaint}")

    # Resolve the latest complaint
    def resolve_complaint(self):
        if not self.complaint_stack:
            return "No complaints to resolve."
        complaint = self.complaint_stack.pop()
        self.resolved_stack.append(complaint)
        return f"Resolved complaint: {complaint}"

    # Undo the last resolved complaint
    def undo_resolution(self):
        if not self.resolved_stack:
            return "No resolved complaints to undo."
        complaint = self.resolved_stack.pop()
        self.complaint_stack.append(complaint)
        return f"Resolution undone for: {complaint}"

    # Display all pending complaints
    def display_complaints(self):
        return f"Pending complaints: {self.complaint_stack}" if self.complaint_stack else "No pending complaints."

    # Display resolved complaints
    def display_resolved(self):
        return f"Resolved complaints: {self.resolved_stack}" if self.resolved_stack else "No resolved complaints."

# Test the UndoRedo functionality
bank_system = UndoRedoComplaintSystem()

# Log complaints
bank_system.log_complaint("Failed ATM withdrawal")
bank_system.log_complaint("POS double debit")

# Resolve a complaint
print(bank_system.resolve_complaint())

# Undo the resolution
print(bank_system.undo_resolution())

# Display all complaints
print(bank_system.display_complaints())
print(bank_system.display_resolved())

Output:

Complaint logged: Failed ATM withdrawal
Complaint logged: POS double debit
Resolved complaint: POS double debit
Resolution undone for: POS double debit
Pending complaints: ['Failed ATM withdrawal', 'POS double debit']
Resolved complaints: []


1. Question: Valid Parentheses

Problem:

Given a string s containing just the characters '(', ')', '{', '}', '[', and ']', determine if the input string is valid.
A string is valid if:

  1. Open brackets are closed by the same type of brackets.

  2. Open brackets are closed in the correct order.

Example:

Input: s = "()"
Output: true

Input: s = "([)]"
Output: false

Input: s = "{[]}"
Output: true

Solution:

def is_valid_parentheses(s):
    stack = []
    mapping = {')': '(', '}': '{', ']': '['}

    for char in s:
        if char in mapping:
            top_element = stack.pop() if stack else '#'
            if mapping[char] != top_element:
                return False
        else:
            stack.append(char)

    return not stack

# Test Cases
print(is_valid_parentheses("()"))         # Output: True
print(is_valid_parentheses("([)]"))       # Output: False
print(is_valid_parentheses("{[]}"))       # Output: True

Explanation:

  • Use a stack to keep track of opening brackets.

  • When encountering a closing bracket, check if it matches the last opening bracket in the stack. If not, return False.

  • At the end, ensure the stack is empty.


2. Question: Implement a Min Stack

Problem:

Design a stack that supports the following operations in O(1) time:

  1. push(x) — Push element x onto the stack.

  2. pop() — Remove the element on top of the stack.

  3. top() — Get the top element.

  4. get_min() — Retrieve the minimum element in the stack.

Example:

Input:
push(-2), push(0), push(-3), get_min(), pop(), top(), get_min()
Output:
-3, 0, -2

Solution:

class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, x):
        self.stack.append(x)
        if not self.min_stack or x <= self.min_stack[-1]:
            self.min_stack.append(x)

    def pop(self):
        if self.stack.pop() == self.min_stack[-1]:
            self.min_stack.pop()

    def top(self):
        return self.stack[-1] if self.stack else None

    def get_min(self):
        return self.min_stack[-1] if self.min_stack else None

# Test Case
min_stack = MinStack()
min_stack.push(-2)
min_stack.push(0)
min_stack.push(-3)
print(min_stack.get_min())  # Output: -3
min_stack.pop()
print(min_stack.top())      # Output: 0
print(min_stack.get_min())  # Output: -2

Explanation:

  • Maintain two stacks:

    1. stack: Stores all elements.

    2. min_stack: Tracks the minimum value at each stage.


3. Question: Reverse a String Using a Stack

Problem:

Write a function that reverses a string using a stack.

Example:

Input: "hello"
Output: "olleh"

Solution:

def reverse_string(s):
    stack = []
    for char in s:
        stack.append(char)

    reversed_str = ""
    while stack:
        reversed_str += stack.pop()

    return reversed_str

# Test Case
print(reverse_string("hello"))  # Output: "olleh"

Explanation:

  • Push each character onto the stack.

  • Pop characters to build the reversed string (LIFO behavior ensures reversal).


4. Question: Next Greater Element

Problem:

Given an array, find the next greater element for each element in the array.
The next greater element for an element x is the first greater element on its right. If it does not exist, output -1.

Example:

Input: [4, 5, 2, 10]
Output: [5, 10, 10, -1]

Solution:

def next_greater_element(nums):
    stack = []
    result = [-1] * len(nums)

    for i in range(len(nums)):
        while stack and nums[stack[-1]] < nums[i]:
            index = stack.pop()
            result[index] = nums[i]
        stack.append(i)

    return result

# Test Case
print(next_greater_element([4, 5, 2, 10]))  # Output: [5, 10, 10, -1]

Explanation:

  • Traverse the array.

  • Use a stack to keep track of indices of elements for which the next greater element has not been found.

  • Update the result for elements whose next greater element is identified.


5. Question: Evaluate Reverse Polish Notation (RPN)

Problem:

Evaluate an arithmetic expression in Reverse Polish Notation.

Example:

Input: tokens = ["2", "1", "+", "3", "*"]
Output: 9
Explanation: ((2 + 1) * 3) = 9

Solution:

def eval_rpn(tokens):
    stack = []

    for token in tokens:
        if token not in "+-*/":
            stack.append(int(token))
        else:
            b = stack.pop()
            a = stack.pop()
            if token == '+':
                stack.append(a + b)
            elif token == '-':
                stack.append(a - b)
            elif token == '*':
                stack.append(a * b)
            elif token == '/':
                stack.append(int(a / b))  # Ensure integer division

    return stack[0]

# Test Case
print(eval_rpn(["2", "1", "+", "3", "*"]))  # Output: 9

Explanation:

  • Push operands onto the stack.

  • When encountering an operator, pop two operands, perform the operation, and push the result back.