Master Data Structures & Algorithms
A hands-on computing curriculum built for Data Science students. Step through 8 foundational algorithms and 4 essential data structures with real-time visual execution, step-by-step memory traces, annotated Python implementations, and interactive comprehension checkpoints.
Pre-requisites
⏱️ ~15–20 min first visitPython fundamentals you need before the algorithms.
A program is a recipe. A variable is a labeled box. Read one card. Do the paper try. Skip to Searching only if this already feels easy.
Pre-requisites · 1 of 9
Programs and execution order
Python starts at the first line and walks down. Nothing jumps unless you later write a loop or a function.
Python code (optional)
Practice question
If the first line is name = "Ada", what does print(name) show?
Answer: Ada
Pre-requisites · 2 of 9
Indentation
Python uses spaces instead of curly braces. Lines that belong together sit a little to the right. Wrong spaces = the recipe breaks.
Python code (optional)
Practice question
The line print("Always runs") is not indented under the if. Does it still run when score is 10?
Answer: Yes — it is not inside the if.
Pre-requisites · 3 of 9
Comments, print, and return
A # line is a note for humans. The computer skips it. print shows something. return sends an answer back to whoever called the function.
Python code (optional)
Practice question
Does return n * 2 show 8 on the screen by itself?
Answer: No. print shows; return only sends the number back.
Pre-requisites · 4 of 9
Lists and indexing
A list is a numbered shelf. The first slot is 0, not 1. Asking for a slot that does not exist is an error (Python says IndexError).
Python code (optional)
Practice question
If nums = [4, 1, 9], what is nums[1]?
Answer: 1 (the middle one). nums[0] is 4.
Pre-requisites · 5 of 9
For and while loops
for repeats a known number of times. while repeats until a condition changes. If the condition never changes, it would never stop — we always change it on purpose.
Python code (optional)
Practice question
A while needs the condition to become false. Who is responsible for changing the counter?
Answer: You (the programmer) — otherwise it never stops.
Pre-requisites · 6 of 9
Range and len
range(3) is 0, 1, 2 — three numbers, ending before 3. len is “how many items.” Together they walk every shelf slot.
Python code (optional)
Practice question
How many times does for i in range(4) run?
Answer: 4 times (i = 0,1,2,3)
Pre-requisites · 7 of 9
If-else conditions
= puts a value in a box. == asks “are these the same?” Mixing them up is the most common first bug.
Python code (optional)
Practice question
To ask whether score is 100, do you write score = 100 or score == 100?
Answer: == asks. = puts 100 in the box.
Pre-requisites · 8 of 9
Functions
The names in the parentheses are inputs. return is the answer you send back. Every algorithm below is a function.
Python code (optional)
Practice question
In def greet(name):, what is name?
Answer: An input (parameter) — the stuff in the parentheses.
Pre-requisites · 9 of 9
Swapping and mutability
Sorting changes the same list in place. That is why we can swap two seats without copying the whole shelf.
Python code (optional)
Practice question
After a, b = b, a if a was 5 and b was 10, what is a?
Answer: 10 — same two boxes, values swapped.
Optional: dictionaries and 2D grids
BFS/A* code uses a dict (name → neighbors) and a grid (row, column). Skip this until those lessons.
Data Structures
⏱️ ~25 minThe foundational containers of computer science: Stack, Queue, Linked List, and Hash Map.
Before you can search or sort data effectively, you need to understand how data is stored, connected, and accessed in memory. Explore LIFO stacks, FIFO queues, pointer-based linked lists, and O(1) hash maps.
Data Structures
Stack
LIFO stands for "Last In, First Out." The most recently added item is the first one you can remove.
Where you've already used this: Every time you press Ctrl+Z (or Cmd+Z on Mac) to undo something, you're using a stack. Each action you take (typing a word, deleting a line, pasting text) gets "pushed" onto an undo stack. When you hit undo, the most recent action gets "popped" off the top and reversed. The action before that is now on top, ready for the next undo. You can't undo the very first action without undoing everything that came after it, because a stack only gives you access to the top item.
What makes a stack a stack: A stack is a container with one strict rule: you can only add and remove items from one end (the "top"). This is what LIFO means in practice. Picture a stack of cafeteria trays. You place clean trays on top and take trays from the top. You physically cannot pull a tray from the middle or bottom without lifting everything above it first. This constraint might seem limiting, but it turns out to be exactly the right structure for many real problems.
The four core operations:
- Push(item): Add a new item to the top. The stack grows by one. This is O(1), meaning it takes the same amount of time regardless of how many items are already in the stack.
- Pop(): Remove and return the top item. The stack shrinks by one. Also O(1). If the stack is empty, this fails (called an underflow), so you should always check before popping.
- Peek() / Top(): Look at the top item without removing it. Useful when you need to check what's next without committing to removing it.
- Is_Empty(): Returns
Trueif the stack has zero items. Always call this before Pop to avoid errors.
Why all operations are O(1): In Python, a stack is implemented using a regular list. Push uses list.append(), which adds to the end. Pop uses list.pop(), which removes from the end. Neither of these operations needs to shift any other elements, so both take constant time regardless of how large the stack is. Compare this to removing from the front of a list (list.pop(0)), which requires shifting every remaining item down one position and takes O(n) time. The stack avoids this entirely by always operating on the same end.
Where stacks appear in computing: Stacks are one of the most widely used data structures in computer science. Your browser's back button maintains a stack of visited pages. Every programming language uses a "call stack" to track which function called which: when function A calls function B, B is pushed onto the stack, and when B finishes, it's popped off and control returns to A. This is exactly how the recursion in Quick Sort works behind the scenes. Text editors use stacks for undo/redo (two stacks: undoing pops from the undo stack and pushes onto the redo stack). Expression evaluation in calculators, syntax checking for matching parentheses in code, and depth-first search in graph algorithms all rely on stacks.
How it compares to a Queue: A stack and a queue are both simple containers with restricted access. The difference is which end items leave from. A stack removes from the same end you add to (the top), so the newest item leaves first (LIFO). A queue removes from the opposite end (the front), so the oldest item leaves first (FIFO). Whether you need a stack or a queue depends entirely on whether "most recent first" or "first come, first served" is the right ordering for your problem.
Common mistakes to watch for: (1) Calling pop() on an empty stack. This raises an IndexError in Python. Always check is_empty() first, or use a try/except block. (2) Confusing list.pop() (removes from the end, O(1)) with list.pop(0) (removes from the front, O(n)). For a stack, you always want pop() with no argument. (3) Using a stack when you actually need a queue. If items should be processed in the order they arrived (like customer support tickets), you need FIFO, not LIFO.
Flowchart
flowchart TD
Start([Start]) --> Push["Push: Add item to the top"]
Push --> B{"What do you
want to do?"}
B -->|Pop| C["Remove and return
the top item"]
B -->|Peek| D["Look at the top item
without removing it"]
B -->|Is Empty| E["Check if the stack
has any items"]
C --> B
D --> B
E --> B
B -->|Done| Stop([Stop])
Algorithm steps
- Push(item): Add an item to the top of the stack. The stack size grows by 1. The new item becomes the new "top."
- Pop(): Remove and return the top item. The stack size shrinks by 1. If the stack is empty, this fails (called an underflow).
- Peek() / Top(): Look at the top item without removing it. Useful for checking what's next without committing to a pop.
- Is_Empty(): Returns
Trueif the stack has zero items,Falseotherwise. Always check before calling Pop!
🧠 The Brain Map: How to code this
- The Container: A stack is just a list (
self.items = []). The "top" of the stack is always the end of the list. - Push: Use
list.append(item). Adds to the end (top). - Pop: Use
list.pop(). Removes and returns the last element. - Peek: Access
self.items[-1]. The last element, without removing it. - Is_Empty: Check
len(self.items) == 0.
Python code (optional)
What class means here (10 seconds): A class is a named bundle of data plus the verbs that use it (push, pop). Stack() makes one bundle. You can skip the object-oriented theory.
Help: Stack
- Only the top plate moves. That is Last In, First Out.
- Pop on empty: the status will say the stack is empty. Do not panic.
classhere means “a labeled box of rules.” You are not taking an OOP course.
Data Structures
Queue
FIFO stands for "First In, First Out." The first item added is the first one removed.
Where you've already used this: Every time you send a document to a printer, it joins a queue. If someone else sent a document before you, theirs prints first. Your document waits its turn and prints when it reaches the front of the line. This is a queue: items are added at the back and removed from the front, in the exact order they arrived. The person (or document, or task) that has been waiting the longest is always served next.
What makes a queue a queue: A queue enforces fairness through its access rule: new items join at the rear, and items leave from the front. This is the opposite of a stack, where everything happens at the same end. In a queue, the item you added first will be the first one removed (FIFO). You cannot skip ahead, and you cannot reach into the middle. This constraint is exactly what you want when order of arrival matters.
The four core operations:
- Enqueue(item): Add a new item to the rear of the queue. The queue grows by one.
- Dequeue(): Remove and return the item at the front. The queue shrinks by one. If the queue is empty, this fails, so always check first.
- Front(): Look at the front item without removing it. Like checking who's next in line without actually serving them.
- Is_Empty(): Returns
Trueif the queue has zero items.
A real Python gotcha with performance: The implementation below stores items in a plain Python list. Adding to the rear with list.append() is O(1), so enqueue() is fast. But removing from the front with list.pop(0) is O(n), because Python has to shift every remaining item down one position to fill the gap at index 0. For a queue with 1,000 items, that means moving 999 items just to remove one. This is fine for learning and for small queues, but real-world Python code uses collections.deque instead. A deque (double-ended queue) is designed to add and remove from both ends in O(1) time, making it the proper choice for production queues.
Where queues appear in computing: Queues are everywhere in systems that need to handle requests fairly and in order. When you press Ctrl+P, your print job enters a queue behind any jobs that were submitted earlier. Customer support ticketing systems use queues so that the person who contacted support first gets helped first. Web servers use request queues to handle incoming traffic: if 1,000 users hit the server at the same time, the requests are queued and processed in order. Operating systems use queues for CPU task scheduling, deciding which program gets processor time next. Message brokers like RabbitMQ and Amazon SQS are essentially sophisticated queue systems that let different parts of a large application communicate reliably.
How it compares to a Stack: Both stacks and queues are containers with restricted access, and both support add/remove operations. The difference is entirely about ordering. A stack gives you the most recent item (LIFO), which is useful for undo operations, backtracking, and recursion tracking. A queue gives you the oldest item (FIFO), which is useful when fairness or arrival order matters. If you're building an undo button, you want a stack. If you're building a task scheduler, you want a queue.
Common mistakes to watch for: (1) Using list.pop(0) in performance-sensitive code. For small queues this is fine, but for queues that grow to thousands of items, the O(n) cost of shifting elements adds up quickly. Switch to collections.deque and use popleft() instead. (2) Confusing a queue with a stack. If you find yourself processing the newest item first, you're using LIFO behaviour and should use a stack instead. (3) Forgetting to check is_empty() before calling dequeue(). Attempting to remove from an empty queue raises an IndexError.
Flowchart
flowchart TD
Start([Start]) --> Enq["Enqueue: Add item to the rear"]
Enq --> B{"What do you
want to do?"}
B -->|Dequeue| C["Remove and return
the front item"]
B -->|Front| D["Look at the front item
without removing it"]
B -->|Is Empty| E["Check if the queue
has any items"]
C --> B
D --> B
E --> B
B -->|Done| Stop([Stop])
Algorithm steps
- Enqueue(item): Add an item to the rear (back) of the queue. The queue size grows by 1.
- Dequeue(): Remove and return the item at the front. The queue size shrinks by 1. If the queue is empty, this fails.
- Front(): Look at the front item without removing it. Like checking who's next in line.
- Is_Empty(): Returns
Trueif the queue has zero items,Falseotherwise.
🧠 The Brain Map: How to code this
- The Container: A queue is also a list (
self.items = []). The front is index 0, the rear is the end. - Enqueue: Use
list.append(item). Adds to the rear (end of list). - Dequeue: Use
list.pop(0). Removes and returns the first element (front). - Front: Access
self.items[0]. The first element, without removing it. - Is_Empty: Check
len(self.items) == 0.
Python code (optional)
You will need this picture for BFS next: closest people finish first because they waited in a fair line.
Help: Queue
- Join at the back. Leave from the front.
list.pop(0)is slow on a long Python list. BFS code usesdequeinstead.- Opposite of a stack.
More Data Structures
Linked List
Dynamic means the structure can grow and shrink as needed with no fixed size limit. Node-based means each item stores its own value and a pointer to the next item in the chain.
Where you've already seen this: Think about a treasure hunt where each clue has two things written on it: a riddle (the data) and directions to the next clue's location (the pointer). You start at clue #1 (the head), solve it, follow the directions to clue #2, solve that, follow the directions to clue #3, and so on. The last clue's directions say "there is no next clue" (that's None in Python). If the organiser wants to add a new clue between clues #2 and #3, they don't have to renumber anything. They just change clue #2's directions to point to the new clue, and the new clue's directions point to #3. That's a linked list.
How it's different from a Python list (array): A Python list stores items in a continuous block of memory, like seats in a cinema row. Item 0 is at seat 0, item 1 is at seat 1, and so on. This means you can instantly access any item by its position (my_list[5] goes directly to seat 5), but inserting or deleting in the middle requires shifting everyone to fill the gap or make room. A linked list stores each item in its own separate location in memory, connected only by pointers. You can't jump directly to item 5 because you don't know where it is. You have to start at the head and follow 5 pointers to get there. But inserting or deleting just requires changing a couple of pointers, with no shifting at all.
The building block, the Node: Every linked list is made of nodes. A node is a small object that holds two things: the data (whatever value you want to store) and a reference to the next node. In Python, this is a simple class with self.data and self.next. The linked list itself only keeps track of one thing: the head node (the very first node in the chain). From the head, you can reach every other node by following the .next pointers.
The core operations and their costs:
- Insert at head, O(1): Create a new node, point its
nextto the current head, then update the head to the new node. This takes the same amount of time regardless of list size because you don't need to touch any other nodes. - Insert at tail, O(n): You have to walk the entire chain from head to the last node, then set the last node's
nextto the new node. For a list with 1,000 nodes, that means following 1,000 pointers. - Delete by value, O(n): Walk through the list until you find the node with the target value, then make the previous node's
nextpointer skip over it. The deleted node is now "unlinked" from the chain. - Search, O(n): Start at the head and follow pointers until you find the value or reach
None. There's no way to jump ahead because you don't know where any particular node lives in memory.
When linked lists outperform arrays: Linked lists shine in situations where you frequently insert or delete items at the beginning or middle of the list. In a Python list (array), inserting at the front requires shifting every single element one position to the right, which is O(n). In a linked list, inserting at the head is O(1). This makes linked lists a good choice for implementing stacks (push/pop at the head), for building undo histories where you prepend new actions, and for any scenario where the list size changes rapidly and unpredictably.
When arrays outperform linked lists: If you need to access items by position frequently (my_list[42]), arrays are the clear winner. Arrays give you O(1) random access; linked lists give you O(n). Linked lists also use more memory per item because each node stores a pointer in addition to the data itself. For most everyday Python programming, the built-in list (which is an array underneath) is the right choice. Linked lists become important when you study more advanced data structures like hash tables (which use linked lists to handle collisions) and graphs (where adjacency lists are essentially linked lists).
Common mistakes to watch for: (1) Losing the rest of the list when inserting. If you set head = new_node before setting new_node.next = head, you lose the reference to the old head and everything after it is gone. Always set the new node's pointer first, then update the head. (2) Forgetting to handle the empty list case. If head is None, operations like traversal or deletion need to handle that gracefully rather than trying to access None.next, which crashes with an AttributeError. (3) Not updating the previous node's pointer when deleting. You need to keep track of the node before the one you want to delete, so you can reroute its next pointer around the deleted node.
Flowchart
flowchart LR
Head([Head]) --> N1["Node: 10"]
N1 --> N2["Node: 20"]
N2 --> N3["Node: 30"]
N3 --> Null([None / End])
Algorithm steps
- Insert at Head: Create a new node, point it at the current head, then update the head to this new node. O(1), instant.
- Insert at Tail: Walk through the list until you find the last node, then point it at the new node. O(n), you have to walk the whole chain.
- Delete by Value: Find the node with the target value, then make the previous node skip over it by pointing to the next node. The deleted node is now "unlinked."
- Search: Start at the head and follow the pointers until you find the value or hit None. O(n), you might have to check every node.
- Traverse: Walk from head to tail, visiting every node. Prints the entire list.
🧠 The Brain Map: How to code this
- The Node: Each node has two parts:
self.data(the value) andself.next(a pointer to the next node, orNoneif it's the last). - The List: The LinkedList class tracks
self.head(the first node). IfheadisNone, the list is empty. - Insert at Head: Create a
new_node, setnew_node.next = self.head, then setself.head = new_node. - Traverse: Start at
self.head, use awhile current is not Noneloop, and move forward withcurrent = current.next.
Python code (optional)
The picture is a teaching sketch. The boxes on the right are not real RAM pointers. The Python below really does use next.
Why not always a Python list? Inserting at the front of a Python list shoves every other item over. A linked list just points a new box at the old first box.
Help: Linked List
- Head = the first box. None = “nothing here.”
- You cannot jump to item 50; you walk 50 arrows.
More Data Structures
Hash Map
Where you've already seen this: Think of a row of labeled lockers in a hallway. You do not walk every locker to find yours — you read the label on your key, the hash function tells you which locker row to open, and you grab the value inside. If two students accidentally get the same locker number, the locker holds a chain of name tags (a tiny linked list). That is a hash map: fast lookup by key, with chains for collisions.
Walk through it: Seven buckets (indices 0–6). Hash = sum of character codes mod 7.
| Step | Action | Bucket | Chain after |
|---|---|---|---|
| 1 | put("Ada", 36) | hash("Ada") % 7 → 0 | 0: Ada:36 |
| 2 | put("Bob", 42) | hash("Bob") % 7 → 1 | 1: Bob:42 |
| 3 | put("Eve", 28) | collides with Ada → bucket 0 | 0: Ada:36 → Eve:28 |
| 4 | get("Ada") | go to bucket 0, walk chain | returns 36 |
Speed: Average lookup is O(1) — jump to the bucket, maybe walk a short chain. Worst case is O(n) if every key collides in one bucket (then you are back to scanning a list). Python's dict is a hash map under the hood; you met it in the Pre-requisites optional snippet.
Flowchart
flowchart LR
Key["Key + Value"] --> Hash["hash(key) % buckets"]
Hash --> Bucket["Bucket index"]
Bucket --> Chain["Chain in bucket"]
Chain --> Found["Match key → return value"]
🧠 The Brain Map: How to code this
- Buckets: A list of empty chains —
[[], [], …]. - put: Hash the key → find bucket → update existing key or append to chain.
- get: Hash the key → walk that bucket's chain until the key matches.
- Collision: Same bucket? Append another
[key, value]link — that is why linked lists matter here.
Python code (optional)
Why after linked lists? Each bucket's chain is a miniature linked list. BFS and Queue lessons used dict for “visited” — same idea, faster than scanning every name in a flat list.
Help: Hash Map
- Hash = locker number. Chain = more than one key in the same locker.
puton an existing key updates the value, not a duplicate key.
Seven buckets. Put adds to a chain; Get walks the chain in the highlighted bucket.
Searching Algorithms
⏱️ ~30 minLinear Search, Binary Search, Breadth-First Search (BFS), and A* Search.
Techniques for finding elements in lists and navigating graph spaces. From sequential scanning to divide-and-conquer binary division, and level-by-level breadth exploration to heuristic pathfinding.
Searching
Linear Search
Brute Force means trying every single possibility until you find the answer. No clever shortcuts, just raw checking. Like trying every key on a keychain to open a lock.
Where you've already done this: Imagine you're looking for a specific book on a messy shelf where nothing is in alphabetical order. You have no choice but to start at one end and check every single book, one by one, until you either find the one you want or reach the other end and confirm it's not there. That's Linear Search. It checks each item in sequence, from first to last, without skipping ahead or making any assumptions about where the target might be.
Why "linear": The name comes from the fact that the time it takes grows in a straight line with the size of the data. If you double the number of items, the search takes (roughly) twice as long in the worst case. A list of 10 items means up to 10 checks. A list of 10,000 items means up to 10,000 checks. This direct, proportional relationship between data size and work done is what computer scientists call O(n), or "linear time."
Walk through it with real numbers: Say you have the list [8, 3, 11, 5, 9] and you're searching for 5.
| Step | Position checked | Value found | Match? |
|---|---|---|---|
| 1 | Index 0 | 8 | No, move on |
| 2 | Index 1 | 3 | No, move on |
| 3 | Index 2 | 11 | No, move on |
| 4 | Index 3 | 5 | Yes! Return 3 |
It took 4 checks to find the target at index 3. If you were searching for a value that doesn't exist (say, 99), the algorithm would check all 5 items and then return -1 to indicate "not found."
How fast (or slow) is it:
- Best case, O(1): The target is the very first item. One check, done.
- Worst case, O(n): The target is the very last item, or it doesn't exist at all. You have to check every single element.
- Average case, O(n): On average, the target will be somewhere in the middle, so you check about half the list. In Big-O notation, n/2 simplifies to O(n) because constants are dropped.
Why this vs the last lesson: You just learned to walk a list with a loop. Linear Search is that walk with a question: “is this the thing?” Next you will put the shelf in order. A faster search that needs order comes after that — do not use it on a messy pile.
Where you'll actually see it used: Linear Search shows up more often in real software than you might expect. Checking if a username exists in a short list, scanning through search results on a page, verifying if an item is in a shopping cart, or looking for a specific row in a small spreadsheet are all forms of linear search. Python's in keyword (if x in my_list) is a linear search under the hood. For small datasets (under a few hundred items), the simplicity of linear search often outweighs the speed benefit of fancier approaches.
Common mistakes to watch for: (1) Forgetting to handle the "not found" case. If the loop finishes without finding the target, the function must explicitly return -1 (or equivalent). Leaving this out causes the function to return None in Python, which can cause confusing bugs later. (2) Using Linear Search on large sorted data when Binary Search would be far more efficient. If you're searching the same sorted list thousands of times, switching to Binary Search turns a slow operation into a near-instant one.
Flowchart
flowchart TD
Start([Start]) --> A["Look at the first item"]
A --> B{"Is this the item
you're looking for?"}
B -- Yes --> Found[("Found it!
Return its position")]
B -- No --> C["Move to the next item"]
C --> D{"Have you checked
every item?"}
D -- No --> B
D -- Yes --> NotFound[("Not found.
Return -1")]
Found --> Stop([Stop])
NotFound --> Stop([Stop])
Algorithm steps
- Start at the first item (index 0).
- Check if the current item matches what you're looking for.
- Found it? Return the position and you're done.
- Not a match? Move to the next item.
- Repeat steps 2–4 until you find it or run out of items.
- Ran out of items? Return
-1, it's not in the list.
🧠 The Brain Map: How to code this
- The Goal: Find the
targetnumber. - The Loop: We need a
forloop to look at every indexifrom start to finish. - The Check: Inside the loop, use an
ifstatement. - The Backup: What if the loop finishes and we found nothing? We
return -1.
Python code (optional)
What if it fails: Empty list? The loop never runs, so you still return -1. Two copies of the same number? You stop at the first one.
Help: Linear Search
- Why -1? It is not a real position. Position 0 is the first item.
- Do I need the list sorted? No. That is the whole point.
- Picture: yellow = checking, grey = already seen, green = found.
Yellow = looking at this bar. Grey = already checked. Green = found. Type 5 (the story number), then Search. New Array picks random numbers.
Binary Search
Binary Search
Sorted means the items are arranged from smallest to largest, like 2, 5, 9, 14, 22. If the list isn't sorted, Binary Search cannot work because there is no way to know which half to discard.
Where you've already done this: Think about looking up a word in a physical dictionary. You don't start at page 1 and read forward. You open to roughly the middle, check whether your word comes before or after what you see, and then throw away the half that can't contain it. You keep halving the remaining pages until you land on your word. That's Binary Search. It works by repeatedly splitting the search area in half, checking the middle item, and discarding the half where the target cannot possibly be.
The one strict rule: Binary Search only works on sorted data. If the list isn't in order, there's no way to know whether the target is in the left half or the right half. This is the trade-off: you need sorted data upfront, but in exchange, you get a search that's dramatically faster than checking every item.
Walk through it with real numbers: Say you have the sorted list [3, 7, 11, 15, 19, 23, 27] and you're searching for 19.
| Step | Search window | Middle value | Decision |
|---|---|---|---|
| 1 | [3, 7, 11, 15, 19, 23, 27] | 15 | 19 > 15, discard left half |
| 2 | [19, 23, 27] | 23 | 19 < 23, discard right half |
| 3 | [19] | 19 | Found it! Return index 4 |
Out of 7 items, it took only 3 checks. A linear search on the same list would have taken 5 checks. The difference becomes massive at scale: a sorted list of 1,000,000 items takes Linear Search up to 1,000,000 checks but Binary Search at most 20.
Why "logarithmic" (O(log n)): Each step cuts the remaining data in half. If you start with n items, after one step you have n/2. After two steps, n/4. After three steps, n/8. The number of steps needed to reach 1 remaining item is log₂(n). For 1,000 items that's about 10 steps. For 1,000,000 items it's about 20. This is why Binary Search feels almost instant even on very large datasets, as long as the data is sorted.
How it compares to Linear Search: Linear Search is simpler and works on unsorted data, but it checks every item one by one. Binary Search is restricted to sorted data, but it eliminates half the candidates per step. The key question is: "Will I search this data more than once?" If yes, sorting it once and then using Binary Search for every lookup is almost always worth it. If you only need to search once, sorting first (which itself costs O(n log n)) plus searching (O(log n)) may cost more total work than a single Linear Search (O(n)).
Where you'll actually see it used: Binary Search is everywhere in real software, often hidden inside other tools. Database indexes use it to find records quickly. When you type a word in a search bar and it auto-completes, the app is often doing a binary search over a sorted word list. Python's bisect module is a built-in binary search implementation. Version control systems like Git use binary search (the git bisect command) to pinpoint which specific commit introduced a bug, halving the suspect range with each test.
Common mistakes to watch for: (1) Applying Binary Search to an unsorted list. It will run without crashing, but it will return incorrect results because it discards halves based on ordering that doesn't exist. (2) Getting the midpoint calculation wrong. The formula mid = (low + high) // 2 works in Python, but in languages with fixed-size integers (like C or Java), low + high can overflow. The safer formula is mid = low + (high - low) // 2. (3) Off-by-one errors when updating low and high. After checking the middle, you must set low = mid + 1 or high = mid - 1 (not low = mid or high = mid), otherwise the search can loop forever.
Flowchart
flowchart TD
Start([Start]) --> A["Your list must be
sorted from low to high"]
A --> B["Mark the start and
end of your search area"]
B --> C{"Is there still an
area to search in?"}
C -- No --> NotFound[("Not found.
Return -1")]
C -- Yes --> D["Pick the middle item
in your search area"]
D --> E{"Is this the item?"}
E -- Yes --> Found[("Found it!
Return its position")]
E -- No --> F{"Is the middle item
smaller than your target?"}
F -- Yes --> G["Target is on the right.
Ignore the left half"]
F -- No --> H["Target is on the left.
Ignore the right half"]
G --> C
H --> C
Found --> Stop([Stop])
NotFound --> Stop([Stop])
Algorithm steps
- Make sure the list is sorted from smallest to largest.
- Set
lowat the start andhighat the end. - Find the middle:
mid = (low + high) // 2(the//drops any decimal, called integer division, so 7 ÷ 2 becomes 3 instead of 3.5). - Check the item at
mid:- Matches target? Return
mid. - Too small? Ignore left half, set
low = mid + 1. - Too big? Ignore right half, set
high = mid - 1.
- Matches target? Return
- Repeat steps 3–4 until you find it or
lowpasseshigh. - Not found? Return
-1.
🧠 The Brain Map: How to code this
- The Pointers: We track our search window with a
low(start) andhigh(end) variable. - The Loop: We use a
while low <= highloop. Keep searching as long as our window exists. - The Logic: Calculate
mid. Ifarr[mid]is too small, move the left boundary (low = mid + 1). If too big, move the right boundary (high = mid - 1).
Python code (optional)
What if the list is not sorted? Binary Search will still run, and it can lie. Example: [9, 1, 4] looking for 1. The middle is 1 — lucky — but looking for 9 it may throw 9 away because it thinks “left is smaller.” Use Linear Search instead.
The // mark: 5 // 2 is 2, not 2.5. We need a whole position number.
Help: Binary Search
- Unsorted? Do not use this. Use Linear Search.
- Grey bars are the half we discarded. They cannot hide the target if the list is sorted.
- “Log” just means “how many times can I cut this in half?”
Grey = thrown away. Yellow = middle of what is left. The first picture is sorted [3, 7, 11, 15, 19, 23, 27]. Try 19.
Pathfinding
Breadth-First Search
Breadth-First Search (BFS) does not scan a sorted list. It walks a graph: it always finishes the close neighbors before it walks farther away.
Where you've already done this: You drop a pebble in a still pond. The first ripple hits the ducks one meter away. The next ripple hits two meters, then three. BFS is that ripple. It never “jumps” to a far node until every closer node has had a turn. A line at a coffee shop is the same idea: whoever has been waiting longest (arrived first) is served first. That line is a Queue — you will meet it properly in Part 3.
A little history: People have asked “what is the shortest walk through a maze?” for a long time. In 1959, Edward F. Moore published a clean method for the shortest path in a maze that is exactly this ripple idea. Social networks still use the same picture: your “friends” are one hop away, “friends of friends” are two hops — that is BFS counting degrees of separation.
On a tree: A tree is a graph with no loops — like an org chart or a folder tree. Start at the boss (the root). BFS visits every person who reports directly to the boss (level 1), then their reports (level 2), and so on. Programmers call this level-order traversal.
Walk through a tiny org chart: Root A has children B and C. B has D and E. Goal: find E.
| Step | Pop from queue (front) | Queue after adding children | Notes |
|---|---|---|---|
| 0 | — | A | Start: put the root in the queue. |
| 1 | A | B, C | A is not E. Enqueue A's children, left to right. |
| 2 | B | C, D, E | B is not E. Enqueue D and E. |
| 3 | C | D, E | C is not E. C has no children here. |
| 4 | D | E | D is not E. |
| 5 | E | (empty) | Match. Stop. Path is A → B → E. |
Notice E is found only after everyone closer to A has been checked. That is the whole algorithm.
On a grid: Treat the floor as graph paper. Each empty square is a node. Its neighbors are up, down, left, right — not diagonals (that keeps later A* math honest). Walls are squares you may not enter. Start in one cell; the goal is another. The ripple fills every cell 1 step away, then 2 steps, then 3. The first time you touch the goal, you have the fewest steps possible, because every cheaper cell was already processed.
Walk through a 3×3 hallway: Start at (0,0), goal at (0,2), wall in the middle of the bottom row.
| Step | Cell dequeued | New neighbors added | Distance from start |
|---|---|---|---|
| 1 | (0,0) | (0,1), (1,0) | 0 |
| 2 | (0,1) | (0,2) goal is next door | 1 |
| 3 | (1,0) | (2,0) if empty | 1 |
| 4 | (0,2) | — | 2 — first visit to the goal. Done. |
How it actually works (the loop):
- Put the start in a queue (FIFO). Remember a parent for every cell so you can walk backward later.
- Mark start as visited so you never enqueue it twice. Forgetting this on a graph with cycles makes an infinite loop.
- While the queue is not empty: take the front item. If it is the goal, stop.
- Look at each neighbor. Skip walls. Skip already visited. Otherwise mark visited, set parent, enqueue.
- If the queue empties, there is no path.
- Build the path by following parent pointers from the goal back to the start, then reverse the list.
A stack (last in, first out) would dive down one corridor (depth-first) and can take a long detour. The queue forces “closer first.”
How fast / how much memory: Let V be nodes and E be edges (on a grid, both are about rows × cols). You look at each node and each edge a constant number of times, so time is O(V+E). The queue and visited set can hold every node, so extra memory is O(V). Best case you find the goal next door (tiny). Worst case you paint the whole reachable map.
Where you'll see it in code: Python's collections.deque is the usual queue (do not list.pop(0) — that is slow). Web crawlers keep a frontier of URLs. “Friend of a friend” is unweighted hops. Games flood-fill a fog map. Compilers and browsers walk trees; document.querySelector is “find a node in this tree.” GPS uses BFS-like ideas when every road is treated as the same cost. Connected-component labeling in images is BFS/DFS on pixels.
How it compares: Linear and Binary Search look up a value in a list. BFS looks up a path in a graph. Binary Search needs sorted numbers; BFS needs a neighborhood. Versus A*: BFS is the fair “check every equally close cell.” A* cheats (in a good way) with a guess toward the goal, so it usually paints fewer cells.
Common mistakes: (1) No visited set — cycles replay forever. (2) Using a Python list as a queue with pop(0) — O(n) every pop; use deque. (3) Diagonal moves while later using Manhattan distance in A*. (4) Thinking BFS minimizes kilometers — it minimizes hops when every edge costs 1.
Flowchart
flowchart TD
Start([Start]) --> Q["Put start in a queue
Mark it visited"]
Q --> Loop{"Is the queue empty?"}
Loop -- Yes --> None[("No path.
Stop")]
Loop -- No --> Pop["Take the front item"]
Pop --> Goal{"Is this the goal?"}
Goal -- Yes --> Path["Walk parent pointers
back to start"]
Path --> Done([Return the path])
Goal -- No --> Nbr["For each neighbor:
skip walls and visited"]
Nbr --> Enq["Set parent, mark visited,
enqueue"]
Enq --> Loop
None --> Stop([Stop])
Done --> Stop
Algorithm steps
- Queue the start node and mark it visited.
- Pop the front of the queue (the oldest waiting node).
- Goal? Reconstruct the path from parent pointers and stop.
- Neighbors: up/down/left/right on a grid, or children on a tree. Skip walls and visited nodes.
- Enqueue each new neighbor; remember who you came from.
- Repeat until the queue is empty (failure) or the goal pops (success).
🧠 The Brain Map: How to code this
- The Goal: A path from start to goal, fewest hops.
- The Queue:
deque— append right, pop left. - The Memory:
visitedplus aparentdict. - The Loop:
while queue:pop, maybe return, else expand neighbors.
Python code (optional)
Python — tree (adjacency list)
Python — grid (4-neighbors)
List search vs path search: Linear/Binary answer “is this number in the list?” BFS answers “how do I walk from here to there, one handshake at a time?”
Help: BFS
- A node is a dot. An edge is a line. A neighbor is one step away.
- You already know the line: that is the Queue lesson.
- Org chart and city grid are two pictures of the same idea. Use the tabs on the right.
Pathfinding
A* Search
A* (say “A star”) still walks a graph, but it does not treat every direction as equal. It keeps a running cost from the start (g) and a heuristic guess of leftover work (h). It always expands the node with the smallest f = g + h.
Where you've already done this: You are in a new city, walking to a cafe you can see on the map. You do not flood every alley equally (BFS). You prefer streets that still point toward the cafe, but you will detour around a closed block. That “how far do I still think it is?” is h. GPS and game characters do the same thing with better maps.
A little history: In 1968 at SRI, Peter Hart, Nils Nilsson, and Bertram Raphael described a family of “A” algorithms. A* was the sweet spot: with an admissible heuristic (a guess that never overestimates), it finds an optimal path and does not waste work the way a blinder search does. Game AI and robot motion planning still teach A* first.
On a tree: Same org chart, but each node also stores a guess “how many hops might remain?” A* does not wait to finish an entire level. It pops the open node whose g + h is smallest — often a child that already looks closer to the goal, even if some cousins are older in the queue sense.
Walk through: Start A (g=0). Children B and C. Goal is under C. If h(C) is smaller than h(B), A* expands C before it finishes B's whole subtree. A step table of the open set (a min-heap in code) is the right picture — not a FIFO line.
| Popped | g | h (guess left) | f = g+h | Why this node |
|---|---|---|---|---|
| A | 0 | 2 | 2 | Only the start is open. |
| C | 1 | 1 | 2 | Looks closer to the goal than B. |
| Goal under C | 2 | 0 | 2 | Goal popped — stop and rebuild the path. |
Important: stop when the goal is popped from the open set, not merely when you first see it as a neighbor. That is how you know no cheaper f is still hiding.
On a grid: g is steps from start (each move costs 1). h is Manhattan distance: |row_goal - row| + |col_goal - col| — the taxi-meter if there were no walls. With only 4-way moves, you can never beat that number, so h never overestimates. Walls only make the real path longer, never shorter than Manhattan. That is why this guess is admissible.
How it actually works (the loop):
- Keep an open set as a min-heap of
(f, node). Put start in withg=0. - Keep
gscores and parents. Optionally a closed set of finished nodes. - Pop the smallest
f. If it is the goal, reconstruct and stop. - For each neighbor: skip walls. Compute
g2 = g + 1. If this is cheaper than any oldg, update parent,f2 = g2 + h(neighbor), push on the heap. - If the heap empties, no path.
If you set h = 0 everywhere, A* has no opinion about the goal and, on equal-cost edges, behaves like Dijkstra — which on a grid of cost-1 moves is the same family as BFS. The heuristic is the only extra brain.
How fast / how much memory: Each heap operation is O(log V), and you may touch every edge, so typical time is O(E log V). A perfect heuristic that guessed the true remaining cost would walk almost a straight path. A useless heuristic (h=0) explores like BFS/Dijkstra. Memory is still O(V) for open/closed maps.
Where you'll see it in code: Python heapq (heappush / heappop). Game NPCs walking around crates. Map apps when they have a straight-line (or Manhattan) estimate. Warehouse robots. The 8-puzzle with Manhattan tile distance. Minecraft-style pathfinding. Any time you can guess remaining cost without lying high.
How it compares: BFS paints an expanding disk. A* paints a blob aimed at the goal — usually fewer cells, same optimal hop-count if h is admissible. Linear/Binary still do not apply: you are not probing a sorted array.
Common mistakes: (1) h that overestimates (e.g. “I swear it is 3 steps” when 5 are required) — you can miss the best path. (2) Diagonal steps plus Manhattan (the guess no longer matches the moves). (3) Scanning the whole open list instead of a heap on large maps. (4) Mixing up “visited fewer cells” with “a different destination.”
Flowchart
flowchart TD
Start([Start]) --> Open["Put start in a min-heap
keyed by f = g + h"]
Open --> Empty{"Heap empty?"}
Empty -- Yes --> None[("No path")]
Empty -- No --> Pop["Pop smallest f"]
Pop --> IsGoal{"Goal popped?"}
IsGoal -- Yes --> Path["Follow parents
back to start"]
Path --> Done([Return the path])
IsGoal -- No --> Nbr["Each neighbor:
g2 = g + 1"]
Nbr --> Better{"Cheaper g?"}
Better -- Yes --> Push["Update parent
push f2 = g2 + h"]
Better -- No --> Empty
Push --> Empty
None --> Stop([Stop])
Done --> Stop
Algorithm steps
- Score start:
g=0,h=heuristic(start), push on a min-heap byf. - Pop the open node with smallest
f. - If it is the goal, reconstruct the path and stop.
- Relax neighbors: if a new path has smaller
g, update parent and push the newf. - Repeat until the heap is empty (no path).
🧠 The Brain Map: How to code this
- The Goal: A cheapest path, using a safe guess
h. - The Heap:
heapqof(f, node). - The Scores: dicts for
gandparent. - The Guard: never let
hbe bigger than the true leftover cost.
Python code (optional)
Python — tree (heuristic on nodes)
Python — grid (Manhattan h)
On a tiny 3-cell walk: start g=0, h=2, f=2. One step toward the goal: g=1, h=1, f=2. Goal: g=2, h=0, f=2. We stop when the goal is taken out of the waiting list, not when we first peek at it.
Admissible means the guess never brags that you are closer than you really are. Footnote: if every guess is 0, A* has no opinion — like BFS on equal steps. That is not a new lesson.
Help: A* Search
- g = steps so far. h = guess left. f = g+h, smallest f goes next.
- Fewer yellow/crimson cells than BFS is typical, not a promise on every maze.
Sorting Algorithms
⏱️ ~30 minSelection Sort, Bubble Sort, Quick Sort, and Merge Sort.
Why order matters: Sorting transforms disordered chaos into predictable sequences, enabling lightning-fast lookups. Contrast simple comparison sorts with divide-and-conquer recursive partitioning and merging.
| Selection | Bubble | Quick | Merge | |
|---|---|---|---|---|
| Method | One swap per pass, locks minimum | Continuous neighbor swaps | Divide and conquer via pivot | Recursive divide & linear merge |
| Average Time | O(n²) | O(n²) | O(n log n) | O(n log n) |
| Space | O(1) | O(1) | O(log n) | O(n) |
Sorting
Selection Sort
Where you've already done this: Imagine you're organising a row of books by height on a shelf. You scan the entire row with your eyes, find the shortest book, and move it to position 1. Then you scan the remaining books, find the next shortest, and move it to position 2. You keep going until every book is in its place. That's Selection Sort. It works by repeatedly selecting the smallest item from the unsorted portion of the list and placing it at the end of the sorted portion.
What makes it different from Bubble Sort: Bubble Sort swaps neighbours constantly as it walks through the list, doing many small swaps per pass. Selection Sort takes a different approach: it looks at everything first, decides which item should go next, and then does exactly one swap to put it in the right spot. The result is that Selection Sort always makes at most n-1 total swaps (one per position), regardless of how messy the data is. Bubble Sort can make far more swaps on the same data.
Walk through it with real numbers: Start with [29, 10, 14, 37, 13].
| Pass | Unsorted portion scanned | Smallest found | Swap with | Result |
|---|---|---|---|---|
| 1 | [29, 10, 14, 37, 13] | 10 | 29 (pos 0) | [10, 29, 14, 37, 13] |
| 2 | [29, 14, 37, 13] | 13 | 29 (pos 1) | [10, 13, 14, 37, 29] |
| 3 | [14, 37, 29] | 14 | 14 (pos 2) | [10, 13, 14, 37, 29] – already in place |
| 4 | [37, 29] | 29 | 37 (pos 3) | [10, 13, 14, 29, 37] |
Notice pass 3: the smallest item was already in the correct position, so the "swap" didn't actually change anything. This happens naturally and the algorithm handles it without any special logic.
How fast (or slow) is it:
- Best case, O(n²): Even if the list is already sorted, Selection Sort still scans the entire unsorted portion on every pass to confirm that the current item is the smallest. It cannot stop early. This is a key difference from Bubble Sort, which can detect a sorted list and stop after one pass.
- Worst case, O(n²): Same number of comparisons regardless of the data's initial order. For 1,000 items, it makes roughly 500,000 comparisons.
- Swaps: At most n-1 swaps total. This is the lowest swap count of any comparison-based sort, which matters when swapping is expensive (for example, writing to flash memory or moving large objects in memory).
The stability question: Selection Sort, as typically written, is not stable. A stable sort preserves the original order of items with equal values. For example, if two students both scored 85 and were originally listed alphabetically, a stable sort keeps them in alphabetical order. Selection Sort can break this order because it swaps items across long distances. Bubble Sort, by contrast, is stable because it only swaps adjacent items.
Where you'll actually see it used: Like Bubble Sort, Selection Sort is too slow for large datasets in production code. However, it has a genuine niche: when the cost of writing data is high relative to the cost of reading it. Flash memory and EEPROM chips, for instance, wear out faster with more write operations. Because Selection Sort minimises the total number of swaps (writes), it is sometimes preferred for embedded systems with this kind of hardware constraint. It's also a useful teaching algorithm because it introduces the concept of "find the minimum in a subarray," which appears in many more advanced algorithms.
Common mistakes to watch for: (1) Assuming Selection Sort can stop early like Bubble Sort. It cannot. It always makes the same number of comparisons regardless of the input, because it must scan the full unsorted portion to guarantee it found the true minimum. (2) Confusing the minimum value with the minimum index. The algorithm tracks min_idx (the position of the smallest item), not the smallest value itself. After the inner loop finishes, it swaps based on that index. (3) Thinking Selection Sort is stable. The long-distance swap can reorder equal elements.
Flowchart
flowchart TD
Start([Start]) --> A["Start with the first
unsorted position"]
A --> B["Assume this position
holds the smallest item"]
B --> C["Check each item
to the right"]
C --> D{"Did you find a
smaller item?"}
D -- Yes --> E["Remember this new
position instead"]
D -- No --> F{"Are there more
items to check?"}
E --> F
F -- Yes --> C
F -- No --> G["Swap the smallest item
into this position"]
G --> H{"Is this the
last unsorted spot?"}
H -- No --> B
H -- Yes --> Sorted[("All items sorted!")]
Sorted --> Stop([Stop])
Algorithm steps
- Start at position 0, the first unsorted spot.
- Assume the smallest item in the unsorted part is at this position.
- Scan every item to the right.
- Found something smaller? Remember its position.
- Swap the smallest item you found into the current position.
- Move to the next position and repeat steps 2–4.
- Done once you've placed every item in its correct spot.
🧠 The Brain Map: How to code this
- Outer Loop: A
forloop (i) representing where the next smallest number should go. - Inner Loop: Assume
iis the minimum. Use a second loop (j) starting fromi+1to scan the rest. - The Swap: If we find something smaller, update
min_idx. After the inner loop, swap them.
Python code (optional)
Help: Selection Sort
- Each pass locks one smallest remaining number into place.
- Green bars on the left are finished. Do not touch them again.
- Compare with Bubble: Selection walks far to swap once; Bubble swaps neighbors all the time.
Amber = the seat we are filling. Crimson = smallest-so-far. Emerald = locked in on the left.
Sorting
Bubble Sort
Where you've already done this: Think about the last time you arranged a messy hand of playing cards. You probably didn't scan the whole hand to find the smallest card first. You just looked at two cards next to each other, swapped them if they were out of order, and moved on to the next pair. That's Bubble Sort. It only ever compares neighbours (two items standing right next to each other in the list) and swaps them if the left one is larger than the right one. One full walk through the list is called a pass.
What actually happens during a pass: On the first pass, the algorithm starts at position 0 and compares every adjacent pair all the way to the end. Every time it finds a pair where the left number is bigger, it swaps them. Here's the key insight: the largest number in the list will win every single comparison it's involved in, so it gets pushed one position to the right each time, all the way to the final position by the end of the pass. This is where the name comes from: the largest value "bubbles up" to the top of the list, the same way an air bubble rises to the surface of water.
Walk through it with real numbers: Start with [5, 1, 4, 2].
| Pass | What happens | Result after pass |
|---|---|---|
| 1 | Compare 5 & 1 → swap. Compare 5 & 4 → swap. Compare 5 & 2 → swap. | [1, 4, 2, 5] – 5 is locked at the end |
| 2 | Compare 1 & 4 → fine. Compare 4 & 2 → swap. | [1, 2, 4, 5] – 4 is locked too |
| 3 | Compare 1 & 2 → fine. Zero swaps this entire pass. | [1, 2, 4, 5] – sorted, stop early |
Notice two things. First, each pass checks one fewer position than the last, because the end of the list is already sorted. That's why the code uses n - i - 1 as the inner loop boundary. It is not a magic formula, just "don't re-check positions that are already in their final place." Second, pass 3 made zero swaps, which told the algorithm the list was already sorted and it could stop. This early-termination check is important: without it, the algorithm would keep making pointless passes over an already-sorted list.
How fast (or slow) is it:
- Best case, O(n): If the list is already sorted, the algorithm does one pass, sees zero swaps, and stops. That single pass makes n−1 comparisons, so it runs in linear time.
- Worst case, O(n²): If the list is in reverse order (e.g.,
[5, 4, 3, 2, 1]), every pair needs a swap on every pass, and the algorithm needs the maximum number of passes. For a list of 1,000 items, that's roughly 500,000 comparisons. - Average case, O(n²): For a randomly shuffled list, Bubble Sort still tends to need most of those passes. This is genuinely slow compared to algorithms like Quick Sort, which averages O(n log n).
How it compares to Selection Sort: Both are O(n²) sorting algorithms, and both are used primarily for teaching. The difference is in how they move data. Selection Sort scans the entire unsorted portion to find the single smallest element, then places it in its final position with one swap. It minimises the number of swaps (at most n−1 total), which matters when writing data is expensive, like on flash storage. Bubble Sort swaps constantly during every pass, but it has a property Selection Sort doesn't: stability. Two elements with equal value will keep their original relative order after sorting. If you sort a class roster by grade and two students both have 85%, Bubble Sort guarantees they stay in whatever order they were in before (say, alphabetical). Selection Sort, as typically implemented, does not make that guarantee.
Where you'll actually see it used: Honestly, almost nowhere in production software. Bubble Sort is one of the slowest general-purpose sorting algorithms. Python's built-in sorted() uses Timsort, which is roughly 1,000× faster on large lists. Bubble Sort survives in every introductory computer science course because it is the easiest sorting algorithm to trace by hand and understand step by step. It builds the mental model you need before tackling recursive algorithms like Quick Sort. It is also a common interview warm-up, not because anyone expects you to use it in real code, but because explaining why it's slow is a good test of whether someone genuinely understands time complexity.
Common mistakes to watch for: (1) Assuming Bubble Sort is always O(n²). With the early-termination check, an already-sorted list finishes in one pass at O(n). (2) Confusing comparisons with swaps. Every pass makes n−i−1 comparisons, but not every comparison causes a swap. (3) Off-by-one errors in the inner loop: forgetting the -1 in n-i-1 and accessing an index that doesn't exist, or forgetting the -i and wastefully re-checking elements already in their final place.
Flowchart
flowchart TD
Start([Start]) --> A["Start a new pass
through the list"]
A --> B["Compare two
neighbouring items"]
B --> C{"Are they in
the wrong order?"}
C -- Yes --> D["Swap them"]
C -- No --> E["Leave them as they are"]
D --> F["Move to the
next pair"]
E --> F
F --> G{"Reached the
end of the list?"}
G -- No --> B
G -- Yes --> H{"Did you swap
anything this pass?"}
H -- Yes --> A
H -- No --> Sorted[("List is sorted!")]
Sorted --> Stop([Stop])
Algorithm steps
- Start at the beginning of the list.
- Compare the current item with the one right next to it.
- Out of order? Swap them so the smaller comes first.
- Move to the next pair and repeat steps 2–3.
- Reached the end? The largest item is now at the very end. It's sorted.
- Repeat steps 1–5 for the remaining unsorted items.
- Done when a full pass needs zero swaps.
🧠 The Brain Map: How to code this
- The Pass: We need to pass through the list multiple times. (Outer
forloop). - The Check: Compare adjacent elements:
arr[j] > arr[j+1]. If they are out of order, swap them! - The Optimization: The inner loop goes to
n - i - 1. Why? Because after every pass, the biggest element is safely at the end.
Python code (optional)
Help: Bubble Sort
- Only neighbors swap. The biggest number of a pass rides to the right.
- If a pass makes zero swaps, we stop. The picture and the Python both do that now.
- That is why an already-sorted list is fast (one pass).
Two neighbors light up. Green = that end is finished. If a pass swaps nothing, we stop.
Recursion (bridge concept)
If a list has 0 or 1 item, it is already sorted. If it is longer, Quick Sort parks one number (the pivot), then asks the same recipe to sort the left leftover and the right leftover. That self-call is recursion. The tiny easy case is the base case.
Divide and Conquer Sorting
Quick Sort
Where you've already done this: Imagine you're a teacher sorting exam papers into grade ranges. You pick one paper at random (say it scored 65), and you ask a student to help you split the pile: everything below 65 goes to the left, everything 65 and above goes to the right. Now the 65 paper is in its correct position. You repeat this process on the left pile and the right pile separately, picking a new "splitter" paper each time, until every pile has just one paper. That's Quick Sort. It picks a reference value (called the pivot), partitions the list around it, and then recursively sorts the two resulting halves.
Why the pivot matters so much: The entire performance of Quick Sort depends on the pivot choice. If you pick a pivot that splits the data roughly in half, each level of recursion cuts the problem size by 2, giving you about log₂(n) levels with n work at each level, for a total of O(n log n). But if you consistently pick the smallest or largest item as the pivot (which happens if the data is already sorted and you always choose the last element), one side gets everything and the other side gets nothing. That degrades the algorithm to O(n²), the same as Selection Sort or Bubble Sort. In practice, strategies like picking a random pivot or using the "median of three" (comparing the first, middle, and last items and choosing the median) make the worst case extremely unlikely.
Walk through it with real numbers: Start with [8, 3, 1, 7, 0, 10, 2], using the last element as the pivot.
| Step | Subarray | Pivot | Result after partitioning |
|---|---|---|---|
| 1 | [8, 3, 1, 7, 0, 10, 2] | 2 | [1, 0, 2, 7, 8, 10, 3] – 2 is now in its final position |
| 2a | [1, 0] | 0 | [0, 1] – left side sorted |
| 2b | [7, 8, 10, 3] | 3 | [3, 8, 10, 7] – 3 is in its final position |
| 3 | [8, 10, 7] | 7 | [7, 10, 8] – 7 is in its final position |
| 4 | [10, 8] | 8 | [8, 10] – done |
Final sorted result: [0, 1, 2, 3, 7, 8, 10]. Notice that steps 2a and 2b happen independently of each other. This is the divide-and-conquer structure: once the pivot is placed, the left and right sides are completely separate problems.
How fast (or slow) is it:
- Best and average case, O(n log n): When the pivot splits the data reasonably well, each level of recursion processes n items total, and there are about log₂(n) levels. For 1,000 items, that's roughly 10,000 operations instead of the 500,000 that O(n²) sorts require.
- Worst case, O(n²): Happens when the pivot is always the smallest or largest item (e.g., the list is already sorted and you pick the last element). Each partition only removes one element, so you get n levels of recursion instead of log n. In practice, randomised pivot selection makes this case negligibly rare.
- Space: O(log n) on average for the recursive call stack. Each recursive call needs a small amount of memory to remember where to return to. In the worst case, this grows to O(n).
How it compares to the O(n²) sorts: Quick Sort is in a fundamentally different speed class. Selection Sort and Bubble Sort compare every pair of items, so doubling the data quadruples the work. Quick Sort's divide-and-conquer approach means doubling the data only slightly more than doubles the work. For 10,000 items, Bubble Sort makes roughly 50 million comparisons; Quick Sort makes roughly 130,000. That's a 400x difference, and it gets wider as data grows.
Quick Sort is not stable: Like Selection Sort, Quick Sort can change the relative order of equal items during partitioning. If you sort student records by grade and two students both scored 85, their original order may not be preserved. This is why Python chose Timsort (a stable O(n log n) sort) for its built-in sorted() function instead of Quick Sort. Quick Sort's strength is raw average-case speed with minimal extra memory, which is why C's standard library qsort() and many database engines use it.
Where you'll actually see it used: Quick Sort (or variants of it) powers the default sort in C, C++, Java (for primitive types), and many database query engines. It's favoured in systems where average-case speed matters most and memory is limited, because it sorts "in place" without creating a full copy of the data. The Linux kernel uses a Quick Sort variant for certain internal sorting tasks. It is also a foundational interview topic because it combines recursion, partitioning, and complexity analysis into one algorithm.
Common mistakes to watch for: (1) Always picking the last element as the pivot without considering the input data. On already-sorted lists, this produces the O(n²) worst case. Use random pivot selection or median-of-three to avoid it. (2) Confusing the partition() function with the quick_sort() function. Partition does the actual rearranging; quick_sort just calls partition and then recurses on the two halves. (3) Forgetting the base case in the recursion. If you don't stop when low >= high, the function recurses forever and crashes with a stack overflow.
Flowchart
flowchart TD
Start([Start]) --> A["Pick the last item
as your 'pivot'"]
A --> B["Mark a boundary
before the first item"]
B --> C["Look at each item
one by one"]
C --> D{"Is this item
smaller than\nthe pivot?"}
D -- Yes --> E["Move it to the left
of the boundary"]
D -- No --> F["Leave it where it is"]
E --> G{"More items
to check?"}
F --> G
G -- Yes --> C
G -- No --> H["Place the pivot at
the boundary"]
H --> I["Repeat everything on
the left group"]
I --> J["Repeat everything on
the right group"]
J --> Sorted[("All groups sorted!")]
Sorted --> Stop([Stop])
Algorithm steps
- Pick the last item as the pivot, the "judge" that every other item will be compared against.
- Mark a boundary (also called a "wall") just before the first item. This wall separates the "smaller than pivot" zone (left of it) from the "bigger than pivot" zone (right of it). At the start, nothing is sorted yet, so the wall sits before item 0.
- Scan every item (except the pivot):
- Smaller than pivot? Move it to the left of the wall (swap it with the item at the wall, then push the wall one step right).
- Bigger or equal? Leave it where it is. It's already on the right side of the wall.
- Place the pivot at the wall. It's now in its final sorted position.
- Repeat steps 1–4 on the left side (smaller items).
- Repeat steps 1–4 on the right side (larger items).
- Done when every section has 0 or 1 items. The whole list is sorted.
🧠 The Brain Map: How to code this
- The Partition: Pick the last item as the
pivot. Use pointerias the "wall". It marks where the "smaller than pivot" section ends. Starts atlow - 1(before the first item). - The Scan: As we loop (
j), if a number is smaller than the pivot, we push the wall right (i += 1) and swap that item into the wall's new position. This way, everything left ofistays smaller than the pivot. - The Recursion: Once the pivot is locked in place, the function calls itself to sort the left and right sides.
Python code (optional)
Teaching vs picture: The Python uses <= when splitting around the pivot. The moving bars use a strict <. Same idea; tiny difference on equal numbers. Beginners can ignore this.
Help: Quick Sort
- Base case: 0 or 1 item is already “sorted.” That is why recursion stops.
- Pivot lands in its final seat, then we sort left and right the same way.
- If you are lost, re-read the Recursion bridge above this lesson.
Crimson = pivot. After it sits, we sort the left group and the right group the same way.
Divide and Conquer Sorting
Merge Sort
Where you've already done this: Two teaching assistants each hold a pile of exam papers already sorted by score. You merge them into one sorted pile by always taking the smaller front paper from either pile. Merge Sort does the same on numbers: split until each piece has one item, then merge sorted halves.
Walk through [38, 27, 43, 3]:
| Phase | What happens | Result |
|---|---|---|
| Split | [38,27,43,3] → [38,27] | [43,3] → … → singles | [38][27][43][3] |
| Merge up | [27,38] and [3,43] — compare fronts | [3,27,38,43] |
vs Quick Sort: Merge Sort guarantees O(n log n) even on already-sorted data, stays stable (equal items keep their order), but needs O(n) extra space for merge buffers. Quick Sort sorts in place with less memory but can hit O(n²) on bad pivots.
Flowchart
flowchart TD
Start([List longer than 1?]) --> Split["Split in half"]
Split --> Left["Sort left half"]
Split --> Right["Sort right half"]
Left --> Merge["Merge two sorted halves"]
Right --> Merge
Merge --> Done([Sorted])
🧠 The Brain Map: How to code this
- Base case:
low >= high— 0 or 1 item, already sorted. - Split:
mid = (low + high) // 2, recurse on[low..mid]and[mid+1..high]. - Merge: Copy both halves to temp lists; walk with two pointers; write the smaller value back.
Python code (optional)
Teaching vs picture: Blue and yellow bars show the left and right halves during a split. Green flash during merge means we are writing the smaller front value.
Help: Merge Sort
- Split until size 1, then merge pairs, then merge bigger pairs.
- After
[4,1,3,2]splits into sorted[1,4]and[2,3], the next step is merge — not another pivot.
Blue = left half, yellow = right half during split. Merge compares fronts and writes the smaller bar.
Cheat Sheet
Everything above, in one scannable place.
You just learned eight algorithms and four data structures. Here's the whole thing on one page. Bookmark this for exam night. Time is how many steps it takes; space is how much extra memory it needs on top of the original list.
📦 Data Structures
| Structure | Add | Remove | Access pattern | Use it when… | Do not use when… |
|---|---|---|---|---|---|
| Stack | O(1) push | O(1) pop | LIFO (last in, first out) | You need to undo actions or track "return points" (like recursion). | |
| Queue | O(1) enqueue | O(n)* dequeue | FIFO (first in, first out) | Requests need to be handled fairly, in the order they arrived. | |
| Linked List | O(1) at head | O(n) by value | Chain of nodes, no random access | You insert/delete often and don't need to jump to a random position. | |
| Hash Map | O(1) avg put | O(1) avg* | Key → value via hash + chain | You look up by name/ID repeatedly (counts, visited sets, caches). |
* With Python's plain list. Use collections.deque for a true O(1) queue on both ends. Hash map delete is average O(1) when chains stay short.
🔎 Searching
| Algorithm | Best | Average | Worst | Space | Needs sorted? | Use it when… | Do not use when… |
|---|---|---|---|---|---|---|---|
| Linear Search | O(1) | O(n) | O(n) | O(1) | No | The list is small, unsorted, or you're only searching once. | The list is huge and already sorted — Binary is kinder. |
| Binary Search | O(1) | O(log n) | O(log n) | O(1) | Yes | The list is already sorted and you'll search it repeatedly. | The list is messy / unsorted. Binary can give a wrong answer. |
🗺️ Pathfinding (trees and grids)
| Algorithm | Best / typical | Worst | Space | Needs weights? | Heuristic? | Use it when… | Do not use when… |
|---|---|---|---|---|---|---|---|
| BFS | O(V+E) | O(V+E) | O(V) | No (equal hops) | No | You want fewest hops on an unweighted graph, tree, or grid. | Roads have different lengths in km — hops are not kilometers. |
| A* | O(E log V) | O(E log V) | O(V) | Optional (here every step costs 1) | Yes, must not overestimate | You have a good guess toward the goal (Manhattan / straight line). | Your guess brags (overestimates). Then the best path can be missed. |
🔀 Sorting
| Algorithm | Best | Average | Worst | Space | Stable? | Use it when… | Do not use when… |
|---|---|---|---|---|---|---|---|
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | No | The list is tiny, or minimizing the number of swaps matters most. | The list is huge and you need speed — use Quick Sort (later). |
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | Yes | You're teaching the concept, or the list is already nearly sorted. | Production code on big data — Python's sorted() is much faster. |
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | No | You need a fast, general-purpose sort on large amounts of data. | You do not understand recursion yet — finish Selection/Bubble first. |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes | You need guaranteed speed and stable ordering (e.g. sort by score, keep name order for ties). | Memory is extremely tight — merge needs an extra buffer. |
🧭 Still not sure which to pick?
- Need "last one in, first one out" (undo, back button)? → Stack.
- Need "first come, first served"? → Queue.
- Inserting/deleting constantly at head, size unknown ahead of time? → Linked List.
- Looking up items by key/name instantly? → Hash Map.
- List is sorted and you'll search it a lot? → Binary Search.
- List is small or unsorted, one-off search? → Linear Search is simpler and fine.
- Need fewest hops on a tree, maze, or unweighted graph? → BFS.
- Same path problem, but you have a good guess toward the goal? → A*.
- Sorting a huge list and speed matters? → Quick Sort (in place) or Merge Sort (stable, guaranteed).
- Need stable O(n log n) or worst-case guarantees? → Merge Sort.
- Just learning how sorting works, or the list is tiny? → Selection or Bubble Sort.
Knowledge Check
Eighteen short situations. Wrong answers point you back to the lesson.
Keys 1–4 pick a choice.
Glossary
📚 Bento ReferenceInteractive Bento Dictionary — every concept, term, and complexity in one place.
Instant lookup for all computing terminology. Use the live search bar or category filters to explore.