InterviewKata
Start here · foundations

Everything the patterns assume you already know

The pattern pages move fast: they expect you to know what O(n) means, when to reach for a dict, and how a recursive function unwinds. This page teaches exactly those basics, from zero, with the same step-through visualizers. Skip any section you already know.

1. How this site teaches

A carpenter doesn't invent a new tool for every job. They own about twenty, know what each one is for, and recognise in seconds which job needs which tool. Coding interviews work the same way.

Most interview problems are variations of about two dozen ideas called patterns: two pointers, sliding window, BFS, dynamic programming and so on. Each pattern has its own page with the same ten sections: what it's for, how it works, how to spot it, when not to use it, a Python template, memory hooks, solved LeetCode problems, common mistakes, a quiz and a practice list.

  1. Learn the foundations here. Big-O, the data structures, recursion and the Python you'll actually type.
  2. Follow the beginner path. It orders the 24 patterns from gentlest to hardest.
  3. On each pattern page, solve the Start here problems first. They're the gentlest 2–3 problems for that pattern.
  4. Press Play on every visualizer. Watching the pointers, windows and stacks move is faster than reading about them. Inside a visualizer, ← → step and space plays.
  5. Hover or tap a dotted word on any page to see a one-line definition. They're all collected in the glossary.

2. Big-O: is my solution fast enough?

Two ways to find a name in a phone book: read every page from the start, or open it in the middle and keep halving. For 100 names both feel instant. For a million names, the first takes a million looks and the second about 20.

Big-O describes how the number of steps grows as the input size n grows. It ignores constants and small terms, because for big inputs only the shape of the growth matters. You find it by counting how many times the work repeats:

Code shapeBig-OWhy
x = nums[i], d[key], x in my_setO(1)One step, whatever the size.
Halve the range every step (binary search)O(log n)A million halves to 1 in about 20 steps.
One loop over the inputO(n)One step per element.
sorted(nums), a heap push for every elementO(n log n)n elements, each costing log n.
A loop inside a loop over the same inputO(n²)n steps, each doing n steps.
Try every subset (each item: take it or not)O(2ⁿ)Each new item doubles the work.
def has_duplicate_slow(nums):          # O(n²): compares every pair
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] == nums[j]:
                return True
    return False

def has_duplicate_fast(nums):          # O(n): one pass, O(1) set lookups
    seen = set()
    for x in nums:
        if x in seen:
            return True
        seen.add(x)
    return False

Drag the slider to see why this matters. Python does roughly ten million simple steps per second, and LeetCode usually allows a second or two.

n = 1,000
Read the constraints first: they tell you the target Big-On ≤ 10 → O(n!) or O(2ⁿ) is fine (backtracking) · n ≤ 20 → O(2ⁿ) · n ≤ 500 → O(n³) · n ≤ 5,000 → O(n²) · n ≤ 10⁵ or 10⁶ → O(n log n) or O(n). If a nested loop would be O(n²) and n is 10⁵, you need a pattern.

Space complexity is the same idea for memory: how much extra storage you use as n grows. has_duplicate_fast trades O(n) memory (the set) for O(n) time. That trade, spending memory to save time, is behind half the patterns on this site.

3. The core data structures

A data structure is a way of storing values so that the operations you need are cheap. Picking the right one is often the whole solution. These are the ones interviews use, and what they cost:

StructureIn PythonGood atCost
Array / list[3, 1, 4]Reading or writing position i; adding at the endindex O(1) · append O(1) · insert/remove at front O(n) · search O(n)
String"abc"Text; immutable, so build with a list and "".joinindex O(1) · slicing O(k) · s += c in a loop O(n²)
Hash mapdict, Counter, defaultdict"Have I seen this?", "how many?", "where was it?"get / set / delete / in: O(1) on average
Hash setsetUnique values, fast membership testsadd / remove / in: O(1) on average
Stacklist: append, popLast in, first out: brackets, undo, nestingpush / pop / peek O(1)
Queuecollections.dequeFirst in, first out: BFS, processing in arrival orderappend / popleft O(1) (list.pop(0) is O(n))
Heapheapq on a listAlways getting the smallest (or largest) nextpeek O(1) · push / pop O(log n)
Linked listListNode objectsRewiring order without shifting elementsinsert after a node O(1) · find the i-th O(n)
TreeTreeNode objectsHierarchies; sorted data (binary search trees)visit all O(n) · BST search O(height)
Graphdict of lists (adjacency list)Networks: roads, friendships, dependenciesvisit all O(nodes + edges)

Stack and queue

Both hold items in a line. A stack adds and removes at the same end, so the newest item leaves first. A queue adds at the back and removes from the front, so the oldest leaves first.

Hash map: counting and remembering

A dict jumps straight to a key's value instead of searching for it. That turns "have I seen this before?" from a loop into one step, which is why it fixes so many slow nested loops.

Linked list: rewiring pointers

Each node knows only its value and the next node. There's no index, so you walk from the head. In exchange, inserting needs no shifting: just change two pointers, in the right order.

Heap: the smallest item, always on top

A heap is a binary tree stored in a plain list, where every parent is smaller than its children. The smallest item is always at index 0. A push adds at the end and swaps upward; a pop moves the last item to the top and swaps it down. Each takes O(log n) because the tree's height is log n.

Trees and graphs

A binary tree: node 1 is the root, 2 and 3 are its children, and 4, 5 and 6 are leaves (no children). Every node with everything below it is a subtree, which is why tree code is usually recursive.

A graph: nodes joined by edges, with no root and possibly cycles (A → B → C → A). Store it as an adjacency list: {"A": ["B", "D"], "B": ["C"], "C": ["A"], "D": []}.

4. Recursion, one call at a time

You're in a dark cinema and want to know your row number. You ask the person in front, who asks the person in front of them, and so on. The front row answers "1". Each person adds 1 and passes the answer back. Nobody needed to see the whole cinema.

A recursive function solves a problem by calling itself on a smaller version of it. Every recursive function has the same three parts:

  1. Base case: the smallest input, answered directly. Without it the calls never stop.
  2. Smaller step: call yourself on a smaller input (n − 1, the left child, the rest of the list).
  3. Combine: trust that the smaller call returns the right answer, and build yours from it.
def factorial(n):
    if n == 1:                    # base case: answer directly
        return 1
    return n * factorial(n - 1)   # smaller step, then combine

Each call waits for the one it made. Python keeps the waiting calls on the call stack, a stack of paused calls with the newest on top:

When a function calls itself twice, the calls form a tree. Fibonacci is the classic example: fib(n) = fib(n − 1) + fib(n − 2). Watch how fib(2) is computed twice: that repeated work is exactly what dynamic programming removes by remembering answers (memoization).

def fib(n):
    if n <= 1:                    # base cases: fib(0) = 0, fib(1) = 1
        return n
    return fib(n - 1) + fib(n - 2)
Python's recursion limitPython stops at about 1,000 nested calls with a RecursionError. For deep inputs (a linked list of 10⁵ nodes, a long path) either add sys.setrecursionlimit(10**6) or rewrite the recursion with your own stack.

Where you'll use it: tree traversal, DFS, backtracking and dynamic programming are all recursion with a twist.

5. The Python toolkit for interviews

You don't need much Python, but you need these few tools at your fingertips:

ToolWhat it's forExample
enumerate, zip, rangeLoop with indices, walk two lists together, count downfor i, x in enumerate(nums) · range(n - 1, -1, -1)
SlicingSub-lists, copies, reversing (each makes a copy: O(k))nums[1:4] · s[::-1] · nums[:]
dict / defaultdictRemember positions, group thingsgroups = defaultdict(list); groups[key].append(x)
CounterCount everything in one lineCounter("banana")["a"] == 3
set"Seen before?" in O(1)if x in seen: ...
dequeQueues for BFSq = deque([start]); q.popleft()
heapqSmallest item next; top kheappush(h, x) · heappop(h) · heappush(h, -x) for max
sorted with keySort by any rulesorted(intervals, key=lambda iv: iv[0])
bisectBinary search in a sorted listbisect_left(arr, x) = first index with value ≥ x
@cacheMemoize a recursive functionfrom functools import cache
float("inf")A starting value for min / maxbest = float("inf")
"".join, splitBuild strings efficiently, split words"".join(parts) · s.split()
from collections import Counter, defaultdict, deque
import heapq

counts = Counter("banana")              # {'a': 3, 'n': 2, 'b': 1}
by_len = defaultdict(list)
for w in ["hi", "yo", "hey"]:
    by_len[len(w)].append(w)            # {2: ['hi', 'yo'], 3: ['hey']}

q = deque([1])                          # queue: add right, remove left
q.append(2)
first = q.popleft()                     # 1

h = []                                  # heap: smallest always at h[0]
for x in [5, 1, 4]:
    heapq.heappush(h, x)
smallest = heapq.heappop(h)             # 1

grid = [[0] * 3 for _ in range(2)]      # 2 rows x 3 columns, rows independent
Python traps that cost interviews[[0] * 3] * 2 makes two references to the same row: change one, both change. Use the list comprehension above. Also: // is integer division (7 // 2 == 3), list.pop(0) is O(n) (use a deque), and s += c inside a loop rebuilds the string every time.

6. How to read a LeetCode problem

Every LeetCode problem has the same parts, and each one gives you information:

  1. The statement. Underline the input, the output and any words like sorted, contiguous, unique, at most k. Those words point to the pattern; the pattern finder lists them.
  2. The examples. Work one by hand before coding. If you can't do it on paper, you can't code it.
  3. The constraints. The size of n tells you the Big-O you need (see section 2). Value ranges tell you about negatives, zeros and overflow.
  4. The function signature. You fill in a method of a class called Solution. LeetCode calls it with each test.
from typing import List, Optional

class Solution:                                  # LeetCode gives you this shell
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        ...                                      # your code goes here

# LeetCode's types, if you want to run tree or list problems yourself:
class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next

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

# To test on your own computer, call it like LeetCode does:
print(Solution().twoSum([2, 7, 11, 15], 9))

7. Your first problem: Two Sum, start to finish

Problem: given a list of numbers and a target, return the indices of the two numbers that add up to the target. Example: nums = [2, 7, 11, 15], target = 9 → [0, 1], because 2 + 7 = 9.

  1. Understand. Exactly one answer exists; you can't use the same element twice; return indices, not values.
  2. Brute force first. Try every pair: two nested loops, O(n²). Say it out loud in an interview; it shows you can solve it, and it gives you something to improve.
  3. Find the wasted work. For each number x, the inner loop searches for target − x. Searching is what a hash map makes instant.
  4. Improve. Walk once; for each x, check whether its partner target − x was already seen; otherwise remember x's index. O(n) time, O(n) space.
  5. Test on the example and the edge cases: negatives, duplicates like [3, 3], the pair at the very end.
class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        # Brute force, O(n²):
        # for i in range(len(nums)):
        #     for j in range(i + 1, len(nums)):
        #         if nums[i] + nums[j] == target:
        #             return [i, j]
        seen = {}                          # value -> index where we saw it
        for i, x in enumerate(nums):
            if target - x in seen:         # partner seen earlier: done
                return [seen[target - x], i]
            seen[x] = i                    # remember x for later numbers

That move, from "search again every time" to "remember what you've seen", is the Hash Map & Set pattern. Step through this exact solution, line by line, on its pattern page. This 5-step routine is the UMPIRE method: use it on every problem.

8. Self-check quiz

A loop over nums with another loop over nums inside it. What's the time complexity?

The inner loop runs n times for each of the n outer steps: n × n.

The constraints say n ≤ 10⁵. What should you aim for?

10⁵ squared is 10¹⁰ steps, minutes in Python. n log n is about 1.7 million.

You need to check "have I seen this value before?" millions of times. Which structure?

Membership in a set is O(1) on average; in a list it's O(n).

Which should you use for a queue in Python?

list.pop(0) shifts every element: O(n). deque.popleft() is O(1).

Your recursive function raises RecursionError on a tiny input. Most likely cause?

Without a reachable base case, the calls never stop. Deep but correct recursion only fails on large inputs.

9. Glossary

Every dotted word on the site comes from this list. Hover or tap one anywhere to see its definition.

10. Where to go next

Follow the beginner path

The 24 patterns in the order to learn them, each with its first problems. Open the beginner path →

Start with pattern 21

Array & String Basics: careful scans and clean output, the skill every other pattern builds on.

Keep the cheat sheet open

All 24 templates and trigger words on one printable page.