Queues and Stacks
Queues and stacks are two simple ways to organize a collection of items.
They are not complicated data structures. Their power comes from the rules they enforce.
A stack gives you the last item first.
A queue gives you the first item first.
These rules appear everywhere in programming: parsers, schedulers, interpreters, graph algorithms, job systems, undo history, message processing, and event loops.
The Basic Difference
A stack is last-in, first-out.
push 10
push 20
push 30
pop -> 30
pop -> 20
pop -> 10The last item added is the first item removed.
A queue is first-in, first-out.
enqueue 10
enqueue 20
enqueue 30
dequeue -> 10
dequeue -> 20
dequeue -> 30The first item added is the first item removed.
| Structure | Add operation | Remove operation | Rule |
|---|---|---|---|
| Stack | push | pop | Last in, first out |
| Queue | enqueue | dequeue | First in, first out |
Stacks
A stack behaves like a pile of plates.
You add to the top.
You remove from the top.
top
[30]
[20]
[10]
bottomThe basic operations are:
push: add an item
pop: remove the most recent item
peek: look at the most recent item without removing itBuilding a Stack with ArrayList
In Zig, the easiest beginner stack is an ArrayList.
Appending to the end is push.
Removing from the end is pop.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var stack = std.ArrayList(i32).init(allocator);
defer stack.deinit();
try stack.append(10);
try stack.append(20);
try stack.append(30);
const a = stack.pop();
const b = stack.pop();
std.debug.print("{} {}\n", .{ a, b });
}Output:
30 20The last value appended was 30, so it comes out first.
Stack Push
This is the push operation:
try stack.append(10);It may allocate memory, so it uses try.
Appending to an ArrayList is usually fast. Sometimes the list must grow its internal buffer, so the operation can allocate.
Stack Pop
This is the pop operation:
const value = stack.pop();In many Zig versions, pop() removes and returns the last item. The exact return type can vary across standard library versions, so check your installed Zig docs when writing production code.
A safe beginner pattern is to check the length before popping:
if (stack.items.len > 0) {
const value = stack.pop();
std.debug.print("{}\n", .{value});
}This avoids popping from an empty stack.
Stack Peek
To look at the top item without removing it, read the last element:
if (stack.items.len > 0) {
const top = stack.items[stack.items.len - 1];
std.debug.print("top = {}\n", .{top});
}The index is:
stack.items.len - 1because indexes start at zero.
For this stack:
[10][20][30]the length is 3, and the last index is 2.
index: 0 1 2
value: 10 20 30A Small Stack Wrapper
You can wrap ArrayList in your own stack type.
const std = @import("std");
const Stack = struct {
items: std.ArrayList(i32),
pub fn init(allocator: std.mem.Allocator) Stack {
return Stack{
.items = std.ArrayList(i32).init(allocator),
};
}
pub fn deinit(self: *Stack) void {
self.items.deinit();
}
pub fn push(self: *Stack, value: i32) !void {
try self.items.append(value);
}
pub fn pop(self: *Stack) ?i32 {
if (self.items.items.len == 0) return null;
return self.items.pop();
}
pub fn peek(self: *Stack) ?i32 {
if (self.items.items.len == 0) return null;
return self.items.items[self.items.items.len - 1];
}
};Using it:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var stack = Stack.init(gpa.allocator());
defer stack.deinit();
try stack.push(10);
try stack.push(20);
if (stack.pop()) |value| {
std.debug.print("{}\n", .{value});
}
}Output:
20This wrapper gives your code clearer names.
Instead of exposing every ArrayList operation, you expose only stack operations.
Why Stacks Matter
Stacks appear naturally when a program must remember nested work.
Examples:
function calls
expression parsing
undo history
depth-first search
matching parentheses
temporary compiler stateWhen a function calls another function, the program uses a call stack.
When a parser enters nested parentheses, it often uses a stack.
When an editor implements undo, the most recent change is undone first.
That is stack behavior.
Queues
A queue behaves like a line of people.
The first person in line is served first.
front back
[10] -> [20] -> [30]The basic operations are:
enqueue: add an item to the back
dequeue: remove an item from the front
peek: look at the front item without removing itA Simple Queue with ArrayList
You can build a beginner queue using ArrayList.
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var queue = std.ArrayList(i32).init(allocator);
defer queue.deinit();
try queue.append(10);
try queue.append(20);
try queue.append(30);
const first = queue.orderedRemove(0);
const second = queue.orderedRemove(0);
std.debug.print("{} {}\n", .{ first, second });
}Output:
10 20This works, but it has a cost.
Every time you remove index 0, the remaining items shift left.
Before:
[10][20][30][40]
Remove 10:
[20][30][40]For small queues, this is fine.
For large queues, it can become expensive.
Better Queue Design: Head Index
A better simple queue keeps a head index.
Instead of removing from index 0, we remember where the front is.
items:
[10][20][30][40]
head = 0
front = items[head]After one dequeue:
items:
[10][20][30][40]
head = 1
front = items[head]The old value is ignored.
No shifting is needed.
Queue with Head Index
Here is a simple integer queue:
const std = @import("std");
const Queue = struct {
items: std.ArrayList(i32),
head: usize,
pub fn init(allocator: std.mem.Allocator) Queue {
return Queue{
.items = std.ArrayList(i32).init(allocator),
.head = 0,
};
}
pub fn deinit(self: *Queue) void {
self.items.deinit();
}
pub fn enqueue(self: *Queue, value: i32) !void {
try self.items.append(value);
}
pub fn dequeue(self: *Queue) ?i32 {
if (self.head >= self.items.items.len) return null;
const value = self.items.items[self.head];
self.head += 1;
return value;
}
pub fn peek(self: *Queue) ?i32 {
if (self.head >= self.items.items.len) return null;
return self.items.items[self.head];
}
pub fn len(self: *const Queue) usize {
return self.items.items.len - self.head;
}
};Using it:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var queue = Queue.init(gpa.allocator());
defer queue.deinit();
try queue.enqueue(10);
try queue.enqueue(20);
try queue.enqueue(30);
while (queue.dequeue()) |value| {
std.debug.print("{}\n", .{value});
}
}Output:
10
20
30This queue removes from the front without shifting all elements.
The Hidden Problem
The head-index queue has one issue.
After many dequeues, the front moves forward:
items:
[old][old][old][40][50][60]
^
headThe old slots are no longer used, but they still occupy memory.
For a long-running queue, you need a cleanup step or a ring buffer.
A ring buffer reuses old slots.
You will see that in a later section.
Stack vs Queue in Algorithms
Two famous graph algorithms show the difference clearly.
Depth-first search uses a stack.
It explores one path deeply before coming back.
visit A
go to B
go to C
backtrackBreadth-first search uses a queue.
It explores nearby nodes first.
visit distance 0
visit distance 1
visit distance 2The collection rule changes the behavior of the algorithm.
Same graph, different data structure, different traversal order.
When to Use a Stack
Use a stack when the newest item should be handled first.
Good examples:
undo operations
nested parsing
temporary states
backtracking
depth-first search
compiler scopesThe rule is simple:
last in, first outWhen to Use a Queue
Use a queue when the oldest item should be handled first.
Good examples:
task scheduling
message handling
network packets
breadth-first search
print jobs
event loopsThe rule is simple:
first in, first outCommon Beginner Mistakes
The first mistake is using orderedRemove(0) for a large queue. It works, but it shifts all remaining items every time.
The second mistake is popping from an empty stack. Check items.len or return an optional.
The third mistake is forgetting that append can fail because it may allocate.
The fourth mistake is exposing too many operations. If you are writing a stack, do not let callers insert into the middle. Wrap the structure and expose only the operations you want.
The fifth mistake is keeping pointers into an ArrayList while pushing more items. The internal buffer may move during growth.
A Useful Mental Model
A stack is a pile.
push 10
push 20
push 30
pop gives 30A queue is a line.
enqueue 10
enqueue 20
enqueue 30
dequeue gives 10The data structure is mostly about the rule.
The rule determines which item gets processed next.
Summary
A stack is last-in, first-out.
A queue is first-in, first-out.
In Zig, you can build both with std.ArrayList.
For a stack, append to the end and pop from the end.
For a beginner queue, append to the end and remove from the front.
For a better queue, keep a head index.
For a production queue with long-running reuse, use or build a ring buffer.
These structures are small, but they are central to many larger systems.