Queue Data Structure in Python – Complete Tutorial with Examples
A queue is one of the most important data structures in computer science and follows the FIFO (First In, First Out) principle. In this tutorial, you'll learn how queues work in Python, understand operations such as enqueue and dequeue, compare different implementation methods, and explore practical applications including task scheduling, BFS traversal, rate limiting, and more.
Designed for both beginners and developers preparing for technical interviews, this tutorial covers:
- Core Concepts & Architecture: How queues power everything from system schedulers to API rate limiters.
- Implementations: From built-in lists to collections.deque and custom data structures built from scratch.
- Real-World Applications: Step-by-step implementations for real-world scenarios.
- Interview Prep & Patterns: In-depth solutions for classic questions like Circular Queues, Sliding Window Maximum, and BFS Grid Traversals.
Table of Contents
Introduction
1. What Is Queue?
A Queue is a linear data structure that follows FIFO - First In, First Out. The first element added is the first one removed.
Real-World Analogy: A line at a coffee shop. The first person to join the line is the first person served. New customers join at the back, and service happens from the front - nobody gets served out of order (no cutting in line!).
Real systems/software that use queues internally:
- Task/job scheduling: operating systems scheduling processes, print queues.
- Message queues: systems like RabbitMQ, Kafka, AWS SQS for handling async tasks between services.
- Message queues: breadth-first search in trees and graphs.
- Request handling: web servers processing incoming requests in order.
- Buffering: streaming data (video buffering, keyboard input buffering).
- Call center / customer support systems: handling requests in the order they arrive.
2. Core Operations & Complexity
Before you write a single line of code, it helps to know exactly what a queue can do - and how fast each action really is. In this section, we break down the core queue operations in Python: enqueue(), dequeue(), peek(), is_empty(), and size(), along with their time and space complexity.
| Operation | Description | Time Complexity | Space Complexity |
|---|---|---|---|
enqueue(x) |
Add element x to the back |
O(1) |
O(1) |
dequeue() |
Remove and return the front element | O(1)* |
O(1) |
peek() / front() |
View the front element without removing it | O(1) |
O(1) |
is_empty() |
Check if the queue has no elements | O(1) |
O(1) |
size() |
Return count of elements | O(1) |
O(1) |
| Overall structure | - | - | O(n) for n elements |
Important: dequeue() is only O(1) with the right underlying structure (like deque or a linked list). Using a plain Python list and removing from index 0 is O(n) - this is a common beginner mistake, explained in Method 1 below.
3. Implementation
Learn how to implement a queue in Python with three practical methods: using a list (simple but inefficient), collections.deque (fast and recommended), and a custom dictionary-based queue (great for understanding internals). This section covers enqueue, dequeue, peek, and other core operations with clear code examples, time complexity explanations, and beginner-friendly insights. Perfect for Python learners and anyone preparing for DSA interviews who wants to build a solid foundation in queue data structures.
Method A: Using Python's built-in list (works, but inefficient)
Python
class QueueUsingList:
def __init__(self):
self.items = []
def enqueue(self, item):
self.items.append(item) # add to the END - O(1)
def dequeue(self):
if self.is_empty():
raise IndexError("dequeue from empty queue")
# pop(0) removes from the FRONT, but every remaining element
# has to shift left by one - this makes it O(n), not O(1)!
return self.items.pop(0)
def peek(self):
if self.is_empty():
raise IndexError("peek from empty queue")
return self.items[0]
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
q = QueueUsingList()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print(q.dequeue()) # 1
print(q.peek()) # 2
print(q.size()) # 2
Step-by-Step Explanation
1. What problem does it solve?
This builds a Queue - a waiting line where whoever arrived first gets served first. Just like a line at a coffee shop: you join at the back, and the person who's been waiting longest gets served next, from the front.
2. Step-by-step walkthrough
Setting up the line
def __init__(self):
self.items = []
- We start with an empty list.
- This list represents the whole queue - no separate "front" or "back" pointer needed, since Python lists already know their own start and end.
Joining the line - enqueue
def enqueue(self, item):
self.items.append(item)
- New people always join at the back of the line.
append()adds to the end of the list - this is fast, because Python just writes the new item into the next free spot.
Leaving the line - dequeue
def dequeue(self):
if self.is_empty():
raise IndexError("dequeue from empty queue")
return self.items.pop(0)
- First, check if the line is even empty - if it is, there's nobody to remove, so it raises an error rather than pretending it worked.
pop(0)removes whoever is at position 0 - the very front of the line - and hands them back to you.- Important detail: once that front person leaves, everyone behind them has to step forward one spot to close the gap. That's what makes this operation slower than enqueue.
Checking who's next - peek
def peek(self):
if self.is_empty():
raise IndexError("peek from empty queue")
return self.items[0]
- Same empty-check as
dequeue, but this only looks at the front person - it doesn't remove them.
Simple line stats
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
is_empty- is the line empty right now?size- how many people are currently waiting?
3. Why a queue fits this problem
Anything that needs "first come, first served" order - checkout lines, print jobs, customer support tickets - needs exactly this behavior: always add to the back, always remove from the front, so the order people arrived in is respected.
4. Visual diagram
enqueue(1) enqueue(2) enqueue(3)
┌───┐ ┌───┬───┐ ┌───┬───┬───┐
│ 1 │ │ 1 │ 2 │ │ 1 │ 2 │ 3 │
└───┘ └───┴───┘ └───┴───┴───┘
FRONT BACK FRONT BACK FRONT BACK
dequeue() - removes 1 (the front), everyone else shifts forward
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
└───┴───┴───┘
│ (1 leaves, 2 and 3 shift left to fill the gap)
▼
┌───┬───┐
│ 2 │ 3 │
└───┴───┘
FRONT BACK
returned: 1
peek() - just looks at the front, nobody leaves
┌───┬───┐
│ 2 │ 3 │ → returns 2 (queue unchanged: [2, 3])
└───┴───┘
size() - counts how many are waiting
┌───┬───┐
│ 2 │ 3 │ → returns 2
└───┴───┘
5. Trace through the successful run
| Step | Action | What happens | Queue state after | Returned / printed |
|---|---|---|---|---|
| 1 | q.enqueue(1) |
1 joins the back | [1] |
- |
| 2 | q.enqueue(2) |
2 joins the back | [1, 2] |
- |
| 3 | q.enqueue(3) |
3 joins the back | [1, 2, 3] |
- |
| 4 | q.dequeue() |
1 leaves from front; 2 and 3 shift forward | [2, 3] |
1 |
| 5 | q.peek() |
Look at front, nobody removed | [2, 3] (unchanged) |
2 |
| 6 | q.size() |
Count items | [2, 3] (unchanged) |
2 |
6. Trace through the failing case
q2 = QueueUsingList()
q2.dequeue()
- Start: Queue is empty (
[]) - Check:
is_empty()returnsTrue - Result: Stops immediately - raises
IndexError("dequeue from empty queue")
It fails right at the empty-check, before ever trying to touch self.items[0] - this is intentional, so you get a clear error message instead of a confusing crash.
7. Summary
New people join the back of the line and the person who's been waiting longest always leaves from the front - but because leaving from the front means everyone else has to step forward, this "line" gets a little more work every time someone's served, compared to adding a new person.
Why show this if it's inefficient?
It's the most intuitive first version, and understanding why pop(0) is slow is exactly what leads you to Method 2.
Method B: Using collections.deque (the recommended approach)
Python
from collections import deque
# deque (double-ended queue) is implemented internally as a doubly
# linked list of blocks, giving guaranteed O(1) operations at BOTH
# ends - this fixes the O(n) problem from Method 1.
queue = deque()
queue.append(1) # enqueue - add to the back
queue.append(2)
queue.append(3)
print(queue.popleft()) # 1 - dequeue, removes from the FRONT in O(1)
print(queue[0]) # 2 - peek at the front
print(len(queue)) # 2 - size
Step-by-Step Explanation
1. What problem does it solve?
This builds a queue using Python's built-in deque - a waiting line where whoever arrived first gets served first. Same idea as your earlier QueueUsingList, but this time using a tool specially built by Python for this job, instead of a plain list.
2. Step-by-step walkthrough
Importing deque
from collections import deque
deque(pronounced "deck," short for double-ended queue) is a ready-made structure from Python's standard library.- Nobody had to build Node classes or write
.nextpointers by hand - Python already provides this.
Creating the queue
queue = deque()
- Creates an empty deque - an empty line, ready for people to join.
Adding to the queue - enqueue
queue.append(1) # enqueue - add to the back
queue.append(2)
queue.append(3)
append()adds each new item to the back - same idea as enqueue from before.dequeuses the same method name Python lists use for "add to the end," so it feels familiar if you've used lists.
Removing from the queue - dequeue
print(queue.popleft()) # 1 - dequeue, removes from the FRONT
popleft()removes and returns whoever is at the front - this is the "dequeue" operation.- The name literally says what it does: "pop from the left side" (the front).
Peeking at the front
print(queue[0]) # 2 - peek at the front
queue[0]looks at the front item without removing it - same as peek from before, just written using indexing directly instead of a separate method.
Checking size
print(len(queue)) # 2 - size
len()works on a deque exactly like it works on a list - tells you how many items are currently in it.
3. Why deque fits this problem better than a plain list
A deque is built internally so that both ends - front and back - are fast to add to or remove from. Unlike a plain Python list (where removing from the front means shifting every other item over), a deque is designed from the ground up so nothing needs to shift, no matter which end you're working with. That's exactly what a queue needs: fast joins at the back, fast departures from the front.
4. Visual diagram - the successful run
append(1) append(2) append(3)
┌───┐ ┌───┬───┐ ┌───┬───┬───┐
│ 1 │ │ 1 │ 2 │ │ 1 │ 2 │ 3 │
└───┘ └───┴───┘ └───┴───┴───┘
FRONT BACK FRONT BACK FRONT BACK
popleft() - removes 1 from the front, NOTHING else has to shift
┌───┬───┬───┐
│ 1 │ 2 │ 3 │
└───┴───┴───┘
│ (1 leaves - 2 simply becomes the new front, no shifting needed)
▼
┌───┬───┐
│ 2 │ 3 │
└───┴───┘
FRONT BACK
returned: 1
queue[0] - just looks at the front, nobody leaves
┌───┬───┐
│ 2 │ 3 │ → returns 2 (queue unchanged: deque([2, 3]))
└───┴───┘
len(queue) - counts how many are waiting
┌───┬───┐
│ 2 │ 3 │ → returns 2
└───┴───┘
5. Trace table - same successful run
| Step | Action | What happens | Queue state after | Returned / printed |
|---|---|---|---|---|
| 1 | queue.append(1) |
1 joins the back | deque([1]) |
- |
| 2 | queue.append(2) |
2 joins the back | deque([1, 2]) |
- |
| 3 | queue.append(3) |
3 joins the back | deque([1, 2, 3]) |
- |
| 4 | queue.popleft() |
1 leaves from front, no shifting needed | deque([2, 3]) |
1 |
| 5 | queue[0] |
Look at front, nobody removed | deque([2, 3]) (unchanged) |
2 |
| 6 | len(queue) |
Count items | deque([2, 3]) (unchanged) |
2 |
This matches the program's printed output exactly: 1, then 2, then 2 - same result as QueueUsingList, just reached without the shifting cost.
6. Second trace - a failing case: popleft() on an empty deque
empty_queue = deque()
empty_queue.popleft()
| Step | Action | Check | Result |
|---|---|---|---|
| 1 | empty_queue = deque() |
- | deque is deque([]) |
| 2 | empty_queue.popleft() |
deque has nothing in it | stops immediately - raises IndexError("pop from an empty deque") |
Unlike your earlier QueueUsingList, this error message comes built into Python itself - deque already handles the empty-check internally, so you don't have to write is_empty() guards by hand to get a clear failure.
7. Summary
People join the back and leave from the front, just like the earlier list-based queue - except deque is specially built so both ends are equally fast, so nobody else in line has to "shift" when someone leaves from the front.
Method 3: From Scratch Using a Dictionary (Beginner Version)
Python
class DictQueue:
def __init__(self):
self.storage = {} # dictionary: slot_number -> value
self.front = 0 # points to the NEXT item to be removed
self.rear = 0 # points to the NEXT empty slot to fill
def enqueue(self, value):
# Place the value in the next available slot, then move rear forward
self.storage[self.rear] = value
self.rear += 1
def dequeue(self):
if self.is_empty():
print("Queue is empty, nothing to dequeue")
return None
# Grab the value at the front slot
value = self.storage[self.front]
# Remove it from the dictionary so memory doesn't pile up
del self.storage[self.front]
# Move front forward to point at the next item in line
self.front += 1
return value
def peek(self):
if self.is_empty():
print("Queue is empty")
return None
return self.storage[self.front]
def is_empty(self):
# Empty when front has caught up to rear (no items left between them)
return self.front == self.rear
def size(self):
return self.rear - self.front
# Let's try it out
q = DictQueue()
q.enqueue("A") # storage: {0: "A"}
q.enqueue("B") # storage: {0: "A", 1: "B"}
q.enqueue("C") # storage: {0: "A", 1: "B", 2: "C"}
print(q.dequeue()) # A -> slot 0 removed, front moves to 1
print(q.peek()) # B -> next in line
print(q.dequeue()) # B -> slot 1 removed, front moves to 2
print(q.size()) # 1 -> only C left, in slot 2
Step-by-Step Explanation
1. What problem does it solve?
This builds a queue - same FIFO waiting-line idea as before - but using a dictionary with two pointers (front and rear) instead of a list or deque. Think of it like numbered lockers in a hallway: each new person gets the next numbered locker, and whoever's at the lowest numbered occupied locker is served next - nobody physically moves lockers, we just track which numbers are still in use.
2. Step-by-step walkthrough
Constructor - setting up storage and pointers
def __init__(self):
self.storage = {}
self.front = 0
self.rear = 0
storageis a dictionary - each key is a "slot number," each value is the item sitting in that slot.front- the slot number of the next item to be removed.rear- the slot number where the next new item will go.- Both start at 0 because the queue is empty - nobody's been placed yet.
Joining the line - enqueue
def enqueue(self, value):
self.storage[self.rear] = value
self.rear += 1
- Put the new value into slot number
rear. - Then bump
rearup by 1, so the next enqueue goes into the next slot - this is exactly why nothing ever needs to shift: each item gets a permanent slot number for life, and we simply move forward.
Leaving the line - dequeue
def dequeue(self):
if self.is_empty():
print("Queue is empty, nothing to dequeue")
return None
value = self.storage[self.front]
del self.storage[self.front]
self.front += 1
return value
- Check if the queue's empty first - if so, print a message and return
Noneinstead of crashing. - Otherwise: grab whatever's sitting in slot
front- that's the item that's been waiting longest. del self.storage[self.front]- remove that entry from the dictionary entirely, so memory doesn't quietly pile up with old, already-served slots.- Move
frontforward by 1, so it now points at the next item in line. - Return the value we grabbed.
Checking who's next - peek
def peek(self):
if self.is_empty():
print("Queue is empty")
return None
return self.storage[self.front]
- Same empty-check, but this just looks at slot
frontwithout deleting it or moving the pointer.
Checking empty state
def is_empty(self):
return self.front == self.rear
- The queue is empty exactly when
fronthas "caught up" torear- meaning there's no gap left between "next to remove" and "next empty slot," so nothing is waiting in between.
Queue size
def size(self):
return self.rear - self.front
- The number of items currently waiting is just the gap between these two pointers - no need to count anything directly.
3. Why this data structure fits
A dictionary here works like an array of numbered lockers rather than a line of people who have to physically shuffle forward. Because every item gets its own permanent slot number, removing the front item never requires moving anyone else - you just delete that one dictionary entry and nudge the front pointer. This avoids the shifting cost of the plain-list queue, without needing Python's built-in deque.
4. Visual diagram - the successful run
START
front = 0, rear = 0
front/rear
↓
0
[empty]
enqueue("A")
front rear
↓ ↓
0 1
┌─────┐
│ A │
└─────┘
enqueue("B")
front rear
↓ ↓
0 1 2
┌─────┐ ┌─────┐
│ A │ │ B │
└─────┘ └─────┘
enqueue("C")
front rear
↓ ↓
0 1 2 3
┌─────┐ ┌─────┐ ┌─────┐
│ A │ │ B │ │ C │
└─────┘ └─────┘ └─────┘
dequeue() → A
front rear
↓ ↓
0 1 2 3
┌─────┐ ┌─────┐ ┌─────┐
│empty│ │ B │ │ C │
└─────┘ └─────┘ └─────┘
peek() → B
front rear
↓ ↓
0 1 2 3
┌─────┐ ┌─────┐ ┌─────┐
│empty│ │ B │ │ C │
└─────┘ └─────┘ └─────┘
↑
only looking
dequeue() → B
front rear
↓ ↓
0 1 2 3
┌─────┐ ┌─────┐ ┌─────┐
│empty│ │empty│ │ C │
└─────┘ └─────┘ └─────┘
Remaining item: C
size = 3 - 2 = 1
5. Trace table - same successful run
| Step | Action | What happens | storage after | front | rear | Returned / printed |
|---|---|---|---|---|---|---|
| 1 | q.enqueue("A") |
"A" placed in slot 0, rear moves to 1 | {0:"A"} |
0 | 1 | - |
| 2 | q.enqueue("B") |
"B" placed in slot 1, rear moves to 2 | {0:"A", 1:"B"} |
0 | 2 | - |
| 3 | q.enqueue("C") |
"C" placed in slot 2, rear moves to 3 | {0:"A", 1:"B", 2:"C"} |
0 | 3 | - |
| 4 | q.dequeue() |
grab slot 0 ("A"), delete it, front moves to 1 | {1:"B", 2:"C"} |
1 | 3 | A |
| 5 | q.peek() |
look at slot 1 ("B"), nothing removed | {1:"B", 2:"C"} (unchanged) |
1 | 3 | B |
| 6 | q.dequeue() |
grab slot 1 ("B"), delete it, front moves to 2 | {2:"C"} |
2 | 3 | B |
| 7 | q.size() |
rear - front = 3 - 2 | {2:"C"} (unchanged) |
2 | 3 | 1 |
This matches the program's printed output exactly: A, B, B, 1.
6. Second trace - a failing case: dequeue() on an empty queue
empty_q = DictQueue()
empty_q.dequeue()
| Step | Action | Check | Result |
|---|---|---|---|
| 1 | empty_q = DictQueue() |
- | storage={}, front=0, rear=0 |
| 2 | empty_q.dequeue() |
front == rear → True |
prints a message, returns None - no crash, no IndexError |
7. Summary
Every item gets its own permanent numbered slot in a dictionary; enqueue hands out the next slot number and moves rear forward, while dequeue removes whatever's in the front slot and moves front forward - so nothing ever needs to shift, we just track two moving pointers instead.
This is genuinely different from Method 1 (list + shifting problem) and Method 2 (deque) - it shows a third way to think about storage entirely, using key-based lookup instead of position-based lists.
4. Real-World Practical Problems
Problem 1: Print Job / Task Scheduler
Use case:A printer processes jobs in the order they were submitted - first requested, first printed.
Python
from collections import deque
class PrintQueue:
def __init__(self):
self.jobs = deque()
def submit_job(self, document_name):
self.jobs.append(document_name)
print(f"'{document_name}' added to print queue")
def print_next(self):
if not self.jobs:
print("No jobs in queue")
return
job = self.jobs.popleft()
print(f"Printing: {job}")
printer = PrintQueue()
printer.submit_job("Report.pdf")
printer.submit_job("Invoice.docx")
printer.print_next() # Printing: Report.pdf
printer.print_next() # Printing: Invoice.docx
Step-by-Step Explanation
1. What problem does it solve?
It solves the “printer line” problem: multiple people send print jobs, but the printer should handle them first-come, first-served, like people waiting in a queue at a shop counter.
2. Step-by-step walkthrough
Import and class setup
from collections import deque
class PrintQueue:
def __init__(self):
self.jobs = deque()
deque(pronounced “deck”) is a double-ended queue from Python's standard library, optimized for adding/removing from both ends.self.jobsis the internal queue that will hold document names in order.
Using deque is ideal here because we'll append at the right end and remove from the left end efficiently, which matches how a real print queue works.
Submit a job (add to the back)
def submit_job(self, document_name):
self.jobs.append(document_name)
print(f"'{document_name}' added to print queue")
append(document_name)adds the new document to the right end of the queue (the back of the line).- Then it prints a message confirming the job was added.
Each new print request joins the end of the line, behind any existing jobs.
Print the next job (remove from the front)
def print_next(self):
if not self.jobs:
print("No jobs in queue")
return
job = self.jobs.popleft()
print(f"Printing: {job}")
if not self.jobs:checks whether the queue is empty.- If it's empty, it prints “No jobs in queue” and stops.
- Otherwise,
popleft()removes and returns the leftmost item, which is the oldest job (the one that's been waiting the longest). - It then prints which document is being printed.
This ensures jobs are printed in the exact order they were submitted.
Using the queue
printer = PrintQueue()
printer.submit_job("Report.pdf")
printer.submit_job("Invoice.docx")
printer.print_next() # Printing: Report.pdf
printer.print_next() # Printing: Invoice.docx
- Create a
PrintQueueobject calledprinter. - Submit
"Report.pdf"→ queue:["Report.pdf"]. - Submit
"Invoice.docx"→ queue:["Report.pdf", "Invoice.docx"]. - First
print_next()removes and prints"Report.pdf"(the first submitted). - Second
print_next()removes and prints"Invoice.docx"(the next in line).
3. Why a queue (and deque) fits this problem
A queue is exactly right for “first-in, first-out” scenarios like print jobs, task schedulers, or message inboxes. deque is used instead of a plain list because removing from the front (popleft) is fast and efficient, while a list's pop(0) would be slower for many items.
4. Visual diagram - successful example
submit_job("Report.pdf") submit_job("Invoice.docx")
┌─────────────┐ ┌─────────────┬──────────────┐
│ Report.pdf │ │ Report.pdf │ Invoice.docx │
└─────────────┘ └─────────────┴──────────────┘
FRONT/BACK FRONT BACK
print_next() → removes Report.pdf from the front
┌─────────────┬──────────────┐
│ Report.pdf │ Invoice.docx │
└─────────────┴──────────────┘
│
▼ (Report.pdf leaves, Invoice.docx becomes new front)
┌──────────────┐
│ Invoice.docx │
└──────────────┘
FRONT/BACK
Printed: Report.pdf
print_next() → removes Invoice.docx, queue now empty
┌──────────────┐
│ Invoice.docx │
└──────────────┘
│
▼
┌ ─ ─ ─ ─ ┐
│ (empty) │
└ ─ ─ ─ ─ ┘
Printed: Invoice.docx
5. Trace table - successful run
| Step | Operation | Action taken | Resulting state (jobs) |
|---|---|---|---|
| 1 | printer = PrintQueue() |
Create empty queue | deque([]) |
| 2 | printer.submit_job("Report.pdf") |
Append "Report.pdf" to right end |
deque(["Report.pdf"]) |
| 3 | printer.submit_job("Invoice.docx") |
Append "Invoice.docx" to right end |
deque(["Report.pdf", "Invoice.docx"]) |
| 4 | printer.print_next() |
Queue not empty; popleft() → "Report.pdf", print it |
deque(["Invoice.docx"]) |
| 5 | printer.print_next() |
Queue not empty; popleft() → "Invoice.docx", print it |
deque([]) |
6. Trace for a failing example (printing when empty)
Now try to print when there are no jobs.
Setup & Operation
printer = PrintQueue()
# jobs: deque([])
printer.print_next()
| Step | Operation | Action taken | Resulting state / note |
|---|---|---|---|
| 1 | printer = PrintQueue() |
Create empty queue | deque([]) |
| 2 | printer.print_next() |
if not self.jobs is True → print "No jobs in queue" and return |
State unchanged; operation fails because queue is empty |
It fails at step 2 because the queue is empty, so there's no job to print.
7. Summary
This code builds a first-come, first-served print queue using deque, so documents are added at the back and printed from the front in the exact order they were submitted.
Problem 2: Customer Support Ticket System
Use case: Support tickets get handled in the order customers submitted them, ensuring fairness (first come, first served).
Python
from collections import deque
class SupportQueue:
def __init__(self):
self.tickets = deque()
def new_ticket(self, customer_name, issue):
self.tickets.append({"customer": customer_name, "issue": issue})
def handle_next_ticket(self):
if not self.tickets:
print("No tickets waiting")
return
ticket = self.tickets.popleft()
print(f"Helping {ticket['customer']} with: {ticket['issue']}")
support = SupportQueue()
support.new_ticket("Alice", "Can't reset password")
support.new_ticket("Bob", "App keeps crashing")
support.handle_next_ticket() # Helping Alice with: Can't reset password
support.handle_next_ticket() # Helping Bob with: App keeps crashing
Step-by-Step Explanation
1. What problem does it solve?
It solves the “support line” problem: many customers report issues, but the team should help them first-come, first-served, like people waiting in a single line at a help desk.
2. Step-by-step walkthrough
Import and class setup
from collections import deque
class SupportQueue:
def __init__(self):
self.tickets = deque()
deque(pronounced “deck”) is a double-ended queue from Python's standard library, optimized for adding/removing from both ends.self.ticketsis the internal queue that will hold ticket information in order.
Using deque is ideal here because we'll append at the right end and remove from the left end efficiently, which matches how a real support queue works.
Create a new ticket (add to the back)
def new_ticket(self, customer_name, issue):
self.tickets.append({"customer": customer_name, "issue": issue})
- This method takes two inputs: the customer's name and a short description of their issue.
- It creates a small dictionary (a key–value pair) like
{"customer": "Alice", "issue": "Can't reset password"}. append(...)adds this dictionary to the right end of the queue (the back of the line).
Each new support request joins the end of the line, behind any existing tickets.
Handle the next ticket (remove from the front)
def handle_next_ticket(self):
if not self.tickets:
print("No tickets waiting")
return
ticket = self.tickets.popleft()
print(f"Helping {ticket['customer']} with: {ticket['issue']}")
if not self.tickets:checks whether the queue is empty.- If it's empty, it prints “No tickets waiting” and stops.
- Otherwise,
popleft()removes and returns the leftmost item, which is the oldest ticket (the one that's been waiting the longest). - It then prints which customer is being helped and what their issue is, using the dictionary's
"customer"and"issue"keys.
This ensures tickets are handled in the exact order they were created.
Using the queue
support = SupportQueue()
support.new_ticket("Alice", "Can't reset password")
support.new_ticket("Bob", "App keeps crashing")
support.handle_next_ticket() # Helping Alice with: Can't reset password
support.handle_next_ticket() # Helping Bob with: App keeps crashing
- Create a
SupportQueueobject calledsupport. - Add Alice's ticket → queue:
[{"customer": "Alice", "issue": "Can't reset password"}]. - Add Bob's ticket → queue:
[Alice-ticket, {"customer": "Bob", "issue": "App keeps crashing"}]. - First
handle_next_ticket()removes and handles Alice's ticket (the first submitted). - Second
handle_next_ticket()removes and handles Bob's ticket (the next in line).
3. Why a queue (and deque) fits this problem
A queue is exactly right for “first-in, first-out” scenarios like support tickets, print jobs, or task schedulers. deque is used instead of a plain list because removing from the front (popleft) is fast and efficient, while a list's pop(0) would be slower for many items.
4. Visual diagram - successful example
new_ticket("Alice", "Can't reset password")
┌──────────────────────────────┐
│ {Alice: Can't reset password}│
└──────────────────────────────┘
FRONT/BACK
new_ticket("Bob", "App keeps crashing")
┌──────────────────────────────┬────────────────────────────┐
│ {Alice: Can't reset password}│ {Bob: App keeps crashing} │
└──────────────────────────────┴────────────────────────────┘
FRONT BACK
handle_next_ticket() → removes Alice's ticket from the front
┌──────────────────────────────┬────────────────────────────┐
│ {Alice: Can't reset password}│ {Bob: App keeps crashing} │
└──────────────────────────────┴────────────────────────────┘
│
▼ (Alice's ticket leaves, Bob's becomes new front)
┌────────────────────────────┐
│ {Bob: App keeps crashing} │
└────────────────────────────┘
FRONT/BACK
Helping Alice with: Can't reset password
handle_next_ticket() → removes Bob's ticket, queue now empty
┌────────────────────────────┐
│ {Bob: App keeps crashing} │
└────────────────────────────┘
│
▼
┌ ─ ─ ─ ─ ┐
│ (empty) │
└ ─ ─ ─ ─ ┘
Helping Bob with: App keeps crashing
5. Trace table - successful run
| Step | Operation | Action taken | Resulting state (tickets) |
|---|---|---|---|
| 1 | support = SupportQueue() |
Create empty queue | deque([]) |
| 2 | new_ticket("Alice", "Can't reset password") |
Append Alice's ticket dict to right end | deque([{"customer":"Alice","issue":"Can't reset password"}]) |
| 3 | new_ticket("Bob", "App keeps crashing") |
Append Bob's ticket dict to right end | deque([Alice-ticket, {"customer":"Bob","issue":"App keeps crashing"}]) |
| 4 | handle_next_ticket() |
Queue not empty; popleft() → Alice-ticket, print “Helping Alice …” |
deque([{"customer":"Bob","issue":"App keeps crashing"}]) |
| 5 | handle_next_ticket() |
Queue not empty; popleft() → Bob-ticket, print “Helping Bob …” |
deque([]) |
6. Trace for a failing example (handling when empty)
Now try to handle a ticket when there are none.
Setup & Operation
support = SupportQueue()
# tickets: deque([])
support.handle_next_ticket()
| Step | Operation | Action taken | Resulting state / note |
|---|---|---|---|
| 1 | support = SupportQueue() |
Create empty queue | deque([]) |
| 2 | handle_next_ticket() |
if not self.tickets is True → print “No tickets waiting” and return |
State unchanged; operation fails because queue is empty |
It fails at step 2 because the queue is empty, so there's no ticket to handle.
7. Summary
Every new ticket joins the back of the line as a small bundle of customer info, and the support agent always works through tickets in the order customers reached out, helping one at a time from the front until the queue is empty.
Problem 3: Breadth-First Search (BFS) - Finding Shortest Path
Use case: Social networks use BFS to find "degrees of connection" (e.g., LinkedIn's "2nd/3rd connection"), and GPS systems use it for shortest routes in unweighted graphs.
Python
from collections import deque
def bfs_shortest_path(graph, start, target):
# graph is a dict like {"A": ["B", "C"], "B": ["D"], ...}
visited = {start}
queue = deque([(start, [start])]) # (current_node, path_so_far)
while queue:
current, path = queue.popleft() # process the OLDEST discovered node first
if current == target:
return path
for neighbor in graph.get(current, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None # no path found
social_network = {
"Alice": ["Bob", "Carol"],
"Bob": ["Alice", "Dave"],
"Carol": ["Alice", "Eve"],
"Dave": ["Bob", "Eve"],
"Eve": ["Carol", "Dave"]
}
print(bfs_shortest_path(social_network, "Alice", "Eve"))
# ['Alice', 'Carol', 'Eve'] - shortest connection path
Step-by-Step Explanation
1. What problem does it solve?
It solves the “shortest friendship chain” problem: given who knows whom, it finds the smallest number of introductions needed to get from one person to another, and returns that chain of people.
2. Step-by-step walkthrough
Function and inputs
def bfs_shortest_path(graph, start, target):
# graph is a dict like {"A": ["B", "C"], "B": ["D"], ...}
graphis a dictionary where each key is a person, and the value is a list of their direct friends (neighbors).startis the person we begin from.targetis the person we want to reach.
The function will return the shortest list of people from start to target, or None if no path exists.
Track visited people and the queue
visited = {start}
queue = deque([(start, [start])]) # (current_node, path_so_far)
visitedis a set that remembers which people we've already seen, so we don't loop forever.queueis a double-ended queue that stores pairs:(current_node, path_so_far).- We start with
(start, [start]), meaning “we're atstart, and the path so far is just[start]”.
Using a queue ensures we explore people in the order we discovered them, which is the key idea behind Breadth-First Search (BFS).
Main loop: process the oldest discovered node first
while queue:
current, path = queue.popleft() # process the OLDEST discovered node first
while queue:means “keep going as long as there are people left to explore”.queue.popleft()removes and returns the oldest entry in the queue (first-in, first-out).currentis the person we're looking at now;pathis the list of people we took to get here.
Processing the oldest discovered node first guarantees we explore layer-by-layer: all friends, then friends-of-friends, and so on.
Check if we reached the target
if current == target:
return path
- If the person we're currently at is the
target, we've found a path. - Because BFS explores in layers, the first time we reach the target, it's guaranteed to be via the shortest path (fewest steps).
- We immediately return
path, which is the list of people fromstarttotarget.
Explore neighbors (friends) of the current person
for neighbor in graph.get(current, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
graph.get(current, [])fetches the list of friends ofcurrent; ifcurrentisn't in the graph, it returns an empty list.- For each
neighbor(friend):- If we haven't visited them yet (
neighbor not in visited), we mark them as visited withvisited.add(neighbor). - Add them to the queue with the updated path:
path + [neighbor].
- If we haven't visited them yet (
This builds longer and longer paths as we move outward, but only keeps each person once, avoiding cycles and repeated work.
No path found
return None # no path found
- If the queue becomes empty and we never hit the
target, it means there's no way to reachtargetfromstartin this network. - In that case, we return
None.
Example social network and call
social_network = {
"Alice": ["Bob", "Carol"],
"Bob": ["Alice", "Dave"],
"Carol": ["Alice", "Eve"],
"Dave": ["Bob", "Eve"],
"Eve": ["Carol", "Dave"]
}
print(bfs_shortest_path(social_network, "Alice", "Eve"))
# ['Alice', 'Carol', 'Eve']
- The graph says: Alice knows Bob and Carol; Bob knows Alice and Dave; Carol knows Alice and Eve; Dave knows Bob and Eve; Eve knows Carol and Dave.
- We ask for the shortest path from
"Alice"to"Eve". - BFS first sees Alice's friends: Bob and Carol.
- From Carol, it immediately sees Eve, so the path
['Alice', 'Carol', 'Eve']is found and returned.
3. Why a queue (and BFS) fits this problem
A queue gives us first-in, first-out ordering, which makes BFS explore nodes level-by-level: all people at distance 1, then distance 2, and so on. This layer-by-layer search is exactly what guarantees the first path we find to the target is the shortest in terms of number of steps.
4. Visual diagram - successful example (Alice → Eve)
social_network:
Alice ── Bob ── Dave
│ │
Carol ───────── Eve
Start: Alice, Target: Eve
Step 1: queue = [(Alice, [Alice])]
visited = {Alice}
pop Alice → not target
friends: Bob, Carol (neither visited)
┌─────────────────────────────────────┐
│ queue: [(Bob,[A,B]), (Carol,[A,C])] │
└─────────────────────────────────────┘
visited = {Alice, Bob, Carol}
Step 2: pop Bob → not target
friends: Alice (visited, skip), Dave (new)
┌───────────────────────────────────────────────────┐
│ queue: [(Carol,[A,C]), (Dave,[A,B,D])] │
└───────────────────────────────────────────────────┘
visited = {Alice, Bob, Carol, Dave}
Step 3: pop Carol → not target
friends: Alice (visited, skip), Eve (new!)
┌───────────────────────────────────────────────────────────┐
│ queue: [(Dave,[A,B,D]), (Eve,[A,C,E])] │
└───────────────────────────────────────────────────────────┘
visited = {Alice, Bob, Carol, Dave, Eve}
Step 4: pop Dave → not target (already found a shorter path to Eve, but
Dave was queued before Eve, so we still process him first)
friends: Bob (visited), Eve (already visited, skip)
┌───────────────────────────────┐
│ queue: [(Eve,[A,C,E])] │
└───────────────────────────────┘
Step 5: pop Eve → MATCH! target found
return ['Alice', 'Carol', 'Eve']
5. Trace table - successful run (Alice → Eve)
| Step | Operation | Action taken | Resulting state (visited, queue) |
|---|---|---|---|
| 1 | Initialize | visited = {Alice}, queue = [(Alice, [Alice])] |
visited={Alice}, queue=[(Alice,[Alice])] |
| 2 | Pop (Alice, [Alice]) | Not target; explore Bob, Carol | visited={Alice,Bob,Carol}, queue=[(Bob,[A,B]), (Carol,[A,C])] |
| 3 | Pop (Bob, [Alice, Bob]) | Not target; explore Alice (skip), Dave | visited={A,B,C,D}, queue=[(Carol,[A,C]), (Dave,[A,B,D])] |
| 4 | Pop (Carol, [Alice, Carol]) | Not target; explore Alice (skip), Eve → enqueue | visited={A,B,C,D,E}, queue=[(Dave,[A,B,D]), (Eve,[A,C,E])] |
| 5 | Pop (Dave, [Alice, Bob, Dave]) | Not target; neighbors Bob, Eve already visited → no new enqueues | visited={A,B,C,D,E}, queue=[(Eve,[A,C,E])] |
| 6 | Pop (Eve, [Alice, Carol, Eve]) | Eve == target → return path | Returns ['Alice', 'Carol', 'Eve'] |
6. Trace for a failing example (no path exists)
Imagine a disconnected network where Eve is in a separate group:
social_network = {
"Alice": ["Bob"],
"Bob": ["Alice"],
"Carol": ["Dave"],
"Dave": ["Carol"],
"Eve": []
}
bfs_shortest_path(social_network, "Alice", "Eve")
| Step | Operation | Action taken | Resulting state / note |
|---|---|---|---|
| 1 | Initialize | visited = {Alice}, queue = [(Alice, [Alice])] |
visited={Alice}, queue=[(Alice,[Alice])] |
| 2 | Pop (Alice, [Alice]) | Not target; neighbors: Bob → enqueue | visited={Alice,Bob}, queue=[(Bob,[A,B])] |
| 3 | Pop (Bob, [Alice, Bob]) | Not target; neighbors: Alice (already visited) → nothing new | visited={Alice,Bob}, queue=[] |
| 4 | Queue empty | Loop ends; return None |
No path found; function returns None |
It fails to find a path because Eve is in a separate component with no connections to Alice or Bob, so the queue empties before reaching her.
7. Summary
Starting from one person, BFS explores everyone one hop away first, then two hops away, and so on - using a queue to always process the oldest discovery first - which guarantees the very first time the target is found, it's found by the shortest possible path.
Problem 4: Rate Limiter (Sliding Window)
Use case: APIs limit how many requests a user can make in a given time window (e.g., max 5 requests per 10 seconds) - a queue tracks recent request timestamps.
Python
from collections import deque
import time
class RateLimiter:
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.timestamps = deque()
def allow_request(self):
now = time.time()
# Remove timestamps that have fallen outside the time window
while self.timestamps and now - self.timestamps[0] > self.window_seconds:
self.timestamps.popleft()
if len(self.timestamps) < self.max_requests:
self.timestamps.append(now)
return True # request allowed
return False # rate limit exceeded
limiter = RateLimiter(max_requests=3, window_seconds=10)
print(limiter.allow_request()) # True
print(limiter.allow_request()) # True
print(limiter.allow_request()) # True
print(limiter.allow_request()) # False - limit hit within the window
Step-by-Step Explanation
1. What problem does it solve?
It solves the too many requests too fast problem: it ensures that no more than a set number of requests are allowed within a rolling time period, protecting servers from overload or abuse.
2. Step-by-step walkthrough
Imports and class setup
from collections import deque
import time
class RateLimiter:
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.timestamps = deque()
dequeis a double-ended queue optimized for adding/removing from both ends.time.time()gives the current time as a number (seconds since a fixed point).max_requestsis the maximum number of allowed requests in the time window.window_secondsis how long the time window lasts (e.g., 10 seconds).self.timestampswill store the times (timestamps) of recent allowed requests, in order.
We use a queue of timestamps so we can efficiently drop old ones and count only the recent requests.
Check if a new request is allowed
def allow_request(self):
now = time.time()
- When someone tries to make a request, we first get the current time as
now.
This timestamp will be used to decide if older requests are still “inside” the time window.
Remove old timestamps outside the window
while self.timestamps and now - self.timestamps[0] > self.window_seconds:
self.timestamps.popleft()
self.timestamps[0]is the oldest stored timestamp (the front of the queue).now - self.timestamps[0]is how many seconds ago that oldest request happened.- If that difference is greater than
window_seconds, the request is too old and should no longer count. - The
whileloop keeps removing the oldest timestamps until all remaining ones are within the lastwindow_seconds.
This keeps the queue “sliding”: it always represents requests in the most recent time window.
Decide whether to allow or block the request
if len(self.timestamps) < self.max_requests:
self.timestamps.append(now)
return True # request allowed
return False # rate limit exceeded
len(self.timestamps)is how many requests are currently inside the time window.- If that number is less than
max_requests, we add the current timenowto the queue as a new allowed request and returnTrue. - Otherwise, we've already used up all allowed requests in this window, so we return
Falseto block it.
This enforces the rule: “at most max_requests in any rolling window_seconds period.”
Example usage
limiter = RateLimiter(max_requests=3, window_seconds=10)
print(limiter.allow_request()) # True
print(limiter.allow_request()) # True
print(limiter.allow_request()) # True
print(limiter.allow_request()) # False - limit hit within the window
- We create a limiter that allows 3 requests per 10 seconds.
- First call: queue is empty → allow, store timestamp →
True. - Second call: 1 request in window → allow, store timestamp →
True. - Third call: 2 requests in window → allow, store timestamp →
True. - Fourth call: now 3 requests are already in the 10-second window → cannot add more →
False.
If you wait until the oldest timestamp becomes older than 10 seconds, it will be removed and a new request can be allowed again.
3. Why a queue of timestamps fits this problem
A queue naturally stores events in the order they happened, which is exactly what we need to track request times. By removing old timestamps from the front and adding new ones at the back, we maintain a sliding window of recent requests and can enforce the rate limit efficiently.
4. Visual diagram - 4 rapid calls (limit = 3 in 10s)
Assume all four calls happen almost instantly, so now is roughly the same for each.
Setup: max_requests=3, window_seconds=10
timestamps: (empty)
allow_request() #1 → time = 100.0
cleanup: nothing to remove (queue empty)
len(timestamps) = 0 < 3 → ALLOWED
┌───────┐
│ 100.0 │
└───────┘
return: True
allow_request() #2 → time = 100.1
cleanup: 100.0 is still within 10s window → keep it
len(timestamps) = 1 < 3 → ALLOWED
┌───────┬───────┐
│ 100.0 │ 100.1 │
└───────┴───────┘
return: True
allow_request() #3 → time = 100.2
cleanup: both still within window → keep them
len(timestamps) = 2 < 3 → ALLOWED
┌───────┬───────┬───────┐
│ 100.0 │ 100.1 │ 100.2 │
└───────┴───────┴───────┘
return: True
allow_request() #4 → time = 100.3
cleanup: all 3 still within window → keep them
len(timestamps) = 3, NOT < 3 → BLOCKED
┌───────┬───────┬───────┐
│ 100.0 │ 100.1 │ 100.2 │ ← nothing added, timestamp NOT recorded
└───────┴───────┴───────┘
return: False
If enough time passes (more than 10 seconds after t1), then t1 will be removed, and a new request can be allowed again.
5. Trace table - successful run (4 rapid calls)
| Step | Operation | Action taken | Resulting state (timestamps) and return value |
|---|---|---|---|
| 1 | Create limiter | max_requests=3, window_seconds=10, timestamps=[] |
deque([]) |
| 2 | allow_request() #1 |
No old entries; len=0 < 3 → append t1, return True |
deque([t1]), returns True |
| 3 | allow_request() #2 |
No old entries; len=1 < 3 → append t2, return True |
deque([t1, t2]), returns True |
| 4 | allow_request() #3 |
No old entries; len=2 < 3 → append t3, return True |
deque([t1, t2, t3]), returns True |
| 5 | allow_request() #4 |
No old entries; len=3 not < 3 → do not append, return False |
deque([t1, t2, t3]), returns False |
6. Trace for a failing example (request blocked, then allowed after window)
Assume: t1, t2, t3 are at time 0, 1, 2 seconds. Next call happens at time 15 seconds.
| Step | Time (s) | Operation | Action taken | Resulting state / note |
|---|---|---|---|---|
| 1 | 0 | allow_request() #1 |
Append t1=0 → True |
deque([0]) |
| 2 | 1 | allow_request() #2 |
Append t2=1 → True |
deque([0,1]) |
| 3 | 2 | allow_request() #3 |
Append t3=2 → True |
deque([0,1,2]) |
| 4 | 3 | allow_request() #4 |
now=3; 3 - 0 = 3 ≤ 10 → no removal; len=3 → False (blocked) |
deque([0,1,2]), returns False |
| 5 | 15 | allow_request() #5 |
now=15; remove 0 (15-0>10), then 1 (15-1>10), then 2 (15-2>10) |
deque([]) after removals |
| 6 | 15 | (same call continued) | Now len=0 < 3 → append 15, return True |
deque([15]), returns True (allowed after window expired) |
It fails at step 4 because all three timestamps are still within the 10-second window, so the limit is hit; it succeeds at step 6 because enough time has passed that all old timestamps are removed, freeing space for a new request.
7. Summary
Every allowed request's timestamp gets remembered in a queue; before allowing a new one, old timestamps that have "expired" past the time window are cleared from the front, and a new request is only let through if there's still room left under the limit.
5. Interview Questions
These come up frequently in coding interviews. Difficulty increases as you go down the list.
Q1 (Easy): Moving Average from a Data Stream
Problem: Given a stream of integers arriving one at a time and a window size, return the moving average of the last k values seen so far.
Key idea: A queue is a natural fit for "the last k things that happened" - enqueue each new value, and once the queue exceeds size k, dequeue the oldest one so the window stays fixed.
Python
from collections import deque
class MovingAverage:
def __init__(self, size):
self.size = size
self.window = deque()
self.window_sum = 0 # track the running sum so we don't re-add every time
def next(self, value):
self.window.append(value)
self.window_sum += value
# Once the window is too big, drop the oldest value
if len(self.window) > self.size:
oldest = self.window.popleft()
self.window_sum -= oldest
return self.window_sum / len(self.window)
ma = MovingAverage(3)
print(ma.next(1)) # 1.0 -> average of [1]
print(ma.next(10)) # 5.5 -> average of [1, 10]
print(ma.next(3)) # 4.67 -> average of [1, 10, 3]
print(ma.next(5)) # 6.0 -> average of [10, 3, 5], 1 has been dropped
Step-by-Step Explanation
1. What problem does it solve?
It solves the recent average problem: instead of averaging all history, it only considers the most recent values (like the last 3 sensor readings or stock prices) so the average reflects current behavior.
2. Step-by-step walkthrough
Imports and class setup
from collections import deque
class MovingAverage:
def __init__(self, size):
self.size = size
self.window = deque()
self.window_sum = 0 # track the running sum so we don't re-add every time
dequeis a double-ended queue optimized for adding/removing from both ends.sizeis how many recent numbers we want to include in the average (the window size).self.windowwill store the recent numbers in order.self.window_sumkeeps a running total of the numbers currently in the window.
Tracking the sum separately avoids re-adding all numbers every time, making each update fast.
Add a new number and update the sum
def next(self, value):
self.window.append(value)
self.window_sum += value
- When a new number (
value) arrives, we add it to the right end of the queue. - We also add it to
window_sumso the sum always matches the numbers currently in the window.
At this point, the window might be temporarily larger than size, which we fix next.
Drop the oldest number if the window is too big
if len(self.window) > self.size:
oldest = self.window.popleft()
self.window_sum -= oldest
len(self.window)is how many numbers are currently stored.- If this count exceeds
self.size, we remove the oldest number from the left end usingpopleft(). - We subtract that oldest number from
window_sumso the sum stays consistent with the remaining numbers.
This keeps exactly the last size numbers in the window, sliding forward each time a new value arrives.
Compute and return the current average
return self.window_sum / len(self.window)
- The average is simply the current sum divided by the current count of numbers in the window.
- Because we maintain
window_sumand the window contents, this calculation is quick and always up to date.
Example usage
ma = MovingAverage(3)
print(ma.next(1)) # 1.0 -> average of [1]
print(ma.next(10)) # 5.5 -> average of [1, 10]
print(ma.next(3)) # 4.67 -> average of [1, 10, 3]
print(ma.next(5)) # 6.0 -> average of [10, 3, 5], 1 has been dropped
- We create a moving average that considers the last 3 numbers.
- First call with
1: window =[1]→ average =1 / 1 = 1.0. - Second call with
10: window =[1, 10]→ average =(1+10) / 2 = 5.5. - Third call with
3: window =[1, 10, 3]→ average =(1+10+3) / 3 ≈ 4.67. - Fourth call with
5:- Add
5→ window becomes[1, 10, 3, 5](temporarily size 4). - Since size > 3, remove oldest
1→ window =[10, 3, 5], sum updated accordingly. - Average =
(10+3+5) / 3 = 18 / 3 = 6.0.
- Add
3. Why a queue of numbers fits this problem
A queue naturally stores values in the order they arrived, which is exactly what we need for a sliding window of “most recent N”. By removing from the front when the window is too big and adding to the back for new values, we maintain a correct, up-to-date window with minimal work.
4. Visual diagram - values 1, 10, 3, 5 with size=3
MovingAverage(3) → window can hold at most 3 numbers
next(1)
window: [1] sum = 1
average = 1 / 1 = 1.0
next(10)
window: [1, 10] sum = 11
average = 11 / 2 = 5.5
next(3)
window: [1, 10, 3] sum = 14
average = 14 / 3 = 4.666... ≈ 4.67
next(5) window is now size 4 - TOO BIG (> 3)
window: [1, 10, 3, 5] sum = 19
│
▼ popleft() removes 1, sum -= 1
window: [10, 3, 5] sum = 18
average = 18 / 3 = 6.0
5. Trace table - successful run (1, 10, 3, 5 with size=3)
| Step | Operation | Action taken | Resulting state (window, window_sum) and returned average |
|---|---|---|---|
| 1 | Create MA(3) | size=3, window=[], window_sum=0 |
deque([]), sum=0 |
| 2 | next(1) |
Append 1 → sum=1; len=1 ≤ 3 → no removal; avg=1/1 | deque([1]), sum=1, returns 1.0 |
| 3 | next(10) |
Append 10 → sum=11; len=2 ≤ 3 → no removal; avg=11/2 | deque([1, 10]), sum=11, returns 5.5 |
| 4 | next(3) |
Append 3 → sum=14; len=3 ≤ 3 → no removal; avg=14/3 | deque([1, 10, 3]), sum=14, returns 4.666... |
| 5 | next(5) |
Append 5 → sum=19; len=4>3 → pop 1, sum=18; avg=18/3 | deque([10, 3, 5]), sum=18, returns 6.0 |
6. Trace for an edge-style example (partial window: only 2 values)
Assume size = 3 and we only call next twice.
| Step | Operation | Action taken | Resulting state (window, window_sum) and returned average |
|---|---|---|---|
| 1 | Create MA(3) | size=3, window=[], window_sum=0 |
deque([]), sum=0 |
| 2 | next(4) |
Append 4 → sum=4; len=1 ≤ 3 → no removal; avg=4/1 | deque([4]), sum=4, returns 4.0 |
| 3 | next(8) |
Append 8 → sum=12; len=2 ≤ 3 → no removal; avg=12/2 | deque([4, 8]), sum=12, returns 6.0 |
Here, the window is not yet full (only 2 values instead of 3), so no values are dropped; the average is over however many values exist so far.
7. Summary
Each new number joins the back of a fixed-size window and gets added to a running total; once the window grows past its allowed size, the oldest number is dropped from the front and subtracted from the total, so the average always reflects only the most recent values.
Q2 (Medium): Implement a Queue Using Two Stacks
Problem: You're only given stack operations (push/pop, which are LIFO) - implement FIFO queue behavior with them.
Key insight: Reversing order twice restores the original order. Use one stack for incoming items, and only when you need to dequeue, dump everything into a second stack - this naturally reverses it into FIFO order.
Python
class QueueUsingStacks:
def __init__(self):
self.in_stack = [] # holds newly enqueued items
self.out_stack = [] # holds items ready to be dequeued, in FIFO order
def enqueue(self, x):
self.in_stack.append(x)
def dequeue(self):
# Only refill out_stack when it's empty - this keeps the
# average cost per operation O(1) even though a single
# refill can take O(n).
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
if not self.out_stack:
raise IndexError("dequeue from empty queue")
return self.out_stack.pop()
q = QueueUsingStacks()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print(q.dequeue()) # 1 - correct FIFO order despite using stacks
Step-by-Step Explanation
1. What problem does it solve?
It solves the “queue with stacks” problem: stacks naturally give last-in, first-out behavior, but by using two stacks together, we can mimic a real queue where the first item added is the first item removed.
2. Step-by-step walkthrough
Class and its fields
class QueueUsingStacks:
def __init__(self):
self.in_stack = [] # holds newly enqueued items
self.out_stack = [] # holds items ready to be dequeued, in FIFO order
self.in_stackis a list used as a stack for new items being enqueued.self.out_stackis another stack that holds items in the correct order for dequeuing.
The key idea: pushing to in_stack is easy; when we need to dequeue, we reverse in_stack into out_stack so the oldest item ends up on top.
Enqueue (add to the back)
def enqueue(self, x):
self.in_stack.append(x)
- To add an item
x, we simply push it ontoin_stack.
All new items pile up in in_stack in the order they arrive, with the newest on top.
Dequeue (remove from the front)
def dequeue(self):
# Only refill out_stack when it's empty
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
if not self.out_stack:
raise IndexError("dequeue from empty queue")
return self.out_stack.pop()
- First, check if
out_stackis empty. - If it is, we “refill” it by moving all items from
in_stacktoout_stack:self.in_stack.pop()removes the top (most recent) item fromin_stack.self.out_stack.append(...)pushes that item ontoout_stack.- Repeating this reverses the order: the oldest item from
in_stackbecomes the top ofout_stack.
- If
out_stackis still empty after trying to refill, we raise an error for "dequeue from empty queue". - Otherwise, we pop from
out_stack, returning the oldest enqueued item.
We only refill out_stack when it's empty, which keeps the average cost per operation low even though a single refill can take time proportional to the number of items.
Example usage
q = QueueUsingStacks()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print(q.dequeue()) # 1 - correct FIFO order despite using stacks
- Create a
QueueUsingStacksobject. - Enqueue
1→in_stack = [1]. - Enqueue
2→in_stack = [1, 2](2 on top). - Enqueue
3→in_stack = [1, 2, 3](3 on top). - Dequeue:
out_stackis empty, so we move all items fromin_stacktoout_stack:- Pop 3 →
out_stack = [3] - Pop 2 →
out_stack = [3, 2] - Pop 1 →
out_stack = [3, 2, 1](1 on top)
- Pop 3 →
- Now pop from
out_stack→ returns1, the first enqueued item.
This shows that even though we used stacks internally, the external behavior is a proper queue.
3. Why two stacks fit this problem
A single stack gives last-in, first-out order, which is the opposite of a queue's first-in, first-out order. By using one stack to collect new items and a second stack to reverse them when needed, we can produce the correct FIFO order while still only using stack operations.
4. Visual diagram - successful example (enqueue 1,2,3 then dequeue)
We'll track in_stack (top on the right) and out_stack (top on the right).
enqueue(1) enqueue(2) enqueue(3)
in_stack: [1] in_stack: [1, 2] in_stack: [1, 2, 3]
out_stack: [] out_stack: [] out_stack: []
▲
(3 is on top - most recent)
dequeue() called
out_stack is EMPTY → refill it by popping everything off in_stack
in_stack: [1, 2, 3]
│ pop 3 → push to out_stack
▼
in_stack: [1, 2] out_stack: [3]
│ pop 2 → push to out_stack
▼
in_stack: [1] out_stack: [3, 2]
│ pop 1 → push to out_stack
▼
in_stack: [] out_stack: [3, 2, 1]
▲
(1 is now on TOP - the oldest item!)
pop from out_stack → removes 1
in_stack: [] out_stack: [3, 2]
returned: 1 correct FIFO order
5. Trace table - successful run (enqueue 1,2,3 then dequeue)
| Step | Operation | Action taken | Resulting state (in_stack, out_stack) and return value |
|---|---|---|---|
| 1 | Create queue | in_stack=[], out_stack=[] |
([], []) |
| 2 | enqueue(1) |
Append 1 to in_stack |
in=[1], out=[] |
| 3 | enqueue(2) |
Append 2 to in_stack |
in=[1, 2], out=[] |
| 4 | enqueue(3) |
Append 3 to in_stack |
in=[1, 2, 3], out=[] |
| 5 | dequeue() |
out empty → move all from in to out (3,2,1); then pop from out → 1 |
in=[], out=[3, 2], returns 1 |
6. Trace for a failing example (dequeue from empty queue)
Try to dequeue when nothing has been enqueued.
q = QueueUsingStacks()
q.dequeue()
| Step | Operation | Action taken | Resulting state / note |
|---|---|---|---|
| 1 | Create queue | in_stack=[], out_stack=[] |
([], []) |
| 2 | dequeue() |
out_stack empty → try to refill, but in_stack also empty → raise error |
Raises IndexError("dequeue from empty queue") |
It fails at step 2 because both stacks are empty, meaning there is no item to remove, so the code correctly raises an error instead of returning a fake value.
Summary
New items pile onto one stack, and only when the second stack runs empty do we flip everything over from the first stack into the second - which reverses the order back to correct, so the oldest item always ends up on top and ready to be removed next.
Q3 (Medium): Design a Circular Queue
Problem: Implement a fixed-size queue that reuses freed-up space at the front instead of wasting memory, using a fixed-size array.
Key insight: Use modular arithmetic (% size) to wrap around the front and rear pointers back to index 0 once they hit the end of the array.
Python
class CircularQueue:
def __init__(self, capacity):
self.capacity = capacity
self.queue = [None] * capacity
self.front = 0
self.rear = -1
self.count = 0 # tracks how many elements are currently stored
def enqueue(self, value):
if self.count == self.capacity:
raise OverflowError("Queue is full")
# Wrap around to the start of the array using modulo
self.rear = (self.rear + 1) % self.capacity
self.queue[self.rear] = value
self.count += 1
def dequeue(self):
if self.count == 0:
raise IndexError("Queue is empty")
value = self.queue[self.front]
self.front = (self.front + 1) % self.capacity # wrap around here too
self.count -= 1
return value
def is_full(self):
return self.count == self.capacity
def is_empty(self):
return self.count == 0
cq = CircularQueue(3)
cq.enqueue(1)
cq.enqueue(2)
cq.enqueue(3)
print(cq.dequeue()) # 1 - frees up a slot
cq.enqueue(4) # reuses the freed slot instead of needing more memory
print(cq.dequeue()) # 2
print(cq.dequeue()) # 3
print(cq.dequeue()) # 4
Step-by-Step Explanation
1. What problem does it solve?
It solves the “bounded waiting line with limited seats” problem: like a ticket counter with a fixed number of slots, where people join at the back, leave from the front, and freed slots are reused in a circle.
2. Step-by-step walkthrough
Class and its fields
class CircularQueue:
def __init__(self, capacity):
self.capacity = capacity
self.queue = [None] * capacity
self.front = 0
self.rear = -1
self.count = 0 # tracks how many elements are currently stored
capacityis the maximum number of items the queue can hold.self.queueis a list of that size, initially filled withNoneas empty slots.self.frontis the index of the next item to be removed (the front of the queue).self.rearis the index of the most recently added item; it starts at-1because nothing is added yet.self.counttracks how many items are currently in the queue.
Using count makes it easy to check if the queue is full or empty without tricky index math.
Enqueue (add to the back)
def enqueue(self, value):
if self.count == self.capacity:
raise OverflowError("Queue is full")
# Wrap around to the start of the array using modulo
self.rear = (self.rear + 1) % self.capacity
self.queue[self.rear] = value
self.count += 1
- First, check if the queue is already full by comparing
counttocapacity. If full, raise an error. (self.rear + 1) % self.capacitymovesrearone step forward and wraps around to0when it reaches the end.- We store
valueat the newrearposition and increasecountby 1.
This circular movement lets us reuse slots at the beginning of the array once we reach the end.
Dequeue (remove from the front)
def dequeue(self):
if self.count == 0:
raise IndexError("Queue is empty")
value = self.queue[self.front]
self.front = (self.front + 1) % self.capacity # wrap around here too
self.count -= 1
return value
- If
countis 0, raise an error for “queue is empty”. - We read the value at
frontand movefrontone step forward using wrap-around modulo. - Decrease
countby 1 and return the removed value.
Even though we don't explicitly clear the old slot, it's logically removed because count and front no longer include it.
is_full and is_empty helpers
def is_full(self):
return self.count == self.capacity
def is_empty(self):
return self.count == 0
is_fullchecks if the number of stored items equals capacity.is_emptychecks if there are no items at all.
Example usage
cq = CircularQueue(3)
cq.enqueue(1)
cq.enqueue(2)
cq.enqueue(3)
print(cq.dequeue()) # 1 - frees up a slot
cq.enqueue(4) # reuses the freed slot instead of needing more memory
print(cq.dequeue()) # 2
print(cq.dequeue()) # 3
print(cq.dequeue()) # 4
- Create a circular queue with capacity 3.
- Enqueue
1,2,3→ queue becomes full. - Dequeue → removes
1, freeing one slot. - Enqueue
4→ uses the freed slot via circular wrap-around. - Subsequent dequeues return
2,3, and4in order.
3. Why a circular array fits this problem
A normal list-based queue that removes from the front can waste space or require shifting elements; a circular array avoids that by treating the list as a loop where indices wrap around. This gives efficient, fixed-size queue behavior with constant-time enqueue and dequeue and no need to move existing items.
4. Visual diagram - capacity=3, operations 1,2,3, dequeue, enqueue 4
We'll show queue as a fixed array of size 3, with front, rear, and count.
Initial state
capacity = 3
queue = [None, None, None]
front = 0
rear = -1
count = 0
After enqueue(1)
rear = (-1 + 1) % 3 = 0
queue = [1, None, None]
front = 0, rear = 0, count = 1
After enqueue(2)
rear = (0 + 1) % 3 = 1
queue = [1, 2, None]
front = 0, rear = 1, count = 2
After enqueue(3)
rear = (1 + 1) % 3 = 2
queue = [1, 2, 3]
front = 0, rear = 2, count = 3 (full)
After dequeue() → returns 1
value = queue[0] = 1
front = (0 + 1) % 3 = 1
queue = [1, 2, 3] # slot 0 logically free now
front = 1, rear = 2, count = 2
After enqueue(4)
rear = (2 + 1) % 3 = 0 (wraps around to start)
queue = [4, 2, 3]
front = 1, rear = 0, count = 3
Logical order in queue: [2, 3, 4] (from front=1 onward, wrapping)
5. Trace table - successful run (capacity=3)
| Step | Operation | Action taken | Resulting state (queue, front, rear, count) and return |
|---|---|---|---|
| 1 | Create CQ(3) | queue=[None,None,None], front=0, rear=-1, count=0 |
([None,None,None], 0, -1, 0) |
| 2 | enqueue(1) |
rear=0, queue[0]=1, count=1 |
([1,None,None], 0, 0, 1) |
| 3 | enqueue(2) |
rear=1, queue[1]=2, count=2 |
([1,2,None], 0, 1, 2) |
| 4 | enqueue(3) |
rear=2, queue[2]=3, count=3 (full) |
([1,2,3], 0, 2, 3) |
| 5 | dequeue() |
value=1, front=1, count=2 |
([1,2,3], 1, 2, 2), returns 1 |
| 6 | enqueue(4) |
rear=(2+1)%3=0, queue[0]=4, count=3 |
([4,2,3], 1, 0, 3) |
| 7 | dequeue() |
value=2, front=2, count=2 |
([4,2,3], 2, 0, 2), returns 2 |
| 8 | dequeue() |
value=3, front=0, count=1 |
([4,2,3], 0, 0, 1), returns 3 |
| 9 | dequeue() |
value=4, front=1, count=0 |
([4,2,3], 1, 0, 0), returns 4 |
6. Trace for a failing example (enqueue when full)
Try to add a fourth item when capacity is 3 and the queue is full.
cq = CircularQueue(3)
cq.enqueue(1)
cq.enqueue(2)
cq.enqueue(3)
cq.enqueue(4)
| Step | Operation | Action taken | Resulting state / note |
|---|---|---|---|
| 1 | Create CQ(3) | queue=[None,None,None], front=0, rear=-1, count=0 |
([None,None,None], 0, -1, 0) |
| 2 | enqueue(1) |
rear=0, queue[0]=1, count=1 |
([1,None,None], 0, 0, 1) |
| 3 | enqueue(2) |
rear=1, queue[1]=2, count=2 |
([1,2,None], 0, 1, 2) |
| 4 | enqueue(3) |
rear=2, queue[2]=3, count=3 (full) |
([1,2,3], 0, 2, 3) |
| 5 | enqueue(4) |
count == capacity → raise OverflowError("Queue is full") |
Raises OverflowError("Queue is full") |
It fails at step 5 because count equals capacity, meaning all slots are logically occupied, so the queue correctly refuses to add more items until some are dequeued.
7. Summary
A fixed-size array acts like a circle of numbered seats - front and rear pointers move forward and wrap back around to the start using modulo whenever they reach the end, letting freed-up slots be reused endlessly without ever needing to grow the array or shift any items.
Q4 (Medium): Reverse the First K Elements of a Queue
Problem: Given a queue and an integer k, reverse the first k elements of the queue while keeping the rest of the queue in its original order.
Key insight: Since a queue can't be reversed directly (FIFO order can't be flipped in place), use a stack as a helper - pushing the first k elements onto a stack naturally reverses their order, since a stack is LIFO.
Python
from collections import deque
def reverse_first_k(queue, k):
if k <= 0 or k > len(queue):
return queue
stack = []
# Move the first k elements into a stack - this reverses their order
for _ in range(k):
stack.append(queue.popleft())
# Push them back into the queue - now in reversed order
while stack:
queue.append(stack.pop())
# Move the remaining (already in-order) elements to the back,
# so the reversed part stays at the front where it belongs
for _ in range(len(queue) - k):
queue.append(queue.popleft())
return queue
q = deque([1, 2, 3, 4, 5])
print(list(reverse_first_k(q, 3)))
# [3, 2, 1, 4, 5]
Step-by-Step Explanation
1. What problem does it solve?
It solves the partially reverse a line problem: imagine people in a queue where you want to reverse the order of the first k people but leave everyone else standing in the same order behind them.
2. Step-by-step walkthrough
Imports and function signature
from collections import deque
def reverse_first_k(queue, k):
dequeis a double-ended queue that supports efficientpopleft()(remove from front) andappend()(add to back).- The function takes two inputs:
queue: a deque representing the line of items.k: how many items from the front should be reversed.
The function will modify the queue so that only its first k elements are reversed.
Validate k
if k <= 0 or k > len(queue):
return queue
- If
kis zero or negative, there's nothing to reverse, so we just return the queue unchanged. - If
kis larger than the number of items in the queue, it's invalid for this logic, so we also return the queue unchanged.
This protects the rest of the code from bad input.
Prepare a temporary stack
stack = []
stackis a normal Python list used as a stack (last-in, first-out).
We'll use this stack to temporarily hold the first k items and reverse their order.
Move the first k elements into the stack
for _ in range(k):
stack.append(queue.popleft())
- Repeat
ktimes:queue.popleft()removes the front item from the queue.stack.append(...)pushes that item onto the stack.
Because a stack is last-in, first-out, the first item removed from the queue ends up at the bottom of the stack, and the k-th item ends up on top, effectively reversing their order.
After this loop:
- The queue now contains only the remaining items (from position k+1 onward).
- The stack contains the first k items in reversed order (top = original k-th item).
Push them back into the queue in reversed order
while stack:
queue.append(stack.pop())
- While the stack is not empty:
stack.pop()removes the top item from the stack.queue.append(...)adds that item to the back of the queue.
Since the stack's top is the original k-th item, these items are added to the queue in reversed order.
At this point:
- The queue's front part is the “rest” of the original items (those after the first k).
- The queue's back part is the reversed first-k items.
Rotate the queue so the reversed part comes to the front
for _ in range(len(queue) - k):
queue.append(queue.popleft())
len(queue) - kis the number of “rest” items (those that were originally after the first k).- Repeat that many times:
queue.popleft()removes the front item (one of the “rest” items).queue.append(...)adds it to the back.
This rotation moves all the “rest” items from the front to the back, so the reversed k items end up at the front of the queue where they belong.
Return the modified queue
return queue
- The queue now has:
- First k items: reversed.
- Remaining items: original order.
Example usage
q = deque([1, 2, 3, 4, 5])
print(list(reverse_first_k(q, 3)))
# [3, 2, 1, 4, 5]
- Start with queue:
[1, 2, 3, 4, 5]. - We want to reverse the first 3 items.
- After moving first 3 to stack:
- Queue:
[4, 5] - Stack (bottom→top):
[1, 2, 3]
- Queue:
- After popping stack back into queue:
- Queue:
[4, 5, 3, 2, 1]
- Queue:
- After rotating
len(queue) - k = 5 - 3 = 2times:- Rotate 1:
[5, 3, 2, 1, 4] - Rotate 2:
[3, 2, 1, 4, 5]
- Rotate 1:
- Final result:
[3, 2, 1, 4, 5].
3. Why a stack fits this problem
A stack naturally reverses the order of items: whatever goes in first comes out last. By moving the first k queue items into a stack and then back into the queue, we automatically get those k items in reversed order without manual index juggling.
4. Visual diagram - successful example (queue = [1, 2, 3, 4, 5], k = 3)
Start: queue = [1, 2, 3, 4, 5], k = 3
--------------------------------------------------------------------------
Step 1: move first 3 into a stack (via popleft)
queue: [1, 2, 3, 4, 5]
│ popleft 1 → push to stack
▼
queue: [2, 3, 4, 5] stack: [1]
│ popleft 2 → push to stack
▼
queue: [3, 4, 5] stack: [1, 2]
│ popleft 3 → push to stack
▼
queue: [4, 5] stack: [1, 2, 3]
▲ top
--------------------------------------------------------------------------
Step 2: pop stack, append to queue (reverses the order!)
stack: [1, 2, 3] queue: [4, 5]
│ pop 3 → append to queue
▼
stack: [1, 2] queue: [4, 5, 3]
│ pop 2 → append to queue
▼
stack: [1] queue: [4, 5, 3, 2]
│ pop 1 → append to queue
▼
stack: [] queue: [4, 5, 3, 2, 1]
--------------------------------------------------------------------------
Step 3: rotate the 2 untouched items (4, 5) from front to back
queue: [4, 5, 3, 2, 1]
│ popleft 4 → append to back
▼
queue: [5, 3, 2, 1, 4]
│ popleft 5 → append to back
▼
queue: [3, 2, 1, 4, 5] ← reversed first 3, untouched rest, in order!
5. Trace table - successful run (queue = [1, 2, 3, 4, 5], k = 3)
| Step | Operation | Action taken | Resulting state (queue, stack) |
|---|---|---|---|
| 1 | Start | queue=[1,2,3,4,5], stack=[] |
queue=[1,2,3,4,5], stack=[] |
| 2 | Move first 3 to stack (i=1) | popleft() → 1, stack.append(1) |
queue=[2,3,4,5], stack=[1] |
| 3 | Move first 3 to stack (i=2) | popleft() → 2, stack.append(2) |
queue=[3,4,5], stack=[1,2] |
| 4 | Move first 3 to stack (i=3) | popleft() → 3, stack.append(3) |
queue=[4,5], stack=[1,2,3] |
| 5 | Push stack back (pop 3) | stack.pop() → 3, queue.append(3) |
queue=[4,5,3], stack=[1,2] |
| 6 | Push stack back (pop 2) | stack.pop() → 2, queue.append(2) |
queue=[4,5,3,2], stack=[1] |
| 7 | Push stack back (pop 1) | stack.pop() → 1, queue.append(1) |
queue=[4,5,3,2,1], stack=[] |
| 8 | Rotate (5-3)=2 times (rot 1) | popleft() → 4, append(4) |
queue=[5,3,2,1,4] |
| 9 | Rotate (rot 2) | popleft() → 5, append(5) |
queue=[3,2,1,4,5] (final) |
6. Trace for a failing / edge example (k invalid)
Case 1: k is larger than the queue length.
q = deque([1, 2, 3])
reverse_first_k(q, 5) # k > len(queue)
| Step | Operation | Action taken | Resulting state / note |
|---|---|---|---|
| 1 | Start | queue=[1,2,3], k=5 |
queue=[1,2,3], stack=[] |
| 2 | Check k |
k > len(queue) is True → immediately return queue |
Queue unchanged: [1,2,3]; function exits early |
The function stops at step 2 because k is invalid (greater than the number of elements), so it safely returns the original queue without modifying anything. (Another similar edge case is k <= 0, which also returns the queue unchanged for the same reason.)
Summary
The first k items are temporarily poured into a stack to flip their order, poured back into the queue, and then the untouched remaining items are rotated from front to back so the newly reversed section settles correctly at the front.
Q5 (Medium-Hard): Sliding Window Maximum
Problem: GGiven an array and a window size k, return the maximum value in each sliding window as it moves across the array.
Key insight: Use a deque that stores indices, keeping it in decreasing order of value. This lets you get the max in O(1) at any point, and the whole array is processed in O(n) total.
Python
from collections import deque
def sliding_window_max(nums, k):
result = []
window = deque() # stores indices; values stay in decreasing order
for i, num in enumerate(nums):
# Remove indices that have slid out of the current window
if window and window[0] <= i - k:
window.popleft()
# Remove smaller values from the back - they can never be
# the max while a bigger, more recent value is still in play
while window and nums[window[-1]] < num:
window.pop()
window.append(i)
# Once we've seen at least k elements, record the max
if i >= k - 1:
result.append(nums[window[0]]) # front of deque = current max
return result
print(sliding_window_max([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]
Step-by-Step Explanation
1. What problem does it solve?
It solves the “moving window maximum” problem: imagine looking at a fixed-size window that slides over a row of numbers, and for each position you want the biggest number currently visible inside that window.
2. Step-by-step walkthrough
Imports and function signature
from collections import deque
def sliding_window_max(nums, k):
dequeis a double-ended queue that allows fast removal from both front and back.numsis the list of numbers.kis the window size (how many consecutive numbers to consider at once).
The function will return a list of maximums, one for each window position.
Prepare result list and the deque
result = []
window = deque() # stores indices; values stay in decreasing order
resultwill collect the maximum for each window.windowis a deque that stores indices of numbers, not the numbers themselves.- The key idea: values at these indices are kept in decreasing order inside the deque, so the front always holds the index of the current maximum.
Storing indices lets us easily check whether an element has slid out of the window.
Loop through each number with its index
for i, num in enumerate(nums):
iis the current index,numis the current number.
At each step, we update the deque to reflect the current window and then possibly record a maximum.
Remove indices that are out of the current window
if window and window[0] <= i - k:
window.popleft()
window[0]is the index at the front of the deque (the oldest index we're tracking).i - kis the index just before the current window starts.- If the front index is less than or equal to
i - k, that element is no longer in the window, so we remove it withpopleft().
Remove smaller values from the back
while window and nums[window[-1]] < num:
window.pop()
window[-1]is the index at the back of the deque.- While its value is smaller than the current
num, we remove it withpop().
A smaller, older value can never be the maximum while a newer, larger value is present in the same or future windows, so we safely discard it. This maintains decreasing order in the deque.
Add the current index
window.append(i)
After removing out-of-window and smaller elements, we add index i to the back of the deque.
Record the maximum once the first full window is formed
if i >= k - 1:
result.append(nums[window[0]]) # front of deque = current max
- The first full window ends at index
k - 1. - For all
i >= k - 1,nums[window[0]]gives the current maximum, which we append toresult.
Return the list of maximums
return result
Example usage
print(sliding_window_max([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]
3. Why a deque fits this problem
A deque allows us to efficiently remove outdated indices from the front and smaller candidates from the back in amortized constant time. This dual-ended removal is exactly what we need to maintain a “best candidates” list for the sliding window maximum.
4. Visual diagram - nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
Initial state
i = -, num = -, window = [], result = []
i = 0, num = 1
window = [0] (vals: [1])
result = []
i = 1, num = 3
window = [1] (vals: [3])
result = []
i = 2, num = -1 (first full window)
window = [1, 2] (vals: [3, -1])
result = [3]
i = 3, num = -3
window = [1, 2, 3] (vals: [3, -1, -3])
result = [3, 3]
i = 4, num = 5
window = [4] (vals: [5])
result = [3, 3, 5]
i = 5, num = 3
window = [4, 5] (vals: [5, 3])
result = [3, 3, 5, 5]
i = 6, num = 6
window = [6] (vals: [6])
result = [3, 3, 5, 5, 6]
i = 7, num = 7
window = [7] (vals: [7])
result = [3, 3, 5, 5, 6, 7]
5. Trace table - successful run
| i | num | Action on window (indices) |
window after actions (indices, values) |
Result so far |
|---|---|---|---|---|
| 0 | 1 | append 0 | [0] (1) |
[] |
| 1 | 3 | pop 0 (1<3), append 1 | [1] (3) |
[] |
| 2 | -1 | append 2 | [1,2] (3,-1) |
[3] |
| 3 | -3 | append 3 | [1,2,3] (3,-1,-3) |
[3,3] |
| 4 | 5 | pop left 1 (out), pop 3 (-3<5), pop 2 (-1<5), append 4 | [4] (5) |
[3,3,5] |
| 5 | 3 | append 5 | [4,5] (5,3) |
[3,3,5,5] |
| 6 | 6 | pop left 4 (out), pop 5 (3<6), append 6 | [6] (6) |
[3,3,5,5,6] |
| 7 | 7 | pop 6 (6<7), append 7 | [7] (7) |
[3,3,5,5,6,7] |
6. Trace for an edge example (k = 1)
When k = 1, each window is just a single element, so the maximum of each window is the element itself.
sliding_window_max([2, 4, 3], 1)
# Expected: [2, 4, 3]
| i | num | Out-of-window check (window[0] <= i-1) |
Back removal (nums[back] < num) |
window after append |
Result so far |
|---|---|---|---|---|---|
| 0 | 2 | window empty → skip | empty → skip | [0] (2) |
[2] |
| 1 | 4 | 0 <= 0 → pop left → [] |
empty → skip | [1] (4) |
[2,4] |
| 2 | 3 | 1 <= 1 → pop left → [] |
4 < 3? No |
[2] (3) |
[2,4,3] |
7. Summary
The deque keeps indices in decreasing order of value, front to back, by discarding old positions that fell out of the window and smaller values that can never win again - leaving the current maximum always sitting right at the front, ready to read off.
Click here to access the source code repository.
6. Common Patterns Table
Not sure which type of queue problem you're looking at? This section breaks down the most common queue patterns you'll run into - both in real coding interviews and in everyday programming. Each pattern comes with the keywords that usually give it away in a problem statement, plus example problems so you can see it in action.
| Pattern | Signal / Keywords in the Problem | Example Problems |
|---|---|---|
| Basic FIFO processing | "First come, first served," "process in order," "scheduling" | Print Queue, Support Ticket System |
| BFS traversal | "Levels," "layer by layer," "spread/infect" | Level Order Traversal, Rotting Oranges |
| Fixed-size window tracking | "Last k values," "moving average," "recent window" | Moving Average from Data Stream, Rate Limiter |
| Queue-to-stack conversion (and back) | "Reverse the order," "implement using [the other structure]" | Reverse First K Elements, Queue using Stacks |
| Circular / fixed-size buffering | "Fixed capacity," "reuse space," "ring buffer" | Design Circular Queue, Design Circular Deque |
| Stack-to-queue conversion | "Implement using [the other structure]" | Queue using Stacks, Stack using Queues |
7. Practice Roadmap
Work through these roughly in order - easy to hard:
- Implement Queue using Array/List - Easy (foundational, build it yourself)
- Design Circular Queue - Medium (LeetCode #622)
- Implement Queue using Stacks - Easy (LeetCode #232)
- Moving Average from Data Stream - Easy (LeetCode #346)
- Number of Recent Calls (Rate Limiter style) - Easy (LeetCode #933)
- Binary Tree Level Order Traversal (BFS basics) - Medium (LeetCode #102)
- Reverse First K Elements of a Queue - Medium (GeeksforGeeks classic - good stack/queue combo practice)
- Rotting Oranges (BFS on a grid) - Medium (LeetCode #994)
- Open the Lock (BFS shortest path) - Medium (LeetCode #752)/li>
Suggested platforms:
- LeetCode: filter by the "Queue" or "Breadth-First Search" tag.
- NeetCode 150: groups these logically, with the "Graphs" and "Trees" sections leaning heavily on BFS/queue usage.
- Codeforces: for competitive-style scheduling and simulation problems.
Next logical topic: Singly Linked List - since the LinkedQueue implementation in Method 3 is built directly on singly linked list nodes, and understanding that structure on its own makes it much easier to see how the front/rear pointers here actually work under the hood.
How to Create and Deploy a Flask App on VPS
Tutorial
Deploy Flask on VPS with Nginx, Gunicorn & SSL. Free & paid options for beginners. Go To Tutorial