15 Stack Interview Questions and Answers
Prepare for your technical interview with our comprehensive guide on stack data structures, featuring common and advanced questions.
Prepare for your technical interview with our comprehensive guide on stack data structures, featuring common and advanced questions.
Stacks are a fundamental data structure in computer science, characterized by their Last In, First Out (LIFO) behavior. They are essential for various applications, including expression evaluation, backtracking algorithms, and memory management. Understanding stacks is crucial for solving complex problems efficiently and is a key concept in many programming languages and systems.
This article offers a curated selection of stack-related interview questions designed to test and enhance your understanding of this vital data structure. By working through these questions, you will gain the confidence and knowledge needed to tackle stack-related challenges in technical interviews.
A stack is a linear data structure that follows the Last In, First Out (LIFO) principle. It is used in various applications such as expression evaluation, backtracking algorithms, and function call management in programming languages.
The primary operations of a stack are:
Here is a simple implementation of a stack in Python:
class Stack: def __init__(self): self.items = [] def push(self, item): self.items.append(item) def pop(self): if not self.is_empty(): return self.items.pop() return None def peek(self): if not self.is_empty(): return self.items[-1] return None def is_empty(self): return len(self.items) == 0 # Example usage stack = Stack() stack.push(1) stack.push(2) stack.push(3) print(stack.peek()) # Output: 3 print(stack.pop()) # Output: 3 print(stack.pop()) # Output: 2 print(stack.is_empty()) # Output: False
A stack is particularly useful for reversing a string, as you can push each character onto the stack and then pop them off in reverse order.
Here is a simple implementation in Python:
def reverse_string_using_stack(s): stack = [] # Push all characters of the string onto the stack for char in s: stack.append(char) # Pop all characters from the stack and form the reversed string reversed_string = '' while stack: reversed_string += stack.pop() return reversed_string # Example usage print(reverse_string_using_stack("hello")) # Output: "olleh"
To check for balanced parentheses in an expression using a stack, traverse the expression and use the stack to keep track of opening parentheses. When a closing parenthesis is encountered, check if it matches the top of the stack. If it does, pop the stack; otherwise, the parentheses are not balanced.
Example:
def is_balanced(expression): stack = [] pairs = {')': '(', '}': '{', ']': '['} for char in expression: if char in pairs.values(): stack.append(char) elif char in pairs.keys(): if stack == [] or pairs[char] != stack.pop(): return False return stack == [] # Test cases print(is_balanced("()")) # True print(is_balanced("({[)]}")) # False print(is_balanced("{[()]}")) # True
To implement a stack using two queues, use one queue to hold the elements and another to assist in the push operation. The idea is to always keep the newest element at the front of the main queue.
from queue import Queue class StackUsingQueues: def __init__(self): self.q1 = Queue() self.q2 = Queue() def push(self, x): self.q2.put(x) while not self.q1.empty(): self.q2.put(self.q1.get()) self.q1, self.q2 = self.q2, self.q1 def pop(self): if self.q1.empty(): return None return self.q1.get() def top(self): if self.q1.empty(): return None return self.q1.queue[0] def empty(self): return self.q1.empty() # Example usage: stack = StackUsingQueues() stack.push(1) stack.push(2) stack.push(3) print(stack.pop()) # Output: 3 print(stack.top()) # Output: 2 print(stack.empty()) # Output: False
To sort a stack using only stack operations, use an auxiliary stack to hold elements in sorted order. Pop elements from the original stack and push them into the auxiliary stack so that the largest elements are on the top.
def sort_stack(original_stack): auxiliary_stack = [] while original_stack: temp = original_stack.pop() while auxiliary_stack and auxiliary_stack[-1] > temp: original_stack.append(auxiliary_stack.pop()) auxiliary_stack.append(temp) while auxiliary_stack: original_stack.append(auxiliary_stack.pop()) return original_stack # Example usage stack = [34, 3, 31, 98, 92, 23] sorted_stack = sort_stack(stack) print(sorted_stack) # Output: [3, 23, 31, 34, 92, 98]
A postfix expression is a mathematical notation where operators follow their operands. A stack is ideal for evaluating postfix expressions because it allows for efficient management of operands and operators.
Here is a concise example of how to evaluate a postfix expression using a stack:
def evaluate_postfix(expression): stack = [] for token in expression.split(): if token.isdigit(): 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(a / b) return stack[0] expression = "3 4 + 2 * 7 /" print(evaluate_postfix(expression)) # Output: 2.0
Recursion uses the call stack to keep track of function calls. When a recursive function is called, the current state is pushed onto the call stack. This allows the function to return to the correct state once the recursive call completes. Each recursive call creates a new frame on the stack, and the function continues to call itself until a base case is reached. Once the base case is met, the stack begins to unwind, and each function call is popped off the stack, returning control to the previous call.
Example:
def factorial(n): if n == 0: return 1 else: return n * factorial(n - 1) print(factorial(5)) # Output: 120
In this example, the factorial
function calls itself with n-1
until n
is 0. Each call to factorial
is pushed onto the call stack. When n
reaches 0, the base case is met, and the stack unwinds, returning the result of each multiplication.
To find the minimum element in a stack in constant time, use an auxiliary stack to keep track of the minimum elements. Maintain a secondary stack that stores the minimum values corresponding to the elements in the main stack.
Here is a concise implementation:
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: if self.stack[-1] == self.min_stack[-1]: self.min_stack.pop() return self.stack.pop() def get_min(self): if self.min_stack: return self.min_stack[-1]
In this implementation, the push
method adds an element to the main stack and also updates the minimum stack if the new element is smaller than or equal to the current minimum. The pop
method removes the top element from the main stack and also updates the minimum stack if the removed element is the current minimum. The get_min
method retrieves the minimum element in constant time.
Infix expressions are the common arithmetic expressions where operators are placed between operands. Postfix expressions, also known as Reverse Polish Notation (RPN), place operators after their operands. Converting an infix expression to a postfix expression can be efficiently done using a stack to handle operator precedence and parentheses.
Here is a concise example of how to implement this conversion using a stack:
def infix_to_postfix(expression): precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '^': 3} stack = [] output = [] for char in expression: if char.isalnum(): output.append(char) elif char == '(': stack.append(char) elif char == ')': while stack and stack[-1] != '(': output.append(stack.pop()) stack.pop() else: while stack and stack[-1] != '(' and precedence[char] <= precedence[stack[-1]]: output.append(stack.pop()) stack.append(char) while stack: output.append(stack.pop()) return ''.join(output) # Example usage: expression = "A*(B+C)/D" print(infix_to_postfix(expression)) # Output: "ABC+*D/"
To implement a stack that supports push, pop, and retrieving the maximum element in constant time, use two stacks: one for storing the actual stack elements and another for keeping track of the maximum elements.
Here is a concise implementation:
class MaxStack: def __init__(self): self.stack = [] self.max_stack = [] def push(self, x): self.stack.append(x) if not self.max_stack or x >= self.max_stack[-1]: self.max_stack.append(x) def pop(self): if self.stack: if self.stack[-1] == self.max_stack[-1]: self.max_stack.pop() return self.stack.pop() def get_max(self): if self.max_stack: return self.max_stack[-1] # Example usage: max_stack = MaxStack() max_stack.push(5) max_stack.push(1) max_stack.push(5) print(max_stack.get_max()) # Output: 5 max_stack.pop() print(max_stack.get_max()) # Output: 5 max_stack.pop() print(max_stack.get_max()) # Output: 5
In a stack, the primary operations are push, pop, peek, and isEmpty. The time complexity of these operations is important for understanding the efficiency of stack usage in different scenarios.
Depth-first search (DFS) is an algorithm used to traverse or search through graph data structures. DFS explores as far as possible along each branch before backtracking, making it a natural fit for a stack-based approach.
In DFS, a stack is used to keep track of the nodes to be explored. The algorithm starts at a given node, marks it as visited, and pushes it onto the stack. It then iteratively pops nodes from the stack, explores their unvisited neighbors, marks them as visited, and pushes them onto the stack. This process continues until the stack is empty.
Example:
def dfs(graph, start): visited = set() stack = [start] while stack: node = stack.pop() if node not in visited: visited.add(node) stack.extend(neighbor for neighbor in graph[node] if neighbor not in visited) return visited graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } print(dfs(graph, 'A')) # Output: {'A', 'B', 'C', 'D', 'E', 'F'}
A stack is particularly useful for backtracking algorithms, where you need to explore possible solutions and revert to previous states when a dead end is reached.
In backtracking algorithms, a stack is used to keep track of the choices made at each step. When a decision leads to a dead end, the algorithm can backtrack by popping the last choice off the stack and trying a different path.
Example:
def solve_maze(maze, start, end): stack = [(start, [start])] while stack: (position, path) = stack.pop() for next_position in get_neighbors(position, maze): if next_position == end: return path + [next_position] if next_position not in path: stack.append((next_position, path + [next_position])) return None def get_neighbors(position, maze): neighbors = [] for move in [(0, 1), (1, 0), (0, -1), (-1, 0)]: new_position = (position[0] + move[0], position[1] + move[1]) if 0 <= new_position[0] < len(maze) and 0 <= new_position[1] < len(maze[0]) and maze[new_position[0]][new_position[1]] == 0: neighbors.append(new_position) return neighbors maze = [ [0, 1, 0, 0, 0], [0, 1, 0, 1, 0], [0, 0, 0, 1, 0], [0, 1, 1, 1, 0], [0, 0, 0, 0, 0] ] start = (0, 0) end = (4, 4) print(solve_maze(maze, start, end)) # Output: [(0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (3, 2), (4, 2), (4, 3), (4, 4)]
In concurrent programming, stacks can present several common pitfalls:
Example:
import threading stack = [] def push_item(item): stack.append(item) def pop_item(): if stack: return stack.pop() return None # Simulating race condition thread1 = threading.Thread(target=push_item, args=(1,)) thread2 = threading.Thread(target=pop_item) thread1.start() thread2.start() thread1.join() thread2.join()
In this example, the lack of synchronization can lead to a race condition where the stack’s state becomes inconsistent.
import heapq class MedianStack: def __init__(self): self.min_heap = [] self.max_heap = [] self.stack = [] def push(self, val): self.stack.append(val) if not self.max_heap or val <= -self.max_heap[0]: heapq.heappush(self.max_heap, -val) else: heapq.heappush(self.min_heap, val) self._balance_heaps() def pop(self): if not self.stack: return None val = self.stack.pop() if val <= -self.max_heap[0]: self.max_heap.remove(-val) heapq.heapify(self.max_heap) else: self.min_heap.remove(val) heapq.heapify(self.min_heap) self._balance_heaps() return val def get_median(self): if len(self.max_heap) > len(self.min_heap): return -self.max_heap[0] elif len(self.min_heap) > len(self.max_heap): return self.min_heap[0] else: return (-self.max_heap[0] + self.min_heap[0]) / 2 def _balance_heaps(self): if len(self.max_heap) > len(self.min_heap) + 1: heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap)) elif len(self.min_heap) > len(self.max_heap) + 1: heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap)) # Example usage: stack = MedianStack() stack.push(1) stack.push(2) stack.push(3) print(stack.get_median()) # Output: 2 stack.pop() print(stack.get_median()) # Output: 1.5