Stacks and The Call Stack
We’ve covered arrays and linked lists – linear structures that store data. Now, let’s introduce a specialized linear data structure called a Stack. A stack follows a very strict rule for adding and removing elements, and understanding it is key to grasping how computers manage function calls, which we’ll delve into with the Call Stack.
Stack Data Structure (LIFO)
Elements are added and removed only from the top. Last-In, First-Out.
🏗️ What is a Stack?
A stack is an abstract data type (ADT) that serves as a collection of elements, with two principal operations:
- Push: Adds an element to the collection.
- Pop: Removes the most recently added element that was not yet removed.
This behavior is known as LIFO (Last-In, First-Out). Think of a stack of plates: you always add new plates to the top, and you always take plates from the top. The last plate you put on is the first one you take off.
Core Operations:
push(element)
: Adds an element to the top of the stack.pop()
: Removes and returns the element from the top of the stack. If the stack is empty, it typically throws an error or returns a special value.peek()
(ortop()
): Returns the element at the top of the stack without removing it.isEmpty()
: Checks if the stack contains any elements.size()
: Returns the number of elements in the stack.
Analogy: A spring-loaded plate dispenser in a cafeteria. You can only interact with the top plate.
💻 Implementing a Stack
Stacks can be implemented using either dynamic arrays (like Python lists or JavaScript arrays) or linked lists. Both have their advantages.
Using a Dynamic Array (List)
This is often the simplest implementation, especially in languages with dynamic arrays that efficiently support appending/removing from the end.
#include <stdio.h>
#include <stdlib.h> // For malloc, realloc, free
typedef struct {
int *data;
int top; // Index of the top element (or -1 if empty)
int capacity; // Current allocated capacity
} ArrayStack;
// Initialize stack
void init_array_stack(ArrayStack *s, int initialCapacity) {
s->data = (int *) malloc(sizeof(int) * initialCapacity);
if (s->data == NULL) { /* Handle error */ exit(EXIT_FAILURE); }
s->top = -1; // Stack is empty
s->capacity = initialCapacity;
}
// Check if stack is empty
int is_empty_array_stack(ArrayStack *s) {
return s->top == -1;
}
// Push an element (amortized O(1))
void push_array_stack(ArrayStack *s, int item) {
if (s->top == s->capacity - 1) { // Stack is full, resize
s->capacity *= 2;
s->data = (int *) realloc(s->data, sizeof(int) * s->capacity);
if (s->data == NULL) { /* Handle error */ exit(EXIT_FAILURE); }
printf("Stack resized to capacity: %d\n", s->capacity);
}
s->data[++(s->top)] = item; // Increment top, then add
}
// Pop an element (O(1))
int pop_array_stack(ArrayStack *s) {
if (is_empty_array_stack(s)) {
printf("Error: Stack is empty!\n");
exit(EXIT_FAILURE);
}
return s->data[(s->top)--]; // Return top, then decrement
}
// Peek at the top element (O(1))
int peek_array_stack(ArrayStack *s) {
if (is_empty_array_stack(s)) {
printf("Error: Stack is empty for peek!\n");
exit(EXIT_FAILURE);
}
return s->data[s->top];
}
// Cleanup
void free_array_stack(ArrayStack *s) {
free(s->data);
s->data = NULL;
s->top = -1;
s->capacity = 0;
}
int main() {
ArrayStack s;
init_array_stack(&s, 2);
push_array_stack(&s, 10); // [10]
push_array_stack(&s, 20); // [10, 20]
printf("Peek: %d\n", peek_array_stack(&s)); // 20
push_array_stack(&s, 30); // [10, 20, 30] (resizes)
printf("Popped: %d\n", pop_array_stack(&s)); // 30
printf("Popped: %d\n", pop_array_stack(&s)); // 20
printf("Is Empty: %d\n", is_empty_array_stack(&s)); // 0 (false)
printf("Popped: %d\n", pop_array_stack(&s)); // 10
printf("Is Empty: %d\n", is_empty_array_stack(&s)); // 1 (true)
free_array_stack(&s);
return 0;
}
class Stack:
def __init__(self):
self._items = [] # Use a Python list as the underlying storage
def is_empty(self):
return len(self._items) == 0
def push(self, item):
self._items.append(item) # O(1) amortized
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
return self._items.pop() # O(1)
def peek(self):
if self.is_empty():
raise IndexError("peek from empty stack")
return self._items[-1] # O(1)
def size(self):
return len(self._items)
# Main for demonstration
s = Stack()
s.push(10) # [10]
s.push(20) # [10, 20]
print(f"Peek: {s.peek()}") # 20
s.push(30) # [10, 20, 30]
print(f"Popped: {s.pop()}") # 30
print(f"Popped: {s.pop()}") # 20
print(f"Is Empty: {s.is_empty()}") # False
print(f"Popped: {s.pop()}") # 10
print(f"Is Empty: {s.is_empty()}") # True
class Stack {
constructor() {
this._items = []; // Use a JavaScript array as the underlying storage
}
isEmpty() {
return this._items.length === 0;
}
push(item) {
this._items.push(item); // O(1) amortized
}
pop() {
if (this.isEmpty()) {
throw new Error("pop from empty stack");
}
return this._items.pop(); // O(1)
}
peek() {
if (this.isEmpty()) {
throw new Error("peek from empty stack");
}
return this._items[this._items.length - 1]; // O(1)
}
size() {
return this._items.length;
}
}
// Main for demonstration
const s = new Stack();
s.push(10); // [10]
s.push(20); // [10, 20]
console.log("Peek:", s.peek()); // 20
s.push(30); // [10, 20, 30]
console.log("Popped:", s.pop()); // 30
console.log("Popped:", s.pop()); // 20
console.log("Is Empty:", s.isEmpty()); // false
console.log("Popped:", s.pop()); // 10
console.log("Is Empty:", s.isEmpty()); // true
Complexity (Stack Operations using Dynamic Array):
- Push: O(1) (amortized, due to occasional resizing, just like dynamic arrays).
- Pop: O(1)
- Peek: O(1)
- isEmpty: O(1)
- Space Complexity: O(N) for storing N elements.
📞 The Call Stack
The Call Stack is a specialized type of stack data structure that programming languages use to manage function calls during program execution. Whenever a function is called, a new “frame” is pushed onto the call stack. When a function returns, its frame is popped off the stack.
How it works:
- Main/Global Scope: When your program starts, the first frame (often for
main()
or the global scope) is pushed onto the call stack. - Function Call: When a function
A
calls another functionB
, a new frame forB
is created and pushed on top ofA
’s frame. This new frame containsB
’s local variables, parameters, and the return address (where to go back inA
). - Function Execution: Function
B
executes. - Function Return: When
B
finishes or returns, its frame is popped off the call stack. Execution resumes from the return address inA
’s frame. - Program End: When the initial (e.g.,
main()
) frame is popped, the program terminates.
The Program Call Stack
Manages function calls using the LIFO principle: last function called is the first to finish.
If too many frames are pushed without returning, it leads to a **Stack Overflow**!
Analogy: Imagine a stack of to-do lists. When you start a big task, you put its list on top. If that task requires a sub-task, you make a new list for it and put it on top. You always work on the list at the very top. When a sub-task is done, you remove its list and continue with the task below it.
Stack Overflow
A Stack Overflow error occurs when the call stack runs out of memory. This typically happens in two scenarios:
- Infinite Recursion: A function calls itself (directly or indirectly) infinitely without reaching a base case. Each call pushes a new frame, eventually overflowing the fixed-size stack memory.
- Too many nested function calls: Even without infinite recursion, an extremely deep chain of legitimate function calls can exceed the stack’s capacity.
#include <stdio.h>
void infinite_recursion() {
printf("Pushing another frame...\n");
infinite_recursion(); // Calls itself endlessly
}
int main() {
infinite_recursion();
return 0;
}
// This program will eventually crash with a 'Stack Overflow' error.
import sys
# Python has a default recursion limit (e.g., 1000)
# You can increase it, but will still hit stack overflow for deep recursion.
# sys.setrecursionlimit(2000)
def infinite_recursion():
print("Pushing another frame...")
infinite_recursion() # Calls itself endlessly
# This program will eventually hit Python's recursion limit and raise
# a RecursionError, which is Python's way of preventing a C-level stack overflow.
try:
infinite_recursion()
except RecursionError:
print("Caught RecursionError (Python's stack overflow)")
function infiniteRecursion() {
console.log("Pushing another frame...");
infiniteRecursion(); // Calls itself endlessly
}
// This program will eventually crash with a 'Maximum call stack size exceeded' error.
try {
infiniteRecursion();
} catch (e) {
console.error("Caught error:", e.message); // Will be "Maximum call stack size exceeded"
}
Practical Usages of the Call Stack:
- Function Call Management: The fundamental mechanism behind how programs execute.
- Recursion: Essential for handling recursive function calls, providing the mechanism to return to the correct point after each call.
- Debugging: Debuggers allow you to inspect the call stack, showing the sequence of function calls that led to the current point in execution.
- Exception Handling: When an exception is thrown, the runtime unwinds the call stack until a matching
catch
block is found.
Note: The concept of a call stack highlights the finite nature of stack memory. While stacks are efficient for managing local variables and function flow, problems requiring very deep recursion might need to be refactored to use an iterative approach or explicit stack data structures to avoid stack overflow.
⚖️ Stack vs. Queue (Brief Intro)
While stacks are LIFO, their counterpart, queues, are FIFO (First-In, First-Out).
- Stack (LIFO): Last element added is the first one out. (e.g.,
push
,pop
). - Queue (FIFO): First element added is the first one out. (e.g.,
enqueue
,dequeue
).
We’ll delve deeper into queues in a future lesson, but it’s important to recognize these two fundamental patterns of adding/removing from linear collections.
Note: Stacks are a fundamental component in many algorithms, including expression parsing (infix to postfix), depth-first search (DFS) in graphs, and implementing undo/redo functionality in applications. Their simple, constrained access pattern makes them predictable and powerful.