🏠 Your DSA Lab

A living notebook β€” 22 topics, each with plain-English concepts, key terms, real code, and an interactive animation you can drive.

How to use this

Work top to bottom down the left sidebar β€” the topics are ordered the way a DSA course teaches them, easiest to hardest. On each page: read the concepts, skim the key terms, then drive the animation (hit Step and predict what happens next) while reading the code. The goal isn't to memorize β€” it's to see the data move. Stuck on anything? Ask the Study Buddy on the right; it knows which page you're on.

Your progress: you coded Stack βœ… and Queue βœ… from scratch. BFS is your current focus β€” and it runs on the very queue you built. Everything else is ready when you are.

The learning path

Seven phases, ordered by difficulty β€” each one builds on the last. Click any topic to jump there.

1 Β· Foundations

⏱️ Complexity πŸ“ Math

2 Β· Linear structures

πŸ”’ Arrays πŸ”— Linked Lists πŸ“š Stack βœ“ 🎟️ Queue βœ“

3 Β· Recursion & sorting

πŸ” Recursion πŸ” Searching πŸ—ƒοΈ Sorting

4 Β· Non-linear structures

πŸ—‚οΈ Hash Tables 🌳 Trees ⛰️ Heaps 🌲 Tries

5 Β· Graphs

πŸ•ΈοΈ Graphs 🌊 BFS NOW

6 Β· Algorithm paradigms

🌴 Backtracking πŸͺ™ Greedy 🧩 Dynamic prog. 🧠 Patterns

7 Β· Extras & rigor

πŸ”Ÿ Bit manipulation βœ… Correctness
The through-line: the containers come first (arrays, lists, stacks, queues) because the big algorithms are just those containers doing a job β€” BFS is a queue exploring a graph, DFS is a stack, Dijkstra is a heap. Learn the tool, then watch it work.

πŸ“š Stack

LIFO β€” Last In, First Out. Think of a stack of plates.

The idea

You can only add to or remove from the top. The last thing you pushed is the first thing you pop. Undo buttons, browser back, and function call stacks all work this way.

Interactive stack
Empty stack

Key terms

push
Add an element to the top of the stack.
pop
Remove and return the top element.
peek / top
Look at the top element without removing it.
LIFO
Last In, First Out β€” the ordering guarantee.
overflow / underflow
Pushing to a full stack / popping from an empty one.

Sample code

Python
JavaScript
# A Python list already works as a stack
stack = []
stack.append(1)   # push
stack.append(2)
stack.append(3)
top = stack[-1]      # peek -> 3
x = stack.pop()      # pop  -> 3
print(stack)        # [1, 2]
const stack = [];
stack.push(1);        // push
stack.push(2);
stack.push(3);
const top = stack[stack.length - 1]; // peek -> 3
const x = stack.pop();  // pop  -> 3
console.log(stack); // [1, 2]
Complexity: push, pop, peek are all O(1).

🎟️ Queue

FIFO β€” First In, First Out. Think of a line at a ticket counter.

The idea

You add at the back (enqueue) and remove from the front (dequeue). The first one in line is the first one served. This is the engine of BFS β€” keep it fresh.

Interactive queue
Empty queue

Key terms

enqueue
Add an element to the back of the queue.
dequeue
Remove and return the front element.
front / rear
The two ends: you read from the front, write to the rear.
FIFO
First In, First Out β€” the ordering guarantee.

Sample code

Python
JavaScript
from collections import deque

q = deque()
q.append("a")     # enqueue (add to rear)
q.append("b")
first = q.popleft() # dequeue -> "a"  (remove from front)
# deque gives O(1) at BOTH ends β€” always use it, not a list
// A simple queue with an array (fine for learning)
const q = [];
q.push("a");         // enqueue
q.push("b");
const first = q.shift(); // dequeue -> "a"
Gotcha: in Python, don't use a plain list and pop(0) for a queue β€” that's O(n). Use collections.deque for O(1) at both ends.

🌊 Breadth-First Search NOW

Explore a graph level by level, using a queue to remember who's next.

Lesson 1 β€” What BFS actually does

Imagine dropping a stone in a pond. Ripples spread out in rings: first everything 1 step away, then everything 2 steps away, and so on. BFS is that ripple. Starting from one node, it visits all its neighbors first, then their neighbors, expanding outward one "ring" (level) at a time.

One-sentence version: BFS visits nodes in order of their distance from the start β€” closest first.

Lesson 2 β€” Why a queue?

To go level by level, you must handle nodes in the order you discovered them. That's exactly FIFO. When you visit a node, you enqueue all its undiscovered neighbors at the back. Because the queue is first-in-first-out, you always finish the current ring before starting the next one. Swap the queue for a stack and you'd get depth-first search instead β€” same skeleton, different container.

The queue holds the frontier: the nodes discovered but not yet explored. It naturally sorts them by distance for you.

Lesson 3 β€” Watch it run πŸ‘‡

Press Step to advance one action at a time (best for learning), or Play to auto-run. Watch how the current node's neighbors get pushed into the queue, and how the queue empties from the front. Hit New example β€³ to try BFS on a different graph.

unseen in queue current visited

Queue (front β†’ back)

empty

Visited (in order)

none yet

What just happened

Press β€œStep” to start BFS from node A.
step 0

Lesson 4 β€” The algorithm, step by step

Every BFS is the same five moves:

  1. Put the start node in the queue and mark it visited.
  2. Dequeue a node from the front β€” call it current.
  3. Look at each neighbor of current.
  4. If a neighbor is not visited yet, mark it visited and enqueue it.
  5. Repeat from step 2 until the queue is empty.
Critical detail: mark a node visited when you enqueue it, not when you dequeue it. Otherwise the same node can get added to the queue multiple times before it's ever processed.

Key terms

graph
A set of nodes (vertices) connected by edges. BFS runs on graphs (a tree is just a special graph).
node / vertex
A single point in the graph (A, B, C…).
edge
A connection between two nodes.
neighbor / adjacent
A node directly connected by one edge.
adjacency list
How we store the graph: a map from each node to its list of neighbors. {A:[B,C], ...}
visited set
The nodes we've already discovered β€” prevents infinite loops on cycles.
frontier
The nodes currently in the queue: discovered but not yet explored.
level / depth
Distance (number of edges) from the start node. BFS finishes one level before the next.

Sample code

Python
JavaScript
from collections import deque

def bfs(graph, start):
    visited = {start}          # mark start visited
    queue   = deque([start])   # frontier starts with the source
    order   = []               # the visiting order (for us to see)

    while queue:                # until the frontier is empty...
        node = queue.popleft() # dequeue the FRONT (FIFO)
        order.append(node)

        for nb in graph[node]:    # each neighbor
            if nb not in visited:
                visited.add(nb)     # mark on ENQUEUE, not dequeue
                queue.append(nb)    # add to the back
    return order

graph = {
    "A": ["B", "C"], "B": ["A", "D", "E"], "C": ["A", "F"],
    "D": ["B"], "E": ["B", "F"], "F": ["C", "E", "G"], "G": ["F"],
}
print(bfs(graph, "A"))  # ['A', 'B', 'C', 'D', 'E', 'F', 'G']
function bfs(graph, start) {
  const visited = new Set([start]); // mark start visited
  const queue   = [start];          // frontier
  const order   = [];

  while (queue.length) {            // until frontier empty
    const node = queue.shift();     // dequeue front (FIFO)
    order.push(node);

    for (const nb of graph[node]) { // each neighbor
      if (!visited.has(nb)) {
        visited.add(nb);            // mark on ENQUEUE
        queue.push(nb);             // add to back
      }
    }
  }
  return order;
}

Why it matters

Shortest path (unweighted): because BFS reaches nodes closest-first, the first time it touches a node is along a shortest path. Store a parent pointer when you enqueue and you can rebuild that path. This is the #1 reason BFS shows up in interviews and real systems.

Complexity

AspectCostWhy
TimeO(V + E)Each vertex dequeued once, each edge examined once.
SpaceO(V)Visited set + queue can hold up to all vertices.

BFS vs DFS β€” the one-line difference

🌊 BFS β€” uses a Queue

Explores level by level (widest first). Finds shortest paths in unweighted graphs. Container: FIFO queue.

πŸ•³οΈ DFS β€” uses a Stack

Explores as deep as possible before backtracking. Great for cycles, topological sort. Container: LIFO stack (or recursion).

Mind-blow: BFS and DFS are the same algorithm β€” the only difference is queue vs stack. That's why we did stacks and queues first.

⏱️ Complexity Analysis

How we measure how fast (time) and how memory-hungry (space) an algorithm is β€” as the input grows.

The idea

We don't measure algorithms in seconds β€” hardware varies. Instead we count how the number of operations grows as the input size n grows. That growth rate is written in Big-O notation. An O(n) algorithm does twice the work when the input doubles; an O(nΒ²) one does four times the work.

One-liner: Big-O describes the shape of the growth, dropping constants and small terms β€” O(2n + 5) is just O(n).
Watch growth rates diverge β€” drag n
Numbers on the right are operation counts. Bars are log-scaled so all of them fit β€” notice how O(nΒ²) and O(2ⁿ) explode while O(log n) barely moves.

The common classes (fastest β†’ slowest)

Big-ONameExample
O(1)ConstantArray index, hash lookup, stack push
O(log n)LogarithmicBinary search, balanced-tree / heap op
O(n)LinearScanning a list, BFS/DFS over V+E
O(n log n)LinearithmicGood sorts (merge, heap, quick-avg)
O(nΒ²)QuadraticNested loops, bubble sort
O(2ⁿ)ExponentialBrute-force subsets, naive recursion

Key terms

Big-O
Upper bound β€” the worst-case growth rate. The one you'll quote most.
Big-Θ (theta)
Tight bound β€” growth that's both an upper and lower bound.
Big-Ξ© (omega)
Lower bound β€” the best case.
time vs space
Time = operations performed. Space = extra memory used. You often trade one for the other.
amortized
Average cost per op over a sequence. A dynamic array's push is O(1) amortized even though occasional resizes cost O(n).
best / avg / worst
The same algorithm can behave differently on lucky vs unlucky inputs (e.g. quicksort).
How to read code for Big-O: one loop over n β†’ O(n). A loop inside a loop β†’ O(nΒ²). Halving the problem each step β†’ O(log n). Then drop constants and lower-order terms.

πŸ”’ Arrays & Strings

Contiguous, index-addressed memory β€” the most fundamental structure. Strings are just arrays of characters.

The idea

An array stores elements back-to-back in memory, each reachable by an index (0-based). Because the computer can jump straight to start + index, reading any element is O(1). The catch: inserting or removing in the middle or front means shifting everything after it β€” O(n).

Interactive array
Access by index is instant. Insert-at-front shifts everything β†’ O(n).

Key terms

index
The position of an element, starting at 0. a[0] is the first.
contiguous
Stored in one unbroken block of memory β€” what makes O(1) access possible.
dynamic array
Auto-resizing array (Python list, JS Array). push is O(1) amortized.
in-place
Modifying the array without allocating a copy β€” O(1) extra space.
two pointers
A common technique: walk two indices toward each other (or at different speeds) to solve problems in one pass.

Sample code

Python
JavaScript
a = [10, 20, 30, 40]
a[2]            # O(1) access -> 30
a.append(50)     # O(1) amortized push
a.insert(0, 5)   # O(n) β€” shifts everything right
20 in a         # O(n) search (unsorted)

# two-pointer: reverse in place
i, j = 0, len(a) - 1
while i < j:
    a[i], a[j] = a[j], a[i]
    i += 1; j -= 1
const a = [10, 20, 30, 40];
a[2];             // O(1) -> 30
a.push(50);       // O(1) amortized
a.unshift(5);     // O(n) β€” shifts everything
a.includes(20);   // O(n) search

Complexity

OperationCost
Access by indexO(1)
Search (unsorted)O(n)
Push / pop at endO(1) amortized
Insert / delete at front or middleO(n) β€” shift the rest
Strings are arrays of characters, and in Python/JS they're immutable β€” "editing" builds a new string. So concatenating in a loop is O(nΒ²); build a list and join instead.

πŸ”— Linked Lists

Nodes scattered in memory, each pointing to the next. No indices β€” you follow the chain.

The idea

A linked list is a chain of nodes. Each node holds a value and a pointer to the next node. There's no contiguous block, so no O(1) index jump β€” to reach the 5th node you walk 5 links (O(n)). The upside: inserting or deleting at a known spot is O(1) β€” just rewire pointers, no shifting.

Interactive singly-linked list
Traverse walks the chain one pointer at a time β€” O(n).

Key terms

node
One link in the chain β€” holds a value plus a pointer.
pointer / next
Reference from one node to the following node.
head / tail
The first node / the last node (whose next is null).
null
Marks the end of the list β€” the last node points to nothing.
singly vs doubly
Singly = points forward only. Doubly = each node also points back, so you can walk both ways.

Sample code

Python
JavaScript
class Node:
    def __init__(self, val):
        self.val = val
        self.next = None

# build 1 -> 2 -> 3
head = Node(1)
head.next = Node(2)
head.next.next = Node(3)

# traverse β€” O(n)
node = head
while node:
    print(node.val)
    node = node.next

# insert at head β€” O(1)
new = Node(0)
new.next = head
head = new
class Node {
  constructor(val) { this.val = val; this.next = null; }
}
let head = new Node(1);
head.next = new Node(2);

// traverse β€” O(n)
let node = head;
while (node) { console.log(node.val); node = node.next; }

Complexity

OperationCost
Access / search the k-th nodeO(n) β€” walk the chain
Insert / delete at headO(1)
Insert / delete at a known nodeO(1) β€” rewire pointers
Array vs linked list: arrays win on random access (O(1) index) and cache friendliness; linked lists win on cheap insert/delete without shifting. Pick based on which operation dominates.

πŸ—‚οΈ Hash Tables & Sets

Near-instant lookup by key. A hash function turns a key into a bucket index.

The idea

A hash table stores key→value pairs in an array of buckets. A hash function converts a key into a bucket index in O(1), so lookups, inserts, and deletes are O(1) on average. When two keys land in the same bucket (a collision), we store both there and scan the short list. A set is the same thing minus the values — it just remembers which keys are present.

Hash a key into a bucket
Type a word and insert β€” watch it hash to a bucket.

Key terms

hash function
Turns any key into a bucket index, fast and deterministically.
bucket
One slot in the backing array where entries land.
collision
Two keys hashing to the same bucket. Handled by chaining (a list per bucket).
load factor
entries Γ· buckets. When it gets high, the table resizes to keep lookups fast.
set vs map
A set stores keys only (membership). A map/dict stores key→value pairs.

Sample code

Python
JavaScript
# dict = hash map, set = hash set
prices = {"apple": 3, "pear": 5}
prices["apple"]        # O(1) avg lookup -> 3
prices["kiwi"] = 2      # O(1) insert
"pear" in prices        # O(1) membership

seen = set()
seen.add("cat")          # O(1)
"cat" in seen           # O(1)
const prices = new Map([["apple", 3]]);
prices.get("apple");   // O(1) -> 3
prices.set("kiwi", 2);
prices.has("apple");   // O(1)

const seen = new Set();
seen.add("cat");
seen.has("cat");        // O(1)

Complexity

OperationAverageWorst
Insert / lookup / deleteO(1)O(n) (everything collides)
Gotcha: hash tables don't keep insertion order meaningfully sorted, and the O(1) is average β€” a bad hash or adversarial keys can degrade to O(n). This is exactly the visited-set that makes BFS efficient!

🌳 Trees

Hierarchical data β€” a root branching into children, with no cycles.

The idea

A tree has a root at the top; every node has one parent and any number of children. A binary tree allows at most two children (left/right). A binary search tree (BST) keeps them ordered β€” everything left of a node is smaller, everything right is larger β€” which makes search O(log n) when the tree is balanced.

Traverse a binary search tree
Pick a traversal. Notice in-order visits a BST in sorted order.

Key terms

root / leaf
Top node with no parent / bottom node with no children.
parent / child
Direct up/down relationships between connected nodes.
depth / height
Depth = distance from root. Height = longest path down to a leaf.
binary tree
Each node has at most two children (left, right).
BST
Binary search tree: left < node < right, giving O(log n) search when balanced.
balanced
Height stays ~log n. Unbalanced, a BST degrades into a linked list (O(n)).

Sample code

Python
JavaScript
class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

def inorder(node):
    if not node: return
    inorder(node.left)    # go left
    print(node.val)       # visit
    inorder(node.right)   # go right
# On a BST, in-order prints values sorted.
class TreeNode {
  constructor(val) { this.val = val; this.left = this.right = null; }
}
function inorder(node) {
  if (!node) return;
  inorder(node.left);      // go left
  console.log(node.val);  // visit
  inorder(node.right);     // go right
}

Complexity (balanced BST)

OperationBalancedWorst (degenerate)
Search / insert / deleteO(log n)O(n)
Balance is the whole game. An unbalanced BST becomes a linked list. Self-balancing variants (AVL, red-black) guarantee O(log n). Heaps and tries are specialized trees too.

🌲 Tries (Prefix Trees)

A tree of characters for storing strings β€” built for lightning-fast prefix search (autocomplete).

The idea

A trie stores words character-by-character down a tree. Words that share a prefix share the same path β€” "car" and "cat" share c β†’ a. Looking up a word or checking a prefix takes O(L) where L is the word's length β€” independent of how many words are stored. That's why it powers autocomplete and spell-check.

Build & search a trie
Words sharing a prefix share a path. Type in the search box to light up the matching path.

Key terms

prefix tree
Another name for a trie β€” the path from root spells out a prefix.
node per character
Each edge is one character; each node has a map of child characters.
end-of-word marker
A flag on a node saying "a complete word ends here" (● in the demo).
prefix search
The trie's superpower: check if any word starts with a prefix in O(L).

Sample code

Python
JavaScript
class Trie:
    def __init__(self):
        self.root = {}          # nested dicts of chars

    def insert(self, word):     # O(L)
        node = self.root
        for ch in word:
            node = node.setdefault(ch, {})
        node["$"] = True     # end-of-word

    def starts_with(self, prefix):  # O(L)
        node = self.root
        for ch in prefix:
            if ch not in node: return False
            node = node[ch]
        return True
class Trie {
  constructor() { this.root = {}; }
  insert(word) {                // O(L)
    let node = this.root;
    for (const ch of word)
      node = node[ch] ??= {};
    node["$"] = true;           // end-of-word
  }
  startsWith(prefix) {           // O(L)
    let node = this.root;
    for (const ch of prefix) {
      if (!(ch in node)) return false;
      node = node[ch];
    }
    return true;
  }
}

Complexity

OperationCost
Insert / search word / prefix checkO(L) β€” L = word length
Spacecan be large β€” up to one node per character stored
Tries trade memory for speed. Independent of the dictionary size, a lookup only costs the length of the word you're checking.

πŸ•ΈοΈ Graphs

Nodes connected by edges β€” the most general structure. Trees and linked lists are special cases.

The idea

A graph is a set of vertices (nodes) joined by edges. Edges can be directed (one-way, like Twitter follows) or undirected (mutual, like Facebook friends), and can carry weights (distance, cost). We usually store a graph as an adjacency list: a map from each node to its neighbors. BFS and DFS are how you explore one.

Click a node to see its neighbors

Adjacency list

Click any node β€” it and its direct neighbors light up, and its adjacency-list row highlights.

Key terms

vertex / edge
A node / a connection between two nodes.
directed vs undirected
Edges one-way (A→B) vs mutual (A—B).
weighted
Edges carry a number (distance, cost). Dijkstra handles these.
adjacency list
Map of node β†’ list of neighbors. Compact for sparse graphs.
adjacency matrix
A VΓ—V grid; edge lookup is O(1) but space is O(VΒ²).
degree
How many edges touch a node.
path / cycle
A route through edges / a path that returns to its start.

Sample code

Python
JavaScript
# adjacency list (undirected)
graph = {
    "A": ["B", "C"],
    "B": ["A", "D"],
    "C": ["A", "D"],
    "D": ["B", "C"],
}
graph["A"]          # neighbors of A -> ["B", "C"]

# weighted: store (neighbor, weight) pairs
wgraph = {"A": [("B", 5), ("C", 2)]}
// adjacency list (undirected)
const graph = {
  A: ["B", "C"],
  B: ["A", "D"],
  C: ["A", "D"],
  D: ["B", "C"],
};
graph.A;            // -> ["B", "C"]

Complexity

RepresentationSpaceEdge lookup
Adjacency listO(V + E)O(degree)
Adjacency matrixO(VΒ²)O(1)
Explore a graph with BFS or DFS β€” both are O(V + E). Head to the 🌊 BFS page to watch a traversal ripple out level by level.

⛰️ Heaps & Priority Queues

A tree that always keeps the smallest (or largest) item at the top β€” O(log n) to add or remove it.

The idea

A min-heap is a complete binary tree where every parent is ≀ its children β€” so the minimum is always at the root. It's stored compactly in an array: for index i, children sit at 2i+1 and 2i+2. Adding an item (sift-up) or removing the top (sift-down) is O(log n) because you only travel one path up or down the tree's height. A priority queue is just a heap with a friendly name.

Min-heap β€” array & tree stay in sync
backing array (index 0 = root)
Insert a value and watch it bubble up to its correct spot.

Key terms

heap property
Every parent ≀ its children (min-heap) β€” so the min is always the root.
min-heap / max-heap
Smallest on top / largest on top.
sift-up / sift-down
Restore the heap property after an insert (bubble up) or removal (bubble down).
complete binary tree
Every level full except possibly the last, filled left-to-right β€” lets us use a flat array.
priority queue
The abstract "always give me the highest-priority item" idea; a heap is how it's built.

Sample code

Python
JavaScript
import heapq

h = []
heapq.heappush(h, 5)   # O(log n)
heapq.heappush(h, 2)
heapq.heappush(h, 8)
heapq.heappush(h, 1)
h[0]                   # peek min -> 1  (O(1))
heapq.heappop(h)       # remove min -> 1 (O(log n))
# max-heap trick: push negated values (-v)
// JS has no built-in heap β€” hand-roll or use a library.
// Concept: array where parent(i) = (i-1)>>1,
// children of i = 2i+1 and 2i+2.
class MinHeap {
  constructor() { this.a = []; }
  push(v) { this.a.push(v); this._up(this.a.length - 1); }
  peek() { return this.a[0]; }   // O(1)
  // _up sifts up; pop() swaps root & last, then sifts down
}

Complexity

OperationCost
Peek min/maxO(1)
Insert (push)O(log n)
Extract min/max (pop)O(log n)
Build heap from n itemsO(n)
Where heaps show up: Dijkstra's shortest path (weighted BFS), heap-sort, and "top-K" problems β€” anywhere you repeatedly need the smallest/largest remaining item.

πŸ“ Mathematical Foundations

The handful of math ideas that show up in almost every algorithm.

Big-O β€” measuring growth, not seconds

Big-O describes how an algorithm's work grows as the input n gets large. We ignore constants and small terms and keep the dominant one β€” O(nΒ²+3n) becomes O(nΒ²). It answers "if I double the input, what happens to the work?"

Logarithms β€” the "how many times can I halve it" number

logβ‚‚(n) is how many times you divide n by 2 before reaching 1. For a million items that's only ~20. Every time an algorithm halves its problem each step, you get an O(log n) β€” that's why binary search and balanced trees are so fast.

Feel it: logβ‚‚(1,000,000) β‰ˆ 20, logβ‚‚(1,000,000,000) β‰ˆ 30. Logarithmic algorithms barely notice huge inputs.

Modular arithmetic & GCD

Modulo (%) is remainder after division β€” "clock math" that wraps around (you used it in the circular queue!). GCD (greatest common divisor) is found fast with Euclid's algorithm: repeatedly replace the pair with (smaller, remainder).

Euclid's algorithm β€” GCD by repeated remainder (modulo in action)
Replace (a, b) with (b, a mod b) until b = 0. The last non-zero a is the GCD.

Key terms

Big-O
Upper bound on growth as n β†’ ∞. The vocabulary for comparing algorithms.
logarithm
Inverse of exponentiation. logβ‚‚(n) = number of halvings to reach 1.
modulo (%)
Remainder after division. Wraps values into a fixed range [0, m).
GCD
Greatest common divisor of two integers, via Euclid's algorithm.
factorial / nCr
Counting arrangements (n!) and combinations (n choose r) β€” the basis of combinatorics.

Sample code

Python
def gcd(a, b):        # Euclid's algorithm β€” O(log(min(a,b)))
    while b:
        a, b = b, a % b   # replace with (b, remainder)
    return a

print(gcd(48, 36))    # 12

# log intuition: how many halvings to reach 1?
n, steps = 1000, 0
while n > 1:
    n //= 2; steps += 1
print(steps)           # 10  (β‰ˆ log2(1000))
Where it clicks: the log n in binary search, heaps, and balanced trees all come straight from this halving idea.

πŸ”Ÿ Bit Manipulation

Working directly with the binary bits of a number β€” fast and memory-cheap.

The idea

Integers are stored as binary (13 = 1101). Bitwise operators act on those bits in parallel, in a single CPU instruction. That makes them extremely fast for flags, sets, and low-level tricks.

The operators

OpNameDoes
&AND1 only where both bits are 1
|OR1 where either bit is 1
^XOR1 where bits differ
~NOTflips every bit
<< / >>shiftslide bits left/right (Γ—2 or Γ·2)
Click any bit to flip it β€” the operators update live
A = 12
B = 10
XOR (^) lights up exactly where A and B differ β€” flip bits until the two rows match and watch it go all zeros.

Key terms

bit / mask
A single 0/1; a mask is a number used to isolate or set specific bits.
set / clear / toggle
Turn a bit on (x | 1<<i), off (x & ~(1<<i)), or flip it (x ^ 1<<i).
x & (x-1)
Clears the lowest set bit β€” the classic trick for counting 1-bits.
XOR trick
a ^ a = 0, so XOR-ing everything cancels pairs and leaves the unique value.

Sample code

Python
x = 0b1010            # 10
is_set = (x >> 1) & 1   # check bit 1 -> 1
x |= (1 << 2)         # set bit 2   -> 1110 (14)
x &= ~(1 << 1)       # clear bit 1 -> 1100 (12)

# find the element that appears once (all others twice)
def unique(nums):
    r = 0
    for v in nums: r ^= v   # pairs cancel to 0
    return r
print(unique([4,1,4,6,1]))  # 6
Why it's beautiful: the XOR "find the unique" trick uses O(1) memory and one pass β€” no hash map needed.

πŸ” Searching Algorithms

Finding an element β€” from a brute-force scan to O(log n) binary search.

Linear search β€” check everything

Walk the collection one item at a time until you find the target (or run out). Works on any data, sorted or not. Simple, but O(n).

Binary search β€” halve the haystack each step

If the data is sorted, compare the target to the middle element and throw away half the range every step. That halving is the log from the math page β†’ O(log n). A million items in ~20 comparisons.

The one precondition: binary search only works on sorted data. On unsorted input it gives wrong answers β€” sort first (or use linear search).
Binary search β€” watch half the array vanish each step
Pick a target and step. lo/hi bound the range; mid is checked each step; the dimmed half is discarded.

Key terms

linear search
Scan front-to-back. O(n). No ordering required.
binary search
Repeatedly halve a sorted range. O(log n).
lo / mid / hi
The three pointers bounding the current search window.
invariant
"If the target exists, it's within [lo, hi]" β€” stays true every iteration.

Sample code

Python
def binary_search(a, target):   # a must be SORTED
    lo, hi = 0, len(a) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if a[mid] == target: return mid
        elif a[mid] < target: lo = mid + 1  # target is right half
        else:                 hi = mid - 1  # target is left half
    return -1                            # not found

print(binary_search([1,3,5,7,9], 7))  # 3

Complexity

AlgorithmTimeNeeds sorted?
Linear searchO(n)no
Binary searchO(log n)yes

πŸ—ƒοΈ Sorting Algorithms

Putting things in order β€” the workhorse behind search, dedup, grouping, and more.

The bar to beat: O(n log n)

Simple sorts (bubble, insertion, selection) compare every pair and cost O(nΒ²). The efficient ones (merge, quick, heap) hit O(n log n) β€” which is the best any comparison-based sort can do. That log n comes from repeatedly halving, just like binary search.

The three you should know

SortIdeaTimeNotes
InsertionBuild a sorted prefix one item at a timeO(nΒ²)Great on tiny / nearly-sorted data
MergeSplit in half, sort each, mergeO(n log n)Stable; needs extra memory
QuickPartition around a pivot, recurseO(n log n) avgIn-place; O(nΒ²) worst case
You've already built one! Your MergeSort.java is exactly the divide-and-conquer merge sort below β€” split, recurse, merge.
Bubble sort β€” the biggest value "bubbles" to the end each pass
Orange = being compared, green = locked in its final spot.

Key terms

comparison sort
Orders by comparing pairs. Lower bound is O(n log n).
stable
Equal elements keep their original relative order (merge sort is; quicksort isn't).
in-place
Sorts using O(1) extra memory (quicksort ~is; merge sort isn't).
pivot / partition
Quicksort's split point and the rearrange step around it.

Sample code

Python
def merge_sort(a):
    if len(a) <= 1: return a           # base case
    mid = len(a) // 2
    left  = merge_sort(a[:mid])          # divide + conquer
    right = merge_sort(a[mid:])
    return merge(left, right)             # combine

def merge(l, r):
    out, i, j = [], 0, 0
    while i < len(l) and j < len(r):
        if l[i] <= r[j]: out.append(l[i]); i += 1
        else:            out.append(r[j]); j += 1
    return out + l[i:] + r[j:]           # leftovers

print(merge_sort([5,2,9,1,6]))    # [1, 2, 5, 6, 9]

πŸ” Recursion & Divide-and-Conquer

A function that calls itself β€” and the strategy of splitting a problem into smaller copies of itself.

Recursion = base case + recursive case

A recursive function solves a big problem by calling itself on a smaller version, until it hits a base case simple enough to answer directly. Every call must move toward the base case, or it never stops.

#1 bug: a missing or unreachable base case β†’ infinite recursion β†’ stack overflow. Always ask: "what's the smallest case, and does each call shrink toward it?"

Divide and conquer

A recursion pattern in three moves: divide the problem into parts, conquer each part by recursing, then combine the results. Merge sort and binary search are the classic examples β€” and it's why they're O(n log n) and O(log n).

The call stack β€” factorial(n) piles up, then unwinds
Each call waits on a smaller one (the pile grows). The base case returns 1, then they resolve back up.

Key terms

base case
The stopping condition β€” solved directly, no further recursion.
recursive case
Where the function calls itself on smaller input.
call stack
The stack (!) of paused function calls waiting to finish. Deep recursion can overflow it.
divide & conquer
Divide β†’ recurse β†’ combine.

Sample code

Python
def factorial(n):
    if n <= 1:            # base case
        return 1
    return n * factorial(n - 1)   # recursive case β€” shrinks toward base

print(factorial(5))     # 120  (5*4*3*2*1)

# the call stack, top to bottom:
# factorial(5) -> 5 * factorial(4) -> 5*4 * factorial(3) -> ...
Connection: the "call stack" is a real stack β€” the same LIFO structure from the Stack page. That's also why DFS can be written with recursion or an explicit stack.

🌴 Backtracking

Try a choice, recurse, and undo it if it leads to a dead end.

The idea: explore a tree of choices

Many problems are "pick options one at a time" β€” arrange these items, fill this board, choose this subset. Backtracking walks that tree of choices with one rhythm: choose β†’ explore (recurse) β†’ un-choose. If a branch can't lead to a solution, you back up (backtrack) and try the next option.

Pruning β€” skip doomed branches early

The power move is checking constraints as you go: the moment a partial choice is already invalid (two queens attack, a sum exceeds the target), abandon that branch instead of building it out. Good pruning is the difference between fast and hopeless.

Backtracking is just DFS over the space of possible choices β€” depth-first dive, undo, try the next branch. You already know the engine.
Backtracking β€” build every subset of [1, 2, 3]
current path
subsets found
none yet
Watch the rhythm: choose β†’ explore β†’ un-choose. Every dead-end path backs up and tries the next branch.

Key terms

choice
One decision at the current step (place a queen, add an item).
constraint
A rule a valid solution must satisfy.
prune
Abandon a branch early once it can't possibly work.
un-choose
Undo the last choice before trying the next β€” restores state.

Sample code

Python
# all subsets of [1,2,3] via choose / explore / un-choose
def subsets(nums):
    out, path = [], []
    def dfs(i):
        if i == len(nums):
            out.append(path[:])      # a complete choice
            return
        path.append(nums[i])         # CHOOSE to include
        dfs(i + 1)                  # EXPLORE
        path.pop()                   # UN-CHOOSE
        dfs(i + 1)                  # explore excluding it
    dfs(0)
    return out

print(subsets([1,2,3]))     # [], [3], [2], [2,3], [1], ...

πŸͺ™ Greedy Algorithms

Take the best-looking option right now, and never look back.

The idea

At each step, make the choice that looks best locally β€” and commit to it. No undoing, no exploring alternatives. It's fast and simple. The catch: a sequence of locally-best choices isn't always globally best, so greedy only works on problems with the right structure.

When greedy is actually correct

You need two properties: greedy-choice property (a locally optimal choice is part of some global optimum) and optimal substructure (an optimal solution contains optimal solutions to subproblems). If you can't argue those, greedy may silently give a wrong answer.

Greedy fails example: coins [1, 3, 4], make 6. Greedy grabs 4+1+1 = 3 coins; the real optimum is 3+3 = 2 coins. Always prove greedy, or use DP.
Greedy coin change β€” grab the biggest coin that fits, repeat
Greedy takes the largest coin ≀ what's left, over and over. Try the tricky set to see it miss the optimum.

Key terms

greedy choice
The locally-best option taken at each step, never revisited.
optimal substructure
Optimal solutions are built from optimal sub-solutions.
exchange argument
The usual proof: show any optimal solution can be swapped toward the greedy one without getting worse.

Sample code

Python
# interval scheduling: most non-overlapping meetings
def max_meetings(intervals):
    intervals.sort(key=lambda x: x[1])  # greedy: earliest FINISH first
    count, end = 0, float('-inf')
    for s, e in intervals:
        if s >= end:          # doesn't overlap the last pick
            count += 1; end = e
    return count

print(max_meetings([(1,3),(2,5),(4,6)]))  # 2
Greedy vs DP: both use optimal substructure, but greedy commits immediately while DP tries all options and remembers the best. When greedy is provably right, it's faster.

🧩 Dynamic Programming

Break a problem into overlapping subproblems, solve each once, and remember the answer.

DP = recursion + memory

When a recursion solves the same subproblem over and over, you're wasting work. DP fixes that: compute each subproblem once and store its answer, so later calls just look it up. That turns exponential blowup into linear.

DP applies when both are true: (1) overlapping subproblems β€” the same smaller problems recur, and (2) optimal substructure β€” the best answer is built from best sub-answers.

Two styles

Top-down (memoization): write the natural recursion, add a cache. Bottom-up (tabulation): fill a table from the smallest subproblems up. Same answers, same complexity β€” pick whichever reads clearer.

Bottom-up DP β€” fill the Fibonacci table, each cell = sum of the two before it
dp[0]=0, dp[1]=1 are the base cases. Every other cell just adds the previous two β€” computed once, never repeated.

Key terms

subproblem / state
A smaller instance, identified by the inputs that matter (the "state").
overlapping subproblems
The same subproblem is needed many times β€” the signal for DP.
memoization
Top-down: cache results of the recursion.
tabulation
Bottom-up: fill a table from base cases upward.

Sample code

Python
from functools import lru_cache

# naive fib recomputes the same values -> O(2^n)
# memoized: each n computed once -> O(n)
@lru_cache(None)
def fib(n):
    if n < 2: return n      # base cases
    return fib(n-1) + fib(n-2)  # cached after first call

print(fib(50))   # instant: 12586269025

Complexity

ApproachTimeWhy
Naive recursionO(2ⁿ)Recomputes the same subproblems exponentially
DP (memo/table)O(n)Each subproblem solved exactly once

🧠 Common Problem-Solving Patterns

The reusable "shapes" that crack the majority of coding problems.

Why patterns?

Most problems aren't new β€” they're a familiar pattern in disguise. Half the battle is recognizing which one. Learn to spot the signal, and the solution structure comes for free.

The pattern toolbox

PatternUse when…Signal
Two pointersSorted array / pair-finding"find a pair", "in-place", sorted input
Sliding windowContiguous subarray/substring"longest/shortest subarray with…"
Fast & slow pointersCycle detection in a listlinked list, "does it loop?"
Hash map countingFrequencies / seen-before"count", "duplicate", "anagram"
Prefix sumsMany range-sum queries"sum between i and j"
Binary search on answerMonotonic feasibility"minimize the max", "smallest valid"
BFS / DFSGraphs, grids, trees"shortest path", "connected", "reachable"
Heap / top-KK largest/smallest, streaming"top K", "K closest", "median"
BacktrackingEnumerate all valid configs"all permutations/combinations"
Dynamic programmingOverlapping subproblems"count ways", "min cost", "can you…"
Two pointers in action β€” find a pair that sums to the target, in one pass
L starts at the smallest, R at the largest. Sum too big β†’ move R in; too small β†’ move L up. O(n), no nested loop.
Study tip: after solving a problem, label which pattern it was. Over time you build a mental index β€” and new problems start looking familiar.

βœ… Algorithm Correctness & Proofs

How you know an algorithm works β€” not just that it passed a few tests.

Correctness = terminates + right answer

Two things to establish: the algorithm always stops (termination), and when it stops the output is correct. Tests can only show bugs exist; a proof shows they don't.

Loop invariants β€” the main tool

A loop invariant is a statement that stays true before and after every iteration. Prove three things and correctness follows:

  1. Initialization β€” it's true before the loop starts.
  2. Maintenance β€” if it's true before an iteration, it's still true after.
  3. Termination β€” when the loop ends, the invariant gives you the result you wanted.
Induction in disguise: initialization is the base case, maintenance is the inductive step. That's why the invariant holds for every iteration.
Loop invariant in action β€” "best = the max of everything seen so far"

Invariant β€” true at the top of every iteration

best = max(a[0..0]) β€” true before we start.
The i pointer advances; green marks the current best. The invariant never breaks β€” that's the proof.

Key terms

loop invariant
A property true at the top of every iteration; the backbone of loop proofs.
induction
Prove a base case, then that each case implies the next.
termination
Argue the algorithm always halts (e.g., a value strictly decreases toward a bound).
pre/postcondition
What must hold before running / what's guaranteed after.
counterexample
A single input where the algorithm fails β€” disproves correctness instantly.

Sample: an invariant in action

Python
def max_of(a):
    best = a[0]
    # INVARIANT: `best` == the maximum of a[0..i-1]
    for i in range(1, len(a)):
        if a[i] > best:
            best = a[i]
        # invariant restored: best == max of a[0..i]
    return best   # at exit i==len(a): best == max of whole array
The habit: for any loop, ask "what's true every time I reach the top?" Name that invariant and correctness usually becomes obvious.