Stack (LIFO)
Last In, First Out. You can only add or remove from the top. Think of a stack of plates.
You have probably stood in a line at a coffee shop, or hit “undo” a hundred times while writing an essay. Believe it or not, both of those everyday moments are basically data structures in disguise. In this guide, we will meet two of the most useful ones in programming: the Stack and the Queue. Then we will use what we learn to solve a classic problem: converting math expressions between infix, prefix, and postfix form.
Imagine a stack of plates at a buffet. You can only do two things with that stack:
You can never grab a plate from the middle or the bottom without removing everything above it first. That is exactly how a stack data structure works.
This behavior has a name: LIFO, which stands for Last In, First Out. The last plate you put down is the first one someone picks up.
In a stack, there is one special position called the top. Every single operation happens here.
Push sequence: 10, 20, 30
Top ↓+----+| 30 |+----+| 20 |+----+| 10 |+----+If you remove (pop) an item right now, 30 comes off first, since it was the last one added.
| Operation | What it does |
|---|---|
| push(x) | Adds x to the top |
| pop() | Removes and gives you the top item |
| peek() / top() | Shows you the top item, but does not remove it |
| isEmpty() | Tells you if the stack has nothing in it |
| isFull() | Tells you if the stack has run out of room (only matters for array-based stacks) |
There is more than one way to build a stack. Let us walk through the common approaches, from the most basic to the most practical.
This is the “from scratch” way of building a stack. You set up a list with a fixed size and keep track of where the top is using a number called top.
When the stack is empty, top starts at -1, because there is nothing there yet.
Initialize: top = -1 size = N arr[size]
push(x): if top == size - 1: overflow (no more room!) top = top + 1 arr[top] = x
pop(): if top == -1: underflow (nothing to remove!) x = arr[top] top = top - 1 return x
peek(): if top == -1: error return arr[top]Here is what this looks like in actual Python code:
class Stack: def __init__(self, size): self.size = size self.arr = [None] * size self.top = -1
def push(self, x): if self.top == self.size - 1: raise Exception("Stack Overflow") self.top += 1 self.arr[self.top] = x
def pop(self): if self.top == -1: raise Exception("Stack Underflow") x = self.arr[self.top] self.top -= 1 return x
def peek(self): if self.top == -1: raise Exception("Stack is Empty") return self.arr[self.top]
def is_empty(self): return self.top == -1Every operation here (push, pop, peek) takes the same amount of time no matter how big the stack is. In Big-O terms, that is O(1) for each one, and the whole stack takes up O(N) space in memory.
The problem with the array version above is that it has a fixed size. If you said the stack could hold 10 items and you try to push an 11th, you get an overflow error, even if your computer has plenty of free memory.
A linked list based stack fixes this. Instead of a fixed-size array, you build the stack out of nodes that point to each other, and you can keep adding nodes as long as your computer has memory available.
Top ↓+------+ +------+ +------+| 30 | -> | 20 | -> | 10 | -> None+------+class Node: def __init__(self, data): self.data = data self.next = None
class Stack: def __init__(self): self.top = None
def push(self, x): node = Node(x) node.next = self.top self.top = node
def pop(self): if self.top is None: raise Exception("Stack Underflow") x = self.top.data self.top = self.top.next return x
def peek(self): if self.top is None: raise Exception("Stack is Empty") return self.top.dataThis still gives you O(1) push and pop, but now you never run out of room unless your computer’s memory itself runs out.
Here is the good news: in real projects, you almost never build a stack from scratch. Python’s regular list already behaves like a stack if you only use it from one end.
stack = []
stack.append(10) # pushstack.append(20)stack.append(30)
stack.pop() # removes and returns 30stack[-1] # peek at the top, without removing itdequeIf you want to be extra safe and explicit, Python has a tool called deque (pronounced “deck”) that is built exactly for this kind of fast adding and removing.
from collections import deque
stack = deque()
stack.append(10) # pushstack.append(20)
stack.pop() # popstack[-1] # peekIf someone in an interview asks you “list or deque for a stack?”, the honest answer is: a list works fine, but deque is considered cleaner and a bit safer, so many people reach for it by default.
Here is something that might surprise you: every time you call a function, your program is secretly using a stack behind the scenes. It is called the call stack.
def fact(n): if n == 0: return 1 return n * fact(n - 1)When you call fact(3), here is what happens to the call stack:
fact(3) -> fact(2) -> fact(1) -> fact(0)Each call gets stacked on top of the previous one, and they only finish (and get removed) once the one above them is done. This is also why infinite recursion crashes your program:
def f(): f()
f() # RecursionError - this is a stack overflow!The function keeps calling itself, adding more and more to the call stack, until there is no room left.
Stacks are not just a theory exercise. They power some genuinely useful tools and show up constantly in coding interviews.
Ever wonder how your code editor knows you forgot to close a bracket? Stacks make that possible.
Input: "{[()]}"Output: True (every bracket is properly closed)The idea is simple:
def is_valid(s): stack = [] mp = {')': '(', ']': '[', '}': '{'}
for ch in s: if ch in mp.values(): stack.append(ch) else: if not stack or stack.pop() != mp[ch]: return False
return not stackThis pattern shows up a lot in interviews. The goal is to find, for each item in a list, the next item that is bigger than it - and to do it efficiently, without checking every pair.
It is used in problems like:
Input: "23*54*+9-"You scan left to right. Whenever you see a number, push it. Whenever you see an operator, pop the top two numbers, do the math, and push the result back. We will cover this expression stuff in detail later in this guide.
This is a stack that can also tell you the smallest item in it, instantly, in O(1) time. The trick is to keep a second helper stack that tracks the minimum at each point.
Before we move to queues, here is a sneak peek at how they differ:
| Stack | Queue |
|---|---|
| LIFO (Last In, First Out) | FIFO (First In, First Out) |
| Works from a single end | Works from two ends |
| Used in DFS (Depth First Search) | Used in BFS (Breadth First Search) |
Now picture a line of people waiting to buy movie tickets. The person who joined the line first gets served first. Nobody can cut in line and get served before the people ahead of them. That is a Queue.
This is called FIFO: First In, First Out.
A queue has two ends that matter:
Front -> where elements leave the queueRear -> where elements join the queue| Operation | Meaning |
|---|---|
| enqueue | Add an element at the rear |
| dequeue | Remove the element at the front |
| peek / front | Look at the front element |
| isEmpty | Check if the queue has nothing in it |
| isFull | Check if the queue has run out of room (array-based only) |
One important rule: a queue never lets you insert or delete from the middle. Only the front and the rear matter.
The simplest version keeps track of two index numbers: front and rear.
Initially:front = -1rear = -1Adding an item (enqueue):
if rear == capacity - 1: overflowelse: if front == -1: front = 0 rear += 1 arr[rear] = valueRemoving an item (dequeue):
if front == -1 or front > rear: underflowelse: value = arr[front] front += 1A circular queue solves the wasted-space problem by treating the array like a loop. Once you reach the last slot, you wrap back around to the first one.
next_index = (index + 1) % capacityHere is what it looks like visually. Notice the two empty slots at the front are still usable - they have not been wasted:
[ _ | _ | 30 | 40 | 50 ] ^ ^ front rearChecking if it’s full:
front == (rear + 1) % capacityChecking if it’s empty:
front == -1Enqueue logic:
if (front == (rear + 1) % capacity): overflowelse: if front == -1: front = rear = 0 else: rear = (rear + 1) % capacity arr[rear] = valueDequeue logic:
if front == -1: underflowelse: value = arr[front] if front == rear: front = rear = -1 else: front = (front + 1) % capacityJust like with stacks, a linked-list based queue grows and shrinks freely, without ever needing to “wrap around” or worry about a fixed size.
Node: data next
Queue: front pointer rear pointerEnqueue:
create new nodeif rear is NULL: front = rear = new nodeelse: rear.next = new node rear = new nodeDequeue:
if front is NULL: underflowelse: temp = front front = front.next if front is NULL: rear = NULLBoth enqueue and dequeue here take O(1) time.
queue = []
queue.append(10) # enqueuequeue.append(20)
queue.pop(0) # dequeue, but this is slow!The problem is pop(0). Removing the very first item of a Python list forces every other item to shift over by one position, which takes O(n) time. For small queues this barely matters, but it adds up fast in real applications.
dequeThe name deque literally stands for Double-Ended Queue, and it is built exactly for this job.
from collections import deque
q = deque()
q.append(10) # enqueueq.append(20)
q.popleft() # dequeueNot every queue is the plain “first come, first served” kind. Here are the variations you will run into:
The basic kind we just covered. Strict FIFO, one end for adding, one end for removing.
Reuses freed-up space by wrapping around to the beginning of the array, instead of wasting it.
Instead of “first in, first out,” items leave based on priority rather than the order they arrived in. The most important (or least important) item goes first, no matter when it was added.
Python gives you this through the heapq module:
import heapq
pq = []
heapq.heappush(pq, 5)heapq.heappush(pq, 1)heapq.heappush(pq, 10)
heapq.heappop(pq) # always returns the smallest valueThis shows up in things like:
A deque lets you add or remove from both ends, not just one.
dq.append(10) # add to the rightdq.appendleft(20) # add to the leftdq.pop() # remove from the rightdq.popleft() # remove from the leftThis flexibility makes it perfect for things like sliding window problems, where you need to add and remove items from either side as a “window” moves across some data.
A more advanced trick where you keep the queue’s elements always increasing or always decreasing. It looks intimidating but the core idea is simple: as new elements come in, you remove smaller (or larger) elements from the back that can no longer matter.
This is the secret behind problems like “Sliding Window Maximum.”
Queues are everywhere once you start noticing them:
Here is what a queue-powered BFS actually looks like in code:
from collections import deque
def bfs(graph, start): visited = set() q = deque([start])
while q: node = q.popleft() if node not in visited: visited.add(node) for nei in graph[node]: q.append(nei)| Implementation | Enqueue / Push | Dequeue / Pop |
|---|---|---|
| Array | O(1) | O(1) |
| Circular Array | O(1) | O(1) |
| Linked List | O(1) | O(1) |
| Python list (as queue) | O(1) | O(n) |
| Python deque | O(1) | O(1) |
The takeaway: deque is almost always your safest, fastest choice in Python, whether you are building a stack or a queue.
Here is something cool: stacks are the secret tool behind how calculators and compilers understand math expressions. Let us use everything we just learned about stacks to solve a classic problem - converting between infix, prefix, and postfix expressions.
Before converting anything, you need two pieces of math knowledge locked in.
Operator precedence (which operation happens first):
^ highest priority* / medium priority+ - lowest priorityAssociativity (which direction ties get resolved):
^ -> right associative (resolves right to left)+ - * / -> left associative (resolves left to right)You have been using infix your whole life. It just means the operator sits between the two values, like in normal math class.
A + B(A + B) * CPrefix puts the operator before the values:
+ A B* + A B CPostfix puts the operator after the values:
A B +A B C * +This is the most common conversion you will be asked to do, and it leans directly on a stack.
The general rule:
operand (a letter or number) -> send straight to the outputoperator -> push to the stack (after checking precedence)( -> push to the stack) -> pop everything until you hit a matching (Let’s convert A + B * C step by step, very slowly so nothing is skipped:
| Read | Stack | Output so far |
|---|---|---|
| A | empty | A |
| + | + | A |
| B | + | AB |
| * | + * | AB |
| C | + * | ABC |
| END | empty | ABC*+ |
Final postfix result:
ABC*+Why does this make sense? Because * has higher precedence than +, the multiplication needs to happen first. Postfix naturally captures that: B C * means “multiply B and C first,” and then A gets added to that result.
B * C happens firstthen A + (that result)The four-step recipe:
1. Reverse the infix expression2. Swap every ( with ) and every ) with (3. Convert that to postfix (using the method above)4. Reverse the postfix result - that gives you prefixLet’s convert (A + B) * C.
Step 1: Reverse the expression
C * ) B + A (Step 2: Swap the brackets
C * ( B + A )Step 3: Convert this to postfix
Converting C * (B + A) to postfix gives:
CBA+*Step 4: Reverse that result
*+ABCFinal prefix result:
*+ABCQuick sanity check: Reading * + A B C as “do the + on A and B first, then * that result with C” gives you back (A + B) * C. That matches what we started with, so we know the conversion worked.
For this one, you read the expression from right to left, and use a stack to combine pieces as you go.
The pattern:
Prefix: operator A BPostfix: A B operatorLet’s convert *+ABC.
| Read (right to left) | Stack |
|---|---|
| C | C |
| B | C, B |
| A | C, B, A |
| + | (A+B) |
| * | (A+B)*C |
Final postfix result:
AB+C*This works almost the same way as prefix to postfix, just scanning right to left and combining pieces with brackets instead.
The pattern:
Prefix: operator A BInfix: (A operator B)Using the same expression, *+ABC:
| Read (right to left) | Stack |
|---|---|
| C | C |
| B | C, B |
| A | C, B, A |
| + | (A+B) |
| * | (A+B)*C |
Final infix result:
(A+B)*CNow we flip directions: postfix gets read left to right.
The pattern:
Postfix: A B operatorInfix: (A operator B)Let’s convert AB+C*.
| Read (left to right) | Stack |
|---|---|
| A | A |
| B | A, B |
| + | (A+B) |
| C | (A+B), C |
| * | (A+B)*C |
Final infix result:
(A+B)*CSame left-to-right scan as above, but you build the prefix form instead of the infix form.
The pattern:
Postfix: A B operatorPrefix: operator A BLet’s convert AB+C*.
| Read (left to right) | Stack |
|---|---|
| A | A |
| B | A, B |
| + | +AB |
| C | +AB, C |
| * | *+ABC |
Final prefix result:
*+ABC| From | To | How to do it |
|---|---|---|
| Infix | Postfix | Use a stack and check precedence |
| Infix | Prefix | Reverse, convert to postfix, reverse again |
| Prefix | Postfix | Scan right to left |
| Prefix | Infix | Scan right to left |
| Postfix | Infix | Scan left to right |
| Postfix | Prefix | Scan left to right |
Stack (LIFO)
Last In, First Out. You can only add or remove from the top. Think of a stack of plates.
Queue (FIFO)
First In, First Out. You add at the rear and remove from the front. Think of a line at a store.
Overflow / Underflow
Overflow = trying to add to something that is already full. Underflow = trying to remove from something that is already empty.
deque is your friend
For both stacks and queues in Python, collections.deque gives you fast, reliable O(1) operations on both ends.
Circular Queue
Solves the wasted-space problem of a basic array queue by wrapping back around to the start once the end is reached.
Expression Conversion
Infix is what you write by hand. Prefix and postfix are what computers prefer, since they skip the need for brackets and precedence rules.
deque over a plain list for both stacks and queuesheapq) dequeue by importance, not arrival order