LeetCode Medium Playbook

48 problems, 16 patterns. Recognize the pattern, and most mediums fall in 20 minutes.

How to use this: For each problem, read the statement and try to name the pattern and sketch the approach in your head before tapping "Show solution". If you were right, skim the code and move on. If not, study the insight line — that one sentence is the transferable part. Codes are idiomatic interview Python. Second pass: only reopen the ones you missed.

Test yourself: tick “test me” on any problem you’ve studied, then hit Test me below — it deals random questions from your bank. Recall the pattern and sketch the code mentally, then tap “Show solution” to check. Your bank is saved on this device.

Arrays & Hashing

Trade memory for speed: a set or dict turns "search" into O(1). Prefix sums turn "range/subarray" questions into lookups.

1

Group Anagrams

LC 49

Given an array of strings, group the anagrams together.

["eat","tea","tan","ate","nat","bat"] → [["eat","tea","ate"],["tan","nat"],["bat"]]

Show solution

Insight: All anagrams share the same character-count signature — use it as a dict key.

from collections import defaultdict

def groupAnagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        key = [0] * 26
        for c in s:
            key[ord(c) - ord('a')] += 1
        groups[tuple(key)].append(s)
    return list(groups.values())

Complexity: O(n·k) time, O(n·k) space. (Sorting each word as the key is O(n·k log k) and also fine.)

2

Top K Frequent Elements

LC 347

Given an integer array and k, return the k most frequent elements.

nums=[1,1,1,2,2,3], k=2 → [1,2]

Show solution

Insight: A frequency can't exceed n, so bucket numbers by frequency and read buckets from high to low — O(n), no sorting.

from collections import Counter

def topKFrequent(nums, k):
    count = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, freq in count.items():
        buckets[freq].append(num)
    res = []
    for freq in range(len(buckets) - 1, 0, -1):
        for num in buckets[freq]:
            res.append(num)
            if len(res) == k:
                return res

Complexity: O(n) time and space. (heapq.nlargest(k, count, key=count.get) is an acceptable O(n log k) one-liner.)

3

Product of Array Except Self

LC 238

Return an array where answer[i] is the product of all elements except nums[i]. No division, O(n).

[1,2,3,4] → [24,12,8,6]

Show solution

Insight: answer[i] = (product of everything left of i) × (product of everything right of i). Two sweeps.

def productExceptSelf(nums):
    n = len(nums)
    res = [1] * n
    prefix = 1
    for i in range(n):
        res[i] = prefix
        prefix *= nums[i]
    suffix = 1
    for i in range(n - 1, -1, -1):
        res[i] *= suffix
        suffix *= nums[i]
    return res

Complexity: O(n) time, O(1) extra space.

4

Longest Consecutive Sequence

LC 128

Given an unsorted array, return the length of the longest run of consecutive integers. Must be O(n).

[100,4,200,1,3,2] → 4 (run 1,2,3,4)

Show solution

Insight: Put everything in a set; only count upward from numbers that start a run (num−1 absent), so each element is visited once.

def longestConsecutive(nums):
    s = set(nums)
    best = 0
    for num in s:
        if num - 1 not in s:        # only start counting at run beginnings
            length = 1
            while num + length in s:
                length += 1
            best = max(best, length)
    return best

Complexity: O(n) time and space.

5

Subarray Sum Equals K

LC 560

Count the number of contiguous subarrays that sum to k. (Negatives allowed, so sliding window does NOT work.)

nums=[1,1,1], k=2 → 2

Show solution

Insight: sum(j..i) = prefix[i] − prefix[j−1]. So at each i, count how many earlier prefixes equal prefix[i] − k — keep them in a dict.

from collections import defaultdict

def subarraySum(nums, k):
    seen = defaultdict(int)
    seen[0] = 1                     # empty prefix
    total = res = 0
    for x in nums:
        total += x
        res += seen[total - k]
        seen[total] += 1
    return res

Complexity: O(n) time and space. This prefix-sum + hashmap trick shows up in many "count subarrays with property X" problems.

Two Pointers

On a sorted (or two-ended) structure, move pointers inward based on a comparison, eliminating candidates in O(n) instead of O(n²).

6

3Sum

LC 15

Find all unique triplets that sum to zero.

[-1,0,1,2,-1,-4] → [[-1,-1,2],[-1,0,1]]

Show solution

Insight: Sort. Fix the smallest number, then it's Two Sum on a sorted array with two pointers. Skip duplicates at every level.

def threeSum(nums):
    nums.sort()
    res = []
    for i in range(len(nums) - 2):
        if nums[i] > 0:
            break
        if i > 0 and nums[i] == nums[i - 1]:
            continue
        l, r = i + 1, len(nums) - 1
        while l < r:
            s = nums[i] + nums[l] + nums[r]
            if s < 0:
                l += 1
            elif s > 0:
                r -= 1
            else:
                res.append([nums[i], nums[l], nums[r]])
                l += 1
                r -= 1
                while l < r and nums[l] == nums[l - 1]:
                    l += 1
    return res

Complexity: O(n²) time, O(1) extra space.

7

Container With Most Water

LC 11

Given heights of vertical lines, pick two that with the x-axis hold the most water (area = width × min height).

[1,8,6,2,5,4,8,3,7] → 49

Show solution

Insight: Start widest. Moving the taller side can only shrink area (width drops, height capped by the shorter side) — so always move the shorter pointer.

def maxArea(height):
    l, r = 0, len(height) - 1
    best = 0
    while l < r:
        best = max(best, (r - l) * min(height[l], height[r]))
        if height[l] < height[r]:
            l += 1
        else:
            r -= 1
    return best

Complexity: O(n) time, O(1) space.

8

Sort Colors

LC 75

Sort an array of 0s, 1s, 2s in-place in one pass (Dutch National Flag).

[2,0,2,1,1,0] → [0,0,1,1,2,2]

Show solution

Insight: Three regions: 0s before low, 2s after high, mid scans. Swap 0s down, 2s up. Don't advance mid after swapping with high — the incoming value is unexamined.

def sortColors(nums):
    low, mid, high = 0, 0, len(nums) - 1
    while mid <= high:
        if nums[mid] == 0:
            nums[low], nums[mid] = nums[mid], nums[low]
            low += 1
            mid += 1
        elif nums[mid] == 2:
            nums[mid], nums[high] = nums[high], nums[mid]
            high -= 1
        else:
            mid += 1

Complexity: O(n) time, O(1) space.

Sliding Window

Maintain a window [l, r] over the array/string; expand r, and shrink l only when a constraint breaks. Each pointer moves at most n times → O(n).

9

Longest Substring Without Repeating Characters

LC 3

Return the length of the longest substring with no repeated characters.

"abcabcbb" → 3 ("abc")

Show solution

Insight: Track each char's last index. When you see a repeat inside the current window, jump the window start past its previous occurrence.

def lengthOfLongestSubstring(s):
    last = {}
    start = best = 0
    for i, c in enumerate(s):
        if c in last and last[c] >= start:
            start = last[c] + 1
        last[c] = i
        best = max(best, i - start + 1)
    return best

Complexity: O(n) time, O(min(n, alphabet)) space.

10

Longest Repeating Character Replacement

LC 424

You may change at most k characters. Return the longest substring you can make all one letter.

s="AABABBA", k=1 → 4

Show solution

Insight: A window is fixable iff window_len − count_of_most_frequent_char ≤ k. Expand right; shrink left when that breaks. (maxf never needs decreasing — a stale maxf only makes the window not grow, which is safe since we want the max.)

from collections import defaultdict

def characterReplacement(s, k):
    count = defaultdict(int)
    l = maxf = best = 0
    for r, c in enumerate(s):
        count[c] += 1
        maxf = max(maxf, count[c])
        while (r - l + 1) - maxf > k:
            count[s[l]] -= 1
            l += 1
        best = max(best, r - l + 1)
    return best

Complexity: O(n) time, O(26) space.

11

Minimum Size Subarray Sum

LC 209

Given positive integers and a target, return the length of the shortest contiguous subarray with sum ≥ target (0 if none).

target=7, nums=[2,3,1,2,4,3] → 2 ([4,3])

Show solution

Insight: All positive → sum grows as the window grows, so greedily shrink from the left while the sum still meets the target.

def minSubArrayLen(target, nums):
    l = total = 0
    best = float('inf')
    for r, x in enumerate(nums):
        total += x
        while total >= target:
            best = min(best, r - l + 1)
            total -= nums[l]
            l += 1
    return best if best != float('inf') else 0

Complexity: O(n) time, O(1) space. Contrast with #5: negatives break this — that's when you need prefix sums.

Stack

LIFO handles nesting and "most recent unresolved thing". A monotonic stack answers "next greater/smaller element" in O(n).

15

Daily Temperatures

LC 739

For each day, how many days until a warmer temperature? (0 if never.)

[73,74,75,71,69,72,76,73] → [1,1,4,2,1,1,0,0]

Show solution

Insight: Keep a stack of indices with decreasing temperatures. A new warmer day resolves everything on the stack that's colder.

def dailyTemperatures(temps):
    res = [0] * len(temps)
    stack = []                       # indices; temps decreasing
    for i, t in enumerate(temps):
        while stack and temps[stack[-1]] < t:
            j = stack.pop()
            res[j] = i - j
        stack.append(i)
    return res

Complexity: O(n) time (each index pushed/popped once), O(n) space.

16

Decode String

LC 394

Decode k[encoded] where the bracketed string repeats k times; nesting allowed.

"3[a2[c]]" → "accaccacc"

Show solution

Insight: On [, push the (string built so far, pending multiplier) and start fresh; on ], pop and combine. The stack holds the unfinished outer contexts.

def decodeString(s):
    stack = []
    cur, num = "", 0
    for c in s:
        if c.isdigit():
            num = num * 10 + int(c)
        elif c == '[':
            stack.append((cur, num))
            cur, num = "", 0
        elif c == ']':
            prev, k = stack.pop()
            cur = prev + cur * k
        else:
            cur += c
    return cur

Complexity: O(output length) time and space.

Linked List

Almost every list medium is built from three moves: dummy head, fast/slow pointers, reverse-in-place. Know them cold.

17

Remove Nth Node From End of List

LC 19

Delete the nth node from the end in one pass.

1→2→3→4→5, n=2 → 1→2→3→5

Show solution

Insight: Two pointers n apart: when the lead hits the end, the trailer is just before the victim. Dummy head handles "delete the first node".

def removeNthFromEnd(head, n):
    dummy = ListNode(0, head)
    fast = slow = dummy
    for _ in range(n):
        fast = fast.next
    while fast.next:
        fast = fast.next
        slow = slow.next
    slow.next = slow.next.next
    return dummy.next

Complexity: O(n) time, O(1) space.

18

Reorder List

LC 143

Reorder L0→L1→…→Ln to L0→Ln→L1→Ln−1→… in-place.

1→2→3→4→5 → 1→5→2→4→3

Show solution

Insight: Three classics chained: find the middle (fast/slow), reverse the second half, interleave the two halves.

def reorderList(head):
    # 1. middle
    slow = fast = head
    while fast.next and fast.next.next:
        slow, fast = slow.next, fast.next.next
    # 2. reverse second half
    prev, cur = None, slow.next
    slow.next = None
    while cur:
        nxt = cur.next
        cur.next = prev
        prev, cur = cur, nxt
    # 3. interleave
    first, second = head, prev
    while second:
        n1, n2 = first.next, second.next
        first.next = second
        second.next = n1
        first, second = n1, n2

Complexity: O(n) time, O(1) space.

19

Linked List Cycle II

LC 142

Return the node where the cycle begins, or None. O(1) space.

Show solution

Insight: Floyd: after slow and fast meet inside the cycle, the distance from head to cycle start equals the distance from the meeting point to cycle start — walk two pointers at equal speed.

def detectCycle(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast:
            p = head
            while p is not slow:
                p, slow = p.next, slow.next
            return p
    return None

Complexity: O(n) time, O(1) space.

20

Find the Duplicate Number

LC 287

n+1 integers in range [1, n]; exactly one value repeats. Find it without modifying the array, O(1) space.

[1,3,4,2,2] → 2

Show solution

Insight: Treat i → nums[i] as a linked list; a duplicate value means two arrows into one node — a cycle. It's exactly Cycle II in disguise.

def findDuplicate(nums):
    slow = fast = 0
    while True:
        slow = nums[slow]
        fast = nums[nums[fast]]
        if slow == fast:
            break
    slow = 0
    while slow != fast:
        slow, fast = nums[slow], nums[fast]
    return slow

Complexity: O(n) time, O(1) space.

21

LRU Cache

LC 146

Design a fixed-capacity cache where get and put run in O(1); evict the least-recently-used key when full.

Show solution

Insight: Hashmap for O(1) lookup + doubly-linked list for O(1) recency reordering. Python's OrderedDict IS that combo — use it, but be ready to explain (or code) the manual dict + DLL version if the interviewer asks.

from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.od = OrderedDict()

    def get(self, key):
        if key not in self.od:
            return -1
        self.od.move_to_end(key)          # mark most-recent
        return self.od[key]

    def put(self, key, value):
        if key in self.od:
            self.od.move_to_end(key)
        self.od[key] = value
        if len(self.od) > self.cap:
            self.od.popitem(last=False)   # evict least-recent

Complexity: O(1) per operation.

Trees

Two engines: DFS (recursion — think "what do I need from each subtree?") and BFS (queue — anything phrased "level by level").

22

Binary Tree Level Order Traversal

LC 102

Return node values level by level.

[3,9,20,null,null,15,7] → [[3],[9,20],[15,7]]

Show solution

Insight: BFS with a queue; snapshot len(q) at the start of each round to know where the level ends. This skeleton also solves right-side view, zigzag, min depth…

from collections import deque

def levelOrder(root):
    if not root:
        return []
    res, q = [], deque([root])
    while q:
        level = []
        for _ in range(len(q)):
            node = q.popleft()
            level.append(node.val)
            if node.left:
                q.append(node.left)
            if node.right:
                q.append(node.right)
        res.append(level)
    return res

Complexity: O(n) time, O(width) space.

23

Validate Binary Search Tree

LC 98

Is the tree a valid BST? (Left subtree all smaller, right all larger, recursively.)

Show solution

Insight: Comparing only parent↔child is the classic wrong answer. Pass down an allowed (lo, hi) range that tightens at each step.

def isValidBST(root):
    def check(node, lo, hi):
        if not node:
            return True
        if not (lo < node.val < hi):
            return False
        return (check(node.left, lo, node.val) and
                check(node.right, node.val, hi))
    return check(root, float('-inf'), float('inf'))

Complexity: O(n) time, O(height) space. (Alt: inorder traversal must be strictly increasing.)

24

Kth Smallest Element in a BST

LC 230

Return the kth smallest value in a BST.

Show solution

Insight: Inorder traversal of a BST yields sorted order — do it iteratively and stop at the kth pop.

def kthSmallest(root, k):
    stack, node = [], root
    while True:
        while node:
            stack.append(node)
            node = node.left
        node = stack.pop()
        k -= 1
        if k == 0:
            return node.val
        node = node.right

Complexity: O(h + k) time, O(h) space.

25

Lowest Common Ancestor of a Binary Tree

LC 236

Given two nodes p and q, return their lowest common ancestor.

Show solution

Insight: Post-order: ask each subtree "did you find p or q?". The first node whose left AND right both report a find is the LCA.

def lowestCommonAncestor(root, p, q):
    if root is None or root is p or root is q:
        return root
    l = lowestCommonAncestor(root.left, p, q)
    r = lowestCommonAncestor(root.right, p, q)
    if l and r:
        return root
    return l or r

Complexity: O(n) time, O(height) space. (BST variant: just walk down using value comparisons.)

26

Construct Binary Tree from Preorder and Inorder

LC 105

Rebuild the (unique-valued) tree from its preorder and inorder traversals.

preorder=[3,9,20,15,7], inorder=[9,3,15,20,7]

Show solution

Insight: Preorder hands you roots in order; inorder tells you how many nodes go left vs right of each root. A value→index map makes each split O(1).

def buildTree(preorder, inorder):
    idx = {v: i for i, v in enumerate(inorder)}
    pre = iter(preorder)

    def build(lo, hi):              # inorder slice [lo, hi]
        if lo > hi:
            return None
        val = next(pre)
        root = TreeNode(val)
        mid = idx[val]
        root.left = build(lo, mid - 1)
        root.right = build(mid + 1, hi)
        return root

    return build(0, len(inorder) - 1)

Complexity: O(n) time and space.

Graphs

Grids are graphs. DFS/BFS flood fill, multi-source BFS for "spreading", topological sort for dependencies.

27

Number of Islands

LC 200

Grid of '1' (land) / '0' (water). Count connected islands (4-directional).

Show solution

Insight: Each time you step on unvisited land, that's one new island — flood-fill (sink) the whole component so you never count it again.

def numIslands(grid):
    rows, cols = len(grid), len(grid[0])

    def sink(r, c):
        if 0 <= r < rows and 0 <= c < cols and grid[r][c] == '1':
            grid[r][c] = '0'
            sink(r + 1, c); sink(r - 1, c)
            sink(r, c + 1); sink(r, c - 1)

    count = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == '1':
                count += 1
                sink(r, c)
    return count

Complexity: O(rows·cols) time; O(rows·cols) recursion worst case (use an explicit stack/BFS for huge grids).

28

Rotting Oranges

LC 994

Each minute, rotten oranges (2) rot adjacent fresh ones (1). Minutes until no fresh remain, or −1.

Show solution

Insight: Shortest "time to reach everything" from many sources at once = multi-source BFS: seed the queue with all rotten oranges, expand in rounds.

from collections import deque

def orangesRotting(grid):
    rows, cols = len(grid), len(grid[0])
    q, fresh = deque(), 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 2:
                q.append((r, c))
            elif grid[r][c] == 1:
                fresh += 1
    minutes = 0
    while q and fresh:
        for _ in range(len(q)):
            r, c = q.popleft()
            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                    grid[nr][nc] = 2
                    fresh -= 1
                    q.append((nr, nc))
        minutes += 1
    return -1 if fresh else minutes

Complexity: O(rows·cols) time and space.

29

Course Schedule

LC 207

Given prerequisite pairs [a, b] (take b before a), can you finish all courses? (= is the graph acyclic?)

Show solution

Insight: Kahn's topological sort: repeatedly take courses with zero remaining prerequisites. If you can't take all of them, there's a cycle.

from collections import defaultdict, deque

def canFinish(numCourses, prerequisites):
    graph = defaultdict(list)
    indeg = [0] * numCourses
    for a, b in prerequisites:
        graph[b].append(a)
        indeg[a] += 1
    q = deque(i for i in range(numCourses) if indeg[i] == 0)
    taken = 0
    while q:
        c = q.popleft()
        taken += 1
        for nxt in graph[c]:
            indeg[nxt] -= 1
            if indeg[nxt] == 0:
                q.append(nxt)
    return taken == numCourses

Complexity: O(V + E) time and space. Course Schedule II = same code, collect the pop order.

30

Clone Graph

LC 133

Deep-copy an undirected graph given one node.

Show solution

Insight: DFS with an original→clone map; the map doubles as the visited set and breaks cycles.

def cloneGraph(node):
    if not node:
        return None
    clones = {}

    def dfs(n):
        if n in clones:
            return clones[n]
        copy = Node(n.val)
        clones[n] = copy              # register BEFORE recursing
        copy.neighbors = [dfs(nb) for nb in n.neighbors]
        return copy

    return dfs(node)

Complexity: O(V + E) time, O(V) space.

Backtracking

Build a partial solution, recurse, undo (choose → explore → un-choose). One template covers subsets, permutations, combinations, and grid search.

31

Subsets

LC 78

Return all subsets of an array of unique integers.

[1,2,3] → [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

Show solution

Insight: At each index, branch twice: include the element or don't. 2^n leaves = 2^n subsets.

def subsets(nums):
    res, path = [], []

    def backtrack(i):
        if i == len(nums):
            res.append(path[:])       # copy!
            return
        path.append(nums[i])          # choose
        backtrack(i + 1)
        path.pop()                    # un-choose
        backtrack(i + 1)

    backtrack(0)
    return res

Complexity: O(n·2^n). (Slick iterative alt: for x in nums: res += [s + [x] for s in res].)

32

Combination Sum

LC 39

Unique candidates, unlimited reuse: return all combinations summing to target.

candidates=[2,3,6,7], target=7 → [[2,2,3],[7]]

Show solution

Insight: Pass a start index so combinations are generated in non-decreasing order — that's what prevents duplicate sets. Reuse is allowed by recursing with i, not i + 1.

def combinationSum(candidates, target):
    res, path = [], []

    def backtrack(start, remaining):
        if remaining == 0:
            res.append(path[:])
            return
        for i in range(start, len(candidates)):
            if candidates[i] <= remaining:
                path.append(candidates[i])
                backtrack(i, remaining - candidates[i])   # i, not i+1: reuse OK
                path.pop()

    backtrack(0, target)
    return res

Complexity: Exponential in the worst case; fine for the given constraints.

33

Permutations

LC 46

Return all orderings of an array of distinct integers.

[1,2,3] → 6 permutations

Show solution

Insight: At each level choose any element not yet used. (Unlike subsets/combinations, order matters, so there's no start index.)

def permute(nums):
    res = []

    def backtrack(path, remaining):
        if not remaining:
            res.append(path)
            return
        for i in range(len(remaining)):
            backtrack(path + [remaining[i]],
                      remaining[:i] + remaining[i + 1:])

    backtrack([], nums)
    return res

Complexity: O(n·n!) time.

34

Word Search

LC 79

Does the word exist in the grid as a path of adjacent cells (no cell reused)?

Show solution

Insight: DFS from every cell; mark the cell in-place with a sentinel while exploring, restore on the way out — that's the backtracking.

def exist(board, word):
    rows, cols = len(board), len(board[0])

    def dfs(r, c, i):
        if i == len(word):
            return True
        if (not (0 <= r < rows and 0 <= c < cols)
                or board[r][c] != word[i]):
            return False
        board[r][c] = '#'             # mark
        found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1) or
                 dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1))
        board[r][c] = word[i]         # restore
        return found

    return any(dfs(r, c, 0)
               for r in range(rows) for c in range(cols))

Complexity: O(rows·cols·3^len(word)) worst case.

Heap

When you only need the k best of a stream/array, a size-k heap beats sorting: O(n log k).

35

Kth Largest Element in an Array

LC 215

Find the kth largest element (not distinct) without fully sorting.

nums=[3,2,1,5,6,4], k=2 → 5

Show solution

Insight: Keep a min-heap of the k largest seen so far; the root is always the current kth largest. Mention quickselect (average O(n)) as the follow-up answer.

import heapq

def findKthLargest(nums, k):
    heap = nums[:k]
    heapq.heapify(heap)
    for x in nums[k:]:
        if x > heap[0]:
            heapq.heapreplace(heap, x)
    return heap[0]

Complexity: O(n log k) time, O(k) space.

Intervals

Sort first — by start to merge, by end to greedily select the most non-overlapping.

36

Merge Intervals

LC 56

Merge all overlapping intervals.

[[1,3],[2,6],[8,10],[15,18]] → [[1,6],[8,10],[15,18]]

Show solution

Insight: Sort by start; then each interval either overlaps the last merged one (extend its end) or starts a new group.

def merge(intervals):
    intervals.sort()
    res = [intervals[0]]
    for start, end in intervals[1:]:
        if start <= res[-1][1]:
            res[-1][1] = max(res[-1][1], end)
        else:
            res.append([start, end])
    return res

Complexity: O(n log n) time.

37

Non-overlapping Intervals

LC 435

Minimum number of intervals to remove so the rest don't overlap.

[[1,2],[2,3],[3,4],[1,3]] → 1

Show solution

Insight: Classic activity selection: sort by end and greedily keep every interval that starts after the last kept end — earliest end leaves maximum room.

def eraseOverlapIntervals(intervals):
    intervals.sort(key=lambda x: x[1])
    removed, prev_end = 0, float('-inf')
    for start, end in intervals:
        if start >= prev_end:
            prev_end = end            # keep it
        else:
            removed += 1              # drop it
    return removed

Complexity: O(n log n) time.

Greedy & Kadane

Prove (or convince yourself) that a local rule never hurts the global optimum — then it's one pass.

38

Maximum Subarray

LC 53

Find the contiguous subarray with the largest sum.

[-2,1,-3,4,-1,2,1,-5,4] → 6 ([4,−1,2,1])

Show solution

Insight: Kadane: at each element, either extend the running subarray or restart — a negative running sum can never help.

def maxSubArray(nums):
    best = cur = nums[0]
    for x in nums[1:]:
        cur = max(x, cur + x)
        best = max(best, cur)
    return best

Complexity: O(n) time, O(1) space.

39

Jump Game

LC 55

Each element is your max jump length from that index. Can you reach the last index?

[2,3,1,1,4] → True; [3,2,1,0,4] → False

Show solution

Insight: Track the furthest reachable index. If you ever stand past it, you're stuck.

def canJump(nums):
    reach = 0
    for i, jump in enumerate(nums):
        if i > reach:
            return False
        reach = max(reach, i + jump)
    return True

Complexity: O(n) time, O(1) space.

40

Gas Station

LC 134

Circular route; gas[i] gained, cost[i] to reach the next station. Find the unique valid start, or −1.

Show solution

Insight: If total gas ≥ total cost, an answer exists. Whenever the running tank goes negative, no station in that stretch can be the start — restart from the next one.

def canCompleteCircuit(gas, cost):
    if sum(gas) < sum(cost):
        return -1
    tank = start = 0
    for i in range(len(gas)):
        tank += gas[i] - cost[i]
        if tank < 0:
            start, tank = i + 1, 0
    return start

Complexity: O(n) time, O(1) space.

Dynamic Programming — 1D

Define dp[i] in words first ("best answer using the first i items"), find the recurrence, mind the base case.

41

House Robber

LC 198

Max sum of non-adjacent elements.

[2,7,9,3,1] → 12 (2+9+1)

Show solution

Insight: At each house: rob it (add to best-if-previous-skipped) or skip it (carry the best so far). Two rolling variables.

def rob(nums):
    take = skip = 0
    for x in nums:
        take, skip = skip + x, max(take, skip)
    return max(take, skip)

Complexity: O(n) time, O(1) space.

42

Coin Change

LC 322

Fewest coins (unlimited supply) to make amount, or −1.

coins=[1,2,5], amount=11 → 3 (5+5+1)

Show solution

Insight: dp[a] = fewest coins for amount a = 1 + min over coins c of dp[a−c]. Unreachable stays infinity.

def coinChange(coins, amount):
    INF = float('inf')
    dp = [0] + [INF] * amount
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a:
                dp[a] = min(dp[a], dp[a - c] + 1)
    return dp[amount] if dp[amount] != INF else -1

Complexity: O(amount · len(coins)) time, O(amount) space.

43

Longest Increasing Subsequence

LC 300

Length of the longest strictly increasing subsequence (not necessarily contiguous).

[10,9,2,5,3,7,101,18] → 4 (2,3,7,101)

Show solution

Insight: Start with the O(n²) DP (dp[i] = LIS ending at i). The upgrade: tails[k] = smallest possible tail of an increasing subsequence of length k+1 — it stays sorted, so each element binary-searches its slot.

from bisect import bisect_left

def lengthOfLIS(nums):
    tails = []
    for x in nums:
        i = bisect_left(tails, x)
        if i == len(tails):
            tails.append(x)           # extends longest
        else:
            tails[i] = x              # better (smaller) tail
    return len(tails)

Complexity: O(n log n) time, O(n) space.

44

Word Break

LC 139

Can the string be segmented into dictionary words (reusable)?

s="leetcode", dict=["leet","code"] → True

Show solution

Insight: dp[i] = "prefix s[:i] is breakable". It's breakable if some earlier breakable point j has s[j:i] in the dictionary.

def wordBreak(s, wordDict):
    words = set(wordDict)
    dp = [True] + [False] * len(s)
    for i in range(1, len(s) + 1):
        for j in range(i):
            if dp[j] and s[j:i] in words:
                dp[i] = True
                break
    return dp[-1]

Complexity: O(n²) time (plus slicing), O(n) space.

Dynamic Programming — 2D

dp over two indices — usually a grid position or a pair of string prefixes.

45

Unique Paths

LC 62

Robot moves only right/down on an m×n grid. How many paths corner to corner?

m=3, n=7 → 28

Show solution

Insight: Paths to a cell = paths from above + paths from the left. One rolling row suffices.

def uniquePaths(m, n):
    row = [1] * n
    for _ in range(m - 1):
        for j in range(1, n):
            row[j] += row[j - 1]
    return row[-1]

Complexity: O(m·n) time, O(n) space.

46

Longest Common Subsequence

LC 1143

Length of the longest subsequence common to both strings.

"abcde", "ace" → 3

Show solution

Insight: dp[i][j] = LCS of a[:i], b[:j]. Last chars match → 1 + diagonal; else best of dropping one char from either string. The template behind edit distance and many string DPs.

def longestCommonSubsequence(a, b):
    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            if a[i - 1] == b[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[-1][-1]

Complexity: O(m·n) time and space.

Matrix Manipulation

Usually no algorithm — just careful index bookkeeping. Walk through a 3×3 example before coding.

47

Rotate Image

LC 48

Rotate an n×n matrix 90° clockwise, in-place.

Show solution

Insight: Rotate = flip vertically, then transpose. Two easy in-place operations instead of fiddly four-way cycles.

def rotate(matrix):
    matrix.reverse()                  # flip rows top<->bottom
    n = len(matrix)
    for i in range(n):
        for j in range(i):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

Complexity: O(n²) time, O(1) space.

48

Spiral Matrix

LC 54

Return all elements in spiral order.

[[1,2,3],[4,5,6],[7,8,9]] → [1,2,3,6,9,8,7,4,5]

Show solution

Insight: Four shrinking boundaries; after the top row and right column, re-check bounds so single rows/columns aren't traversed twice.

def spiralOrder(matrix):
    res = []
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1
    while top <= bottom and left <= right:
        for c in range(left, right + 1):
            res.append(matrix[top][c])
        top += 1
        for r in range(top, bottom + 1):
            res.append(matrix[r][right])
        right -= 1
        if top <= bottom:
            for c in range(right, left - 1, -1):
                res.append(matrix[bottom][c])
            bottom -= 1
        if left <= right:
            for r in range(bottom, top - 1, -1):
                res.append(matrix[r][left])
            left += 1
    return res

Complexity: O(m·n) time.

Trie

A tree of characters for prefix problems: autocomplete, word dictionaries, prefix counting.

49

Implement Trie / Prefix Tree

LC 208

Support insert(word), search(word), startsWith(prefix).

Show solution

Insight: Nested dicts, one level per character; a sentinel key marks "a word ends here". That's the whole data structure.

class Trie:
    def __init__(self):
        self.root = {}

    def insert(self, word):
        node = self.root
        for c in word:
            node = node.setdefault(c, {})
        node['$'] = True                 # end-of-word marker

    def _walk(self, s):
        node = self.root
        for c in s:
            if c not in node:
                return None
            node = node[c]
        return node

    def search(self, word):
        node = self._walk(word)
        return node is not None and '$' in node

    def startsWith(self, prefix):
        return self._walk(prefix) is not None

Complexity: O(len(word)) per operation.

Test mode