Skip to main content

Blind 75 Solutions

The Blind 75 with brute-force and optimal solutions side by side — the actual answers interviewers want to hear, with the reasoning that gets you there.

Free reference · last reviewed

The Blind 75 is the classic list of 75 LeetCode problems chosen to cover every pattern interviewers actually test — arrays, strings, binary search, linked lists, trees, graphs, DP, intervals, and heaps. Work through them and you've seen the shape of almost any coding question they can ask.

Each problem has two solutions:

  1. Brute force: naive, easy-to-see approach
  2. Optimal: the actual interview answer

Language: Python 3.


Array

1. Two Sum

Given nums and target, return indices of two numbers that add to target.

Predict the pattern

Brute force: O(n²) / O(1) Check every pair.

def twoSum(nums, target):
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return [i, j]

Optimal: O(n) / O(n) Hash map: for each x, look up target - x.

def twoSum(nums, target):
    seen = {}
    for i, x in enumerate(nums):
        if target - x in seen:
            return [seen[target - x], i]
        seen[x] = i

2. Best Time to Buy and Sell Stock

Max profit from one buy + one sell.

Brute force: O(n²) / O(1) Try every (buy, sell) pair.

def maxProfit(prices):
    best = 0
    for i in range(len(prices)):
        for j in range(i + 1, len(prices)):
            best = max(best, prices[j] - prices[i])
    return best

Optimal: O(n) / O(1) Track running min price; profit = price − min so far.

def maxProfit(prices):
    lo, best = float('inf'), 0
    for p in prices:
        lo = min(lo, p)
        best = max(best, p - lo)
    return best

3. Contains Duplicate

Return True if any value appears at least twice.

Brute force: O(n²) / O(1)

def containsDuplicate(nums):
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] == nums[j]:
                return True
    return False

Optimal: O(n) / O(n)

def containsDuplicate(nums):
    return len(set(nums)) != len(nums)

4. Product of Array Except Self

output[i] = product of all elements except nums[i]. No division.

Brute force: O(n²) / O(n)

def productExceptSelf(nums):
    n = len(nums)
    out = [1] * n
    for i in range(n):
        for j in range(n):
            if i != j:
                out[i] *= nums[j]
    return out

Optimal: O(n) / O(1) extra Two passes: prefix products, then multiply by suffix on the fly.

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

5. Maximum Subarray

Largest sum of a contiguous subarray.

Brute force: O(n²) / O(1)

def maxSubArray(nums):
    best = float('-inf')
    for i in range(len(nums)):
        s = 0
        for j in range(i, len(nums)):
            s += nums[j]
            best = max(best, s)
    return best

Optimal: Kadane, O(n) / O(1) At each index, either extend or start fresh.

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

6. Maximum Product Subarray

Largest product of a contiguous subarray.

Brute force: O(n²) / O(1)

def maxProduct(nums):
    best = nums[0]
    for i in range(len(nums)):
        p = 1
        for j in range(i, len(nums)):
            p *= nums[j]
            best = max(best, p)
    return best

Optimal: O(n) / O(1) Track min and max ending here (negatives flip them).

def maxProduct(nums):
    cur_max = cur_min = best = nums[0]
    for x in nums[1:]:
        cands = (x, x * cur_max, x * cur_min)
        cur_max, cur_min = max(cands), min(cands)
        best = max(best, cur_max)
    return best

7. Find Minimum in Rotated Sorted Array

Brute force: O(n) / O(1)

def findMin(nums):
    return min(nums)

Optimal: Binary search, O(log n) / O(1)

def findMin(nums):
    lo, hi = 0, len(nums) - 1
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] > nums[hi]:
            lo = mid + 1
        else:
            hi = mid
    return nums[lo]

8. Search in Rotated Sorted Array

Brute force: O(n) / O(1)

def search(nums, target):
    for i, x in enumerate(nums):
        if x == target:
            return i
    return -1

Optimal: Binary search, O(log n) / O(1)

def search(nums, target):
    lo, hi = 0, len(nums) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:          # left half sorted
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        else:                               # right half sorted
            if nums[mid] < target <= nums[hi]:
                lo = mid + 1
            else:
                hi = mid - 1
    return -1

9. 3 Sum

All unique triplets that sum to 0.

Brute force: O(n³) / O(n)

def threeSum(nums):
    res = set()
    n = len(nums)
    for i in range(n):
        for j in range(i + 1, n):
            for k in range(j + 1, n):
                if nums[i] + nums[j] + nums[k] == 0:
                    res.add(tuple(sorted((nums[i], nums[j], nums[k]))))
    return [list(t) for t in res]

Optimal: Sort + two pointers, O(n²) / O(1)

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

10. Container With Most Water

Max area between two vertical lines.

Brute force: O(n²) / O(1)

def maxArea(height):
    best = 0
    for i in range(len(height)):
        for j in range(i + 1, len(height)):
            best = max(best, (j - i) * min(height[i], height[j]))
    return best

Optimal: Two pointers, O(n) / O(1) Move the shorter side inward.

def maxArea(height):
    l, r, best = 0, len(height) - 1, 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

Binary

11. Sum of Two Integers (no +/-)

Predict the pattern

Brute force: built-in

def getSum(a, b):
    return a + b

Optimal: bit manipulation, O(1) XOR is sum without carry; AND<<1 is the carry.

def getSum(a, b):
    mask = 0xFFFFFFFF
    while b & mask:
        a, b = (a ^ b) & mask, ((a & b) << 1) & mask
    return a if a <= 0x7FFFFFFF else ~(a ^ mask)

12. Number of 1 Bits

Brute force: O(32)

def hammingWeight(n):
    return bin(n).count('1')

Optimal: Brian Kernighan, O(set-bits) n & (n-1) clears the lowest set bit.

def hammingWeight(n):
    c = 0
    while n:
        n &= n - 1
        c += 1
    return c

13. Counting Bits

For 0..n, return list of popcount(i).

Brute force: O(n log n)

def countBits(n):
    return [bin(i).count('1') for i in range(n + 1)]

Optimal: DP, O(n) dp[i] = dp[i >> 1] + (i & 1).

def countBits(n):
    dp = [0] * (n + 1)
    for i in range(1, n + 1):
        dp[i] = dp[i >> 1] + (i & 1)
    return dp

14. Missing Number

Array contains n distinct values in [0, n]; find the missing one.

Brute force: O(n log n) sort, or O(n)/O(n) set

def missingNumber(nums):
    s = set(nums)
    for i in range(len(nums) + 1):
        if i not in s:
            return i

Optimal: sum or XOR, O(n) / O(1)

def missingNumber(nums):
    n = len(nums)
    return n * (n + 1) // 2 - sum(nums)

15. Reverse Bits

Brute force: string flip

def reverseBits(n):
    return int(bin(n)[2:].zfill(32)[::-1], 2)

Optimal: bit shift, O(32) / O(1)

def reverseBits(n):
    res = 0
    for _ in range(32):
        res = (res << 1) | (n & 1)
        n >>= 1
    return res

Dynamic Programming

16. Climbing Stairs

Take 1 or 2 steps; count ways to reach n.

Predict the pattern

Brute force: recursion, O(2ⁿ)

def climbStairs(n):
    if n <= 2: return n
    return climbStairs(n - 1) + climbStairs(n - 2)

Optimal: Fibonacci, O(n) / O(1)

def climbStairs(n):
    a, b = 1, 1
    for _ in range(n):
        a, b = b, a + b
    return a

17. Coin Change

Fewest coins to make amount, or -1.

Brute force: recursion, O(coinsᵃᵐᵗ)

def coinChange(coins, amount):
    def rec(rem):
        if rem < 0: return float('inf')
        if rem == 0: return 0
        return min(rec(rem - c) for c in coins) + 1
    ans = rec(amount)
    return ans if ans != float('inf') else -1

Optimal: bottom-up DP, O(amount·coins) / O(amount)

def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    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] != float('inf') else -1

18. Longest Increasing Subsequence

Brute force: DP, O(n²) / O(n)

def lengthOfLIS(nums):
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)

Optimal: patience sort, O(n log n) / O(n)

import bisect
def lengthOfLIS(nums):
    tails = []
    for x in nums:
        i = bisect.bisect_left(tails, x)
        if i == len(tails):
            tails.append(x)
        else:
            tails[i] = x
    return len(tails)

19. Longest Common Subsequence

Brute force: recursion, O(2^(m+n))

def longestCommonSubsequence(a, b):
    def rec(i, j):
        if i == len(a) or j == len(b): return 0
        if a[i] == b[j]: return 1 + rec(i + 1, j + 1)
        return max(rec(i + 1, j), rec(i, j + 1))
    return rec(0, 0)

Optimal: 2D DP, O(mn) / O(mn)

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

20. Word Break

Can s be segmented into dict words?

Brute force: recursion, O(2ⁿ)

def wordBreak(s, wordDict):
    words = set(wordDict)
    def rec(i):
        if i == len(s): return True
        return any(s[i:j] in words and rec(j) for j in range(i + 1, len(s) + 1))
    return rec(0)

Optimal: DP, O(n²) / O(n)

def wordBreak(s, wordDict):
    words = set(wordDict)
    dp = [False] * (len(s) + 1)
    dp[0] = True
    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]

21. Combination Sum

All unique combos summing to target (reuse allowed).

Brute force: pure backtrack with duplicate sets

def combinationSum(candidates, target):
    res = set()
    def rec(rem, path):
        if rem == 0:
            res.add(tuple(sorted(path)))
            return
        if rem < 0: return
        for c in candidates:
            rec(rem - c, path + [c])
    rec(target, [])
    return [list(t) for t in res]

Optimal: backtrack with start index, prunes dupes

def combinationSum(candidates, target):
    res, candidates = [], sorted(candidates)
    def rec(start, rem, path):
        if rem == 0:
            res.append(path[:])
            return
        for i in range(start, len(candidates)):
            if candidates[i] > rem: break
            path.append(candidates[i])
            rec(i, rem - candidates[i], path)
            path.pop()
    rec(0, target, [])
    return res

22. House Robber

Can't rob two adjacent.

Brute force: recursion, O(2ⁿ)

def rob(nums):
    def rec(i):
        if i >= len(nums): return 0
        return max(nums[i] + rec(i + 2), rec(i + 1))
    return rec(0)

Optimal: O(n) / O(1)

def rob(nums):
    prev = curr = 0
    for x in nums:
        prev, curr = curr, max(curr, prev + x)
    return curr

23. House Robber II

Houses in a circle.

Brute force: try both windows with the O(2ⁿ) recursion above (omitted).

Optimal: run House Robber I twice, O(n) / O(1)

def rob(nums):
    if len(nums) == 1: return nums[0]
    def helper(arr):
        prev = curr = 0
        for x in arr:
            prev, curr = curr, max(curr, prev + x)
        return curr
    return max(helper(nums[1:]), helper(nums[:-1]))

24. Decode Ways

'A'..'Z''1'..'26'. Count decodings of digit string.

Brute force: recursion, O(2ⁿ)

def numDecodings(s):
    def rec(i):
        if i == len(s): return 1
        if s[i] == '0': return 0
        ways = rec(i + 1)
        if i + 1 < len(s) and 10 <= int(s[i:i + 2]) <= 26:
            ways += rec(i + 2)
        return ways
    return rec(0)

Optimal: DP, O(n) / O(1)

def numDecodings(s):
    if not s or s[0] == '0': return 0
    prev2, prev1 = 1, 1
    for i in range(1, len(s)):
        cur = 0
        if s[i] != '0': cur += prev1
        if 10 <= int(s[i - 1:i + 1]) <= 26: cur += prev2
        prev2, prev1 = prev1, cur
    return prev1

25. Unique Paths

m x n grid, only right/down.

Brute force: recursion, O(2^(m+n))

def uniquePaths(m, n):
    def rec(i, j):
        if i == m - 1 or j == n - 1: return 1
        return rec(i + 1, j) + rec(i, j + 1)
    return rec(0, 0)

Optimal: DP, O(mn) / O(n)

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

(Combinatorial closed form: C(m+n-2, m-1).)


26. Jump Game

Can you reach the last index?

Brute force: recursion, O(2ⁿ)

def canJump(nums):
    def rec(i):
        if i >= len(nums) - 1: return True
        for step in range(1, nums[i] + 1):
            if rec(i + step): return True
        return False
    return rec(0)

Optimal: greedy, O(n) / O(1)

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

Graph

27. Clone Graph

Predict the pattern

Brute force = optimal: BFS/DFS with a map.

Optimal: DFS, O(V+E) / O(V)

def cloneGraph(node):
    if not node: return None
    cache = {}
    def dfs(n):
        if n in cache: return cache[n]
        copy = Node(n.val)
        cache[n] = copy
        for nb in n.neighbors:
            copy.neighbors.append(dfs(nb))
        return copy
    return dfs(node)

28. Course Schedule

Detect cycle in directed graph.

Brute force: DFS from every node (still O(V+E) if memoized; without memo, exponential).

def canFinish(n, prereqs):
    g = [[] for _ in range(n)]
    for a, b in prereqs: g[b].append(a)
    def has_cycle(u, seen):
        if u in seen: return True
        seen.add(u)
        for v in g[u]:
            if has_cycle(v, seen): return True
        seen.remove(u)
        return False
    return not any(has_cycle(i, set()) for i in range(n))

Optimal: Kahn's BFS topo sort, O(V+E)

from collections import deque
def canFinish(n, prereqs):
    g = [[] for _ in range(n)]
    indeg = [0] * n
    for a, b in prereqs:
        g[b].append(a); indeg[a] += 1
    q = deque(i for i in range(n) if indeg[i] == 0)
    done = 0
    while q:
        u = q.popleft(); done += 1
        for v in g[u]:
            indeg[v] -= 1
            if indeg[v] == 0: q.append(v)
    return done == n

29. Pacific Atlantic Water Flow

Cells that can flow to both oceans.

Brute force: DFS from each cell, O((mn)²) For each cell, DFS to see if it reaches both oceans.

Optimal: DFS from oceans inward, O(mn)

def pacificAtlantic(heights):
    if not heights: return []
    m, n = len(heights), len(heights[0])
    pac, atl = set(), set()
    def dfs(i, j, seen):
        seen.add((i, j))
        for di, dj in ((1,0),(-1,0),(0,1),(0,-1)):
            ni, nj = i + di, j + dj
            if 0 <= ni < m and 0 <= nj < n and (ni, nj) not in seen \
               and heights[ni][nj] >= heights[i][j]:
                dfs(ni, nj, seen)
    for i in range(m):
        dfs(i, 0, pac); dfs(i, n - 1, atl)
    for j in range(n):
        dfs(0, j, pac); dfs(m - 1, j, atl)
    return [list(c) for c in pac & atl]

30. Number of Islands

Count connected components of '1's.

Brute force = optimal: DFS/BFS flood fill, O(mn).

def numIslands(grid):
    if not grid: return 0
    m, n = len(grid), len(grid[0])
    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
            return
        grid[i][j] = '0'
        for di, dj in ((1,0),(-1,0),(0,1),(0,-1)):
            dfs(i + di, j + dj)
    c = 0
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                dfs(i, j); c += 1
    return c

31. Longest Consecutive Sequence

Brute force: sort, O(n log n)

def longestConsecutive(nums):
    if not nums: return 0
    nums = sorted(set(nums))
    best = run = 1
    for i in range(1, len(nums)):
        if nums[i] == nums[i - 1] + 1:
            run += 1; best = max(best, run)
        else:
            run = 1
    return best

Optimal: set, O(n) / O(n) Only start counting from sequence beginnings.

def longestConsecutive(nums):
    s = set(nums); best = 0
    for x in s:
        if x - 1 not in s:
            y = x
            while y + 1 in s: y += 1
            best = max(best, y - x + 1)
    return best

32. Alien Dictionary

Topological order from sorted alien words.

Brute force: try all permutations, O(k!·...) (impractical).

Optimal: topo sort, O(C) where C = total chars

from collections import defaultdict, deque
def alienOrder(words):
    g = defaultdict(set)
    indeg = {c: 0 for w in words for c in w}
    for w1, w2 in zip(words, words[1:]):
        for a, b in zip(w1, w2):
            if a != b:
                if b not in g[a]:
                    g[a].add(b); indeg[b] += 1
                break
        else:
            if len(w1) > len(w2): return ""
    q = deque([c for c in indeg if indeg[c] == 0])
    res = []
    while q:
        c = q.popleft(); res.append(c)
        for nb in g[c]:
            indeg[nb] -= 1
            if indeg[nb] == 0: q.append(nb)
    return "".join(res) if len(res) == len(indeg) else ""

33. Graph Valid Tree

n nodes, edges form a tree iff connected and len(edges) == n-1.

Brute force: DFS check connectivity + cycle

def validTree(n, edges):
    if len(edges) != n - 1: return False
    g = [[] for _ in range(n)]
    for a, b in edges: g[a].append(b); g[b].append(a)
    seen = set()
    def dfs(u):
        seen.add(u)
        for v in g[u]:
            if v not in seen: dfs(v)
    dfs(0)
    return len(seen) == n

Optimal: Union-Find, O(n·α(n))

def validTree(n, edges):
    if len(edges) != n - 1: return False
    parent = list(range(n))
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x
    for a, b in edges:
        ra, rb = find(a), find(b)
        if ra == rb: return False
        parent[ra] = rb
    return True

34. Number of Connected Components

Brute force: DFS from each unvisited node, O(V+E) (same as optimal).

def countComponents(n, edges):
    g = [[] for _ in range(n)]
    for a, b in edges: g[a].append(b); g[b].append(a)
    seen, c = set(), 0
    def dfs(u):
        seen.add(u)
        for v in g[u]:
            if v not in seen: dfs(v)
    for i in range(n):
        if i not in seen: dfs(i); c += 1
    return c

Optimal: Union-Find, O((V+E)·α)

def countComponents(n, edges):
    parent = list(range(n))
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]; x = parent[x]
        return x
    c = n
    for a, b in edges:
        ra, rb = find(a), find(b)
        if ra != rb: parent[ra] = rb; c -= 1
    return c

Interval

35. Insert Interval

Predict the pattern

Brute force: append, sort, then merge, O(n log n)

def insert(intervals, newInterval):
    intervals = sorted(intervals + [newInterval])
    merged = [intervals[0]]
    for s, e in intervals[1:]:
        if s <= merged[-1][1]:
            merged[-1][1] = max(merged[-1][1], e)
        else:
            merged.append([s, e])
    return merged

Optimal: O(n) single pass

def insert(intervals, newInterval):
    res, i, n = [], 0, len(intervals)
    while i < n and intervals[i][1] < newInterval[0]:
        res.append(intervals[i]); i += 1
    while i < n and intervals[i][0] <= newInterval[1]:
        newInterval = [min(newInterval[0], intervals[i][0]),
                       max(newInterval[1], intervals[i][1])]
        i += 1
    res.append(newInterval)
    res.extend(intervals[i:])
    return res

36. Merge Intervals

Brute force: repeatedly merge until stable, O(n²) (slow).

Optimal: sort + sweep, O(n log n)

def merge(intervals):
    intervals.sort()
    out = [intervals[0]]
    for s, e in intervals[1:]:
        if s <= out[-1][1]:
            out[-1][1] = max(out[-1][1], e)
        else:
            out.append([s, e])
    return out

37. Non-overlapping Intervals

Min removals to make non-overlapping.

Brute force: DP on subsets, O(2ⁿ) (skip).

Optimal: greedy by end time, O(n log n)

def eraseOverlapIntervals(intervals):
    intervals.sort(key=lambda x: x[1])
    end = float('-inf'); kept = 0
    for s, e in intervals:
        if s >= end:
            end = e; kept += 1
    return len(intervals) - kept

38. Meeting Rooms

Can a single person attend all?

Brute force: check every pair, O(n²)

def canAttendMeetings(intervals):
    for i in range(len(intervals)):
        for j in range(i + 1, len(intervals)):
            a, b = intervals[i], intervals[j]
            if a[0] < b[1] and b[0] < a[1]: return False
    return True

Optimal: sort, O(n log n)

def canAttendMeetings(intervals):
    intervals.sort()
    for i in range(1, len(intervals)):
        if intervals[i][0] < intervals[i - 1][1]: return False
    return True

39. Meeting Rooms II

Min rooms needed.

Brute force: timeline tally, O((max_t)·n) (slow for big times).

Optimal: min-heap of end times, O(n log n)

import heapq
def minMeetingRooms(intervals):
    intervals.sort()
    heap = []
    for s, e in intervals:
        if heap and heap[0] <= s:
            heapq.heappop(heap)
        heapq.heappush(heap, e)
    return len(heap)

Linked List

40. Reverse Linked List

Predict the pattern

Brute force: stack into new list, O(n) / O(n)

def reverseList(head):
    stack = []
    while head:
        stack.append(head); head = head.next
    dummy = ListNode(0); cur = dummy
    while stack:
        cur.next = stack.pop(); cur = cur.next
    cur.next = None
    return dummy.next

Optimal: in place, O(n) / O(1)

def reverseList(head):
    prev = None
    while head:
        head.next, prev, head = prev, head, head.next
    return prev

41. Detect Cycle

Brute force: hash set, O(n) / O(n)

def hasCycle(head):
    seen = set()
    while head:
        if head in seen: return True
        seen.add(head); head = head.next
    return False

Optimal: Floyd's tortoise & hare, O(n) / O(1)

def hasCycle(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
        if slow is fast: return True
    return False

42. Merge Two Sorted Lists

Brute force: extract values, sort, rebuild, O((m+n)log(m+n))

def mergeTwoLists(a, b):
    vals = []
    while a: vals.append(a.val); a = a.next
    while b: vals.append(b.val); b = b.next
    dummy = cur = ListNode(0)
    for v in sorted(vals):
        cur.next = ListNode(v); cur = cur.next
    return dummy.next

Optimal: two pointers, O(m+n) / O(1)

def mergeTwoLists(a, b):
    dummy = cur = ListNode(0)
    while a and b:
        if a.val < b.val:
            cur.next = a; a = a.next
        else:
            cur.next = b; b = b.next
        cur = cur.next
    cur.next = a or b
    return dummy.next

43. Merge K Sorted Lists

Brute force: collect all + sort, O(N log N)

def mergeKLists(lists):
    vals = []
    for h in lists:
        while h: vals.append(h.val); h = h.next
    dummy = cur = ListNode(0)
    for v in sorted(vals):
        cur.next = ListNode(v); cur = cur.next
    return dummy.next

Optimal: min-heap, O(N log k)

import heapq
def mergeKLists(lists):
    heap = []
    for i, h in enumerate(lists):
        if h: heapq.heappush(heap, (h.val, i, h))
    dummy = cur = ListNode(0)
    while heap:
        v, i, n = heapq.heappop(heap)
        cur.next = n; cur = n
        if n.next:
            heapq.heappush(heap, (n.next.val, i, n.next))
    return dummy.next

44. Remove Nth Node From End

Brute force: two passes, O(n)

def removeNthFromEnd(head, n):
    length = 0; cur = head
    while cur: length += 1; cur = cur.next
    dummy = ListNode(0, head); cur = dummy
    for _ in range(length - n): cur = cur.next
    cur.next = cur.next.next
    return dummy.next

Optimal: one pass two pointers, O(n) / O(1)

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

45. Reorder List

L0→L1→...→Ln-1→LnL0→Ln→L1→Ln-1→...

Brute force: array indexing, O(n) / O(n)

def reorderList(head):
    arr = []
    cur = head
    while cur: arr.append(cur); cur = cur.next
    l, r = 0, len(arr) - 1
    while l < r:
        arr[l].next = arr[r]
        l += 1
        if l == r: break
        arr[r].next = arr[l]
        r -= 1
    arr[l].next = None

Optimal: mid + reverse + merge, O(n) / O(1)

def reorderList(head):
    slow = fast = head
    while fast and fast.next:
        slow, fast = slow.next, fast.next.next
    prev, cur = None, slow.next
    slow.next = None
    while cur:
        cur.next, prev, cur = prev, cur, cur.next
    a, b = head, prev
    while b:
        a.next, b.next, a, b = b, a.next, a.next, b.next

Matrix

46. Set Matrix Zeroes

Predict the pattern

Brute force: copy matrix, O(mn) / O(mn)

def setZeroes(m):
    rows, cols = set(), set()
    for i in range(len(m)):
        for j in range(len(m[0])):
            if m[i][j] == 0: rows.add(i); cols.add(j)
    for i in range(len(m)):
        for j in range(len(m[0])):
            if i in rows or j in cols: m[i][j] = 0

Optimal: first row/col as flags, O(mn) / O(1)

def setZeroes(matrix):
    m, n = len(matrix), len(matrix[0])
    first_row = any(matrix[0][j] == 0 for j in range(n))
    first_col = any(matrix[i][0] == 0 for i in range(m))
    for i in range(1, m):
        for j in range(1, n):
            if matrix[i][j] == 0:
                matrix[i][0] = matrix[0][j] = 0
    for i in range(1, m):
        for j in range(1, n):
            if matrix[i][0] == 0 or matrix[0][j] == 0:
                matrix[i][j] = 0
    if first_row:
        for j in range(n): matrix[0][j] = 0
    if first_col:
        for i in range(m): matrix[i][0] = 0

47. Spiral Matrix

Brute force = optimal: layer walking, O(mn).

def spiralOrder(matrix):
    res = []
    while matrix:
        res += matrix.pop(0)
        matrix = list(zip(*matrix))[::-1]
    return res

Cleaner pointer version:

def spiralOrder(matrix):
    res = []
    top, bot = 0, len(matrix) - 1
    lft, rgt = 0, len(matrix[0]) - 1
    while top <= bot and lft <= rgt:
        for j in range(lft, rgt + 1): res.append(matrix[top][j])
        top += 1
        for i in range(top, bot + 1): res.append(matrix[i][rgt])
        rgt -= 1
        if top <= bot:
            for j in range(rgt, lft - 1, -1): res.append(matrix[bot][j])
            bot -= 1
        if lft <= rgt:
            for i in range(bot, top - 1, -1): res.append(matrix[i][lft])
            lft += 1
    return res

48. Rotate Image (90° clockwise, in place)

Brute force: copy to new matrix, O(n²) / O(n²)

def rotate(m):
    n = len(m)
    tmp = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            tmp[j][n - 1 - i] = m[i][j]
    for i in range(n):
        m[i] = tmp[i]

Optimal: transpose + reverse rows, O(n²) / O(1)

def rotate(m):
    n = len(m)
    for i in range(n):
        for j in range(i + 1, n):
            m[i][j], m[j][i] = m[j][i], m[i][j]
    for row in m:
        row.reverse()

Brute force = standard DFS backtracking, O(mn·4^L)

def exist(board, word):
    m, n = len(board), len(board[0])
    def dfs(i, j, k):
        if k == len(word): return True
        if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]:
            return False
        board[i][j] = '#'
        found = any(dfs(i + di, j + dj, k + 1)
                    for di, dj in ((1,0),(-1,0),(0,1),(0,-1)))
        board[i][j] = word[k]
        return found
    return any(dfs(i, j, 0) for i in range(m) for j in range(n))

Optimal: same DFS + start-cell pruning by frequency Reverse word if the last char is rarer than the first, so DFS prunes earlier.

def exist(board, word):
    from collections import Counter
    cnt = Counter(c for row in board for c in row)
    if Counter(word) - cnt: return False
    if cnt[word[0]] > cnt[word[-1]]: word = word[::-1]
    # then same DFS as brute force
    ...

String

50. Longest Substring Without Repeating Characters

Predict the pattern

Brute force: O(n³) check every substring.

def lengthOfLongestSubstring(s):
    best = 0
    for i in range(len(s)):
        for j in range(i, len(s)):
            if len(set(s[i:j + 1])) == j - i + 1:
                best = max(best, j - i + 1)
    return best

Optimal: sliding window, O(n)

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

51. Longest Repeating Character Replacement

At most k replacements; longest single-letter run.

Brute force: O(n²·26) try each window.

Optimal: sliding window, O(n) Window valid if (window_len − max_count) ≤ k.

def characterReplacement(s, k):
    from collections import Counter
    cnt = Counter(); l = best = max_f = 0
    for r, c in enumerate(s):
        cnt[c] += 1
        max_f = max(max_f, cnt[c])
        if r - l + 1 - max_f > k:
            cnt[s[l]] -= 1; l += 1
        best = max(best, r - l + 1)
    return best

52. Minimum Window Substring

Smallest window of s containing all chars of t.

Brute force: O(n²) every substring, check containment.

Optimal: sliding window, O(n + m)

from collections import Counter
def minWindow(s, t):
    need = Counter(t); missing = len(t)
    l = best_l = 0; best = ""
    for r, c in enumerate(s, 1):
        if need[c] > 0: missing -= 1
        need[c] -= 1
        if missing == 0:
            while need[s[l]] < 0:
                need[s[l]] += 1; l += 1
            if not best or r - l < len(best):
                best = s[l:r]
            need[s[l]] += 1; missing += 1; l += 1
    return best

53. Valid Anagram

Brute force: sort both, O(n log n)

def isAnagram(s, t):
    return sorted(s) == sorted(t)

Optimal: counter, O(n)

from collections import Counter
def isAnagram(s, t):
    return Counter(s) == Counter(t)

54. Group Anagrams

Brute force: pairwise compare, O(n²·k)

def groupAnagrams(strs):
    out = []
    used = [False] * len(strs)
    for i, s in enumerate(strs):
        if used[i]: continue
        group = [s]; used[i] = True
        for j in range(i + 1, len(strs)):
            if not used[j] and sorted(strs[j]) == sorted(s):
                group.append(strs[j]); used[j] = True
        out.append(group)
    return out

Optimal: hash by sorted key, O(n·k log k)

from collections import defaultdict
def groupAnagrams(strs):
    groups = defaultdict(list)
    for s in strs:
        groups[''.join(sorted(s))].append(s)
    return list(groups.values())

55. Valid Parentheses

Brute force: repeated replace, O(n²)

def isValid(s):
    while '()' in s or '[]' in s or '{}' in s:
        s = s.replace('()', '').replace('[]', '').replace('{}', '')
    return s == ''

Optimal: stack, O(n)

def isValid(s):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for c in s:
        if c in pairs:
            if not stack or stack.pop() != pairs[c]: return False
        else:
            stack.append(c)
    return not stack

56. Valid Palindrome

Brute force: clean + reverse, O(n) / O(n)

def isPalindrome(s):
    cleaned = [c.lower() for c in s if c.isalnum()]
    return cleaned == cleaned[::-1]

Optimal: two pointers, O(n) / O(1)

def isPalindrome(s):
    l, r = 0, len(s) - 1
    while l < r:
        while l < r and not s[l].isalnum(): l += 1
        while l < r and not s[r].isalnum(): r -= 1
        if s[l].lower() != s[r].lower(): return False
        l += 1; r -= 1
    return True

57. Longest Palindromic Substring

Brute force: O(n³) check every substring.

def longestPalindrome(s):
    best = ""
    for i in range(len(s)):
        for j in range(i, len(s)):
            sub = s[i:j + 1]
            if sub == sub[::-1] and len(sub) > len(best):
                best = sub
    return best

Optimal: expand around center, O(n²) / O(1)

def longestPalindrome(s):
    def expand(l, r):
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1; r += 1
        return s[l + 1:r]
    best = ""
    for i in range(len(s)):
        for cand in (expand(i, i), expand(i, i + 1)):
            if len(cand) > len(best): best = cand
    return best

(Manacher's O(n) is the textbook fastest but rarely required.)


58. Palindromic Substrings (count)

Brute force: O(n³)

def countSubstrings(s):
    c = 0
    for i in range(len(s)):
        for j in range(i, len(s)):
            sub = s[i:j + 1]
            if sub == sub[::-1]: c += 1
    return c

Optimal: expand around center, O(n²) / O(1)

def countSubstrings(s):
    def count(l, r):
        c = 0
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1; r += 1; c += 1
        return c
    return sum(count(i, i) + count(i, i + 1) for i in range(len(s)))

59. Encode and Decode Strings

Brute force: pick a separator not in any string (fragile).

Optimal: length-prefix encoding

def encode(strs):
    return ''.join(f"{len(s)}#{s}" for s in strs)

def decode(s):
    res, i = [], 0
    while i < len(s):
        j = s.index('#', i)
        n = int(s[i:j])
        res.append(s[j + 1:j + 1 + n])
        i = j + 1 + n
    return res

Tree

60. Maximum Depth of Binary Tree

Predict the pattern

Brute force = optimal: DFS recursion, O(n).

def maxDepth(root):
    if not root: return 0
    return 1 + max(maxDepth(root.left), maxDepth(root.right))

BFS variant:

from collections import deque
def maxDepth(root):
    if not root: return 0
    q = deque([root]); d = 0
    while q:
        d += 1
        for _ in range(len(q)):
            n = q.popleft()
            if n.left: q.append(n.left)
            if n.right: q.append(n.right)
    return d

61. Same Tree

Brute force = optimal: recursive compare, O(n).

def isSameTree(p, q):
    if not p and not q: return True
    if not p or not q or p.val != q.val: return False
    return isSameTree(p.left, q.left) and isSameTree(p.right, q.right)

62. Invert Binary Tree

Brute force = optimal, O(n).

def invertTree(root):
    if not root: return None
    root.left, root.right = invertTree(root.right), invertTree(root.left)
    return root

63. Binary Tree Maximum Path Sum

Brute force: for each node, compute best path through it, O(n²)

def maxPathSum(root):
    best = float('-inf')
    def max_arm(n):
        if not n: return 0
        return max(0, n.val + max(max_arm(n.left), max_arm(n.right)))
    def dfs(n):
        nonlocal best
        if not n: return
        best = max(best, n.val + max_arm(n.left) + max_arm(n.right))
        dfs(n.left); dfs(n.right)
    dfs(root)
    return best

Optimal: single DFS, O(n)

def maxPathSum(root):
    best = float('-inf')
    def dfs(n):
        nonlocal best
        if not n: return 0
        l = max(0, dfs(n.left))
        r = max(0, dfs(n.right))
        best = max(best, n.val + l + r)
        return n.val + max(l, r)
    dfs(root)
    return best

64. Binary Tree Level Order Traversal

Brute force: DFS with level index, O(n)

def levelOrder(root):
    res = []
    def dfs(n, d):
        if not n: return
        if d == len(res): res.append([])
        res[d].append(n.val)
        dfs(n.left, d + 1); dfs(n.right, d + 1)
    dfs(root, 0)
    return res

Optimal: BFS, O(n)

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

65. Serialize and Deserialize Binary Tree

Brute force: store inorder + preorder, then rebuild, O(n²) rebuild.

Optimal: preorder w/ null markers, O(n)

class Codec:
    def serialize(self, root):
        out = []
        def dfs(n):
            if not n: out.append('#'); return
            out.append(str(n.val)); dfs(n.left); dfs(n.right)
        dfs(root)
        return ','.join(out)

    def deserialize(self, data):
        it = iter(data.split(','))
        def build():
            v = next(it)
            if v == '#': return None
            n = TreeNode(int(v))
            n.left = build(); n.right = build()
            return n
        return build()

66. Subtree of Another Tree

Brute force: for each node, run sameTree, O(m·n)

def isSubtree(root, sub):
    def same(a, b):
        if not a and not b: return True
        if not a or not b or a.val != b.val: return False
        return same(a.left, b.left) and same(a.right, b.right)
    if not root: return False
    return same(root, sub) or isSubtree(root.left, sub) or isSubtree(root.right, sub)

Optimal: serialize + substring, O(m+n)

def isSubtree(root, sub):
    def ser(n):
        if not n: return '#'
        return f'^{n.val},{ser(n.left)},{ser(n.right)}'
    return ser(sub) in ser(root)

67. Build Tree from Preorder + Inorder

Brute force: O(n²): linear search for root each call.

def buildTree(preorder, inorder):
    if not preorder: return None
    root = TreeNode(preorder[0])
    i = inorder.index(preorder[0])
    root.left = buildTree(preorder[1:1 + i], inorder[:i])
    root.right = buildTree(preorder[1 + i:], inorder[i + 1:])
    return root

Optimal: index map + pointers, O(n)

def buildTree(preorder, inorder):
    idx = {v: i for i, v in enumerate(inorder)}
    self_i = 0
    def build(l, r):
        nonlocal self_i
        if l > r: return None
        v = preorder[self_i]; self_i += 1
        n = TreeNode(v)
        n.left = build(l, idx[v] - 1)
        n.right = build(idx[v] + 1, r)
        return n
    return build(0, len(inorder) - 1)

68. Validate BST

Brute force: for each node verify whole subtree, O(n²)

Optimal: inorder traversal must be strictly increasing, O(n)

def isValidBST(root):
    prev = float('-inf'); ok = True
    def dfs(n):
        nonlocal prev, ok
        if not n or not ok: return
        dfs(n.left)
        if n.val <= prev: ok = False; return
        prev = n.val
        dfs(n.right)
    dfs(root)
    return ok

Or with bounds:

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

69. Kth Smallest in BST

Brute force: collect inorder, return k-1, O(n) / O(n)

def kthSmallest(root, k):
    arr = []
    def dfs(n):
        if not n: return
        dfs(n.left); arr.append(n.val); dfs(n.right)
    dfs(root)
    return arr[k - 1]

Optimal: iterative inorder, stop at k, O(h+k)

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

70. LCA of BST

Brute force: find paths, compare, O(n)

Optimal: descend by BST property, O(h)

def lowestCommonAncestor(root, p, q):
    while root:
        if p.val < root.val and q.val < root.val:
            root = root.left
        elif p.val > root.val and q.val > root.val:
            root = root.right
        else:
            return root

71. Implement Trie

Brute force: store all words in a set, O(L) per op but no prefix search.

Optimal: trie nodes, O(L) per op

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

    def insert(self, word):
        cur = self.root
        for c in word:
            cur = cur.setdefault(c, {})
        cur['$'] = True

    def search(self, word):
        cur = self.root
        for c in word:
            if c not in cur: return False
            cur = cur[c]
        return '$' in cur

    def startsWith(self, prefix):
        cur = self.root
        for c in prefix:
            if c not in cur: return False
            cur = cur[c]
        return True

72. Add and Search Word (. wildcard)

Brute force: list of words; on search, regex match each, O(N·L) per query.

Optimal: trie + DFS for ., O(L) avg / O(26^L) worst

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

    def addWord(self, word):
        cur = self.root
        for c in word:
            cur = cur.setdefault(c, {})
        cur['$'] = True

    def search(self, word):
        def dfs(node, i):
            if i == len(word): return '$' in node
            c = word[i]
            if c == '.':
                return any(k != '$' and dfs(node[k], i + 1) for k in node)
            return c in node and dfs(node[c], i + 1)
        return dfs(self.root, 0)

73. Word Search II

Brute force: Word Search per word, O(W·mn·4^L)

Optimal: single DFS using trie of words, O(mn·4^L)

def findWords(board, words):
    root = {}
    for w in words:
        cur = root
        for c in w:
            cur = cur.setdefault(c, {})
        cur['$'] = w

    m, n = len(board), len(board[0])
    res = []
    def dfs(i, j, node):
        c = board[i][j]
        if c not in node: return
        nxt = node[c]
        if '$' in nxt:
            res.append(nxt.pop('$'))
        board[i][j] = '#'
        for di, dj in ((1,0),(-1,0),(0,1),(0,-1)):
            ni, nj = i + di, j + dj
            if 0 <= ni < m and 0 <= nj < n:
                dfs(ni, nj, nxt)
        board[i][j] = c
    for i in range(m):
        for j in range(n):
            dfs(i, j, root)
    return res

Heap

74. Top K Frequent Elements

Predict the pattern

Brute force: count + sort, O(n log n)

from collections import Counter
def topKFrequent(nums, k):
    return [x for x, _ in Counter(nums).most_common(k)]

Optimal: bucket sort, O(n)

from collections import Counter
def topKFrequent(nums, k):
    cnt = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]
    for x, c in cnt.items():
        buckets[c].append(x)
    res = []
    for c in range(len(buckets) - 1, 0, -1):
        for x in buckets[c]:
            res.append(x)
            if len(res) == k: return res

(Or min-heap of size k → O(n log k).)


75. Find Median from Data Stream

Brute force: keep sorted list, O(n) insert / O(1) median

import bisect
class MedianFinder:
    def __init__(self):
        self.arr = []
    def addNum(self, n):
        bisect.insort(self.arr, n)
    def findMedian(self):
        n = len(self.arr); m = n // 2
        return self.arr[m] if n % 2 else (self.arr[m - 1] + self.arr[m]) / 2

Optimal: two heaps, O(log n) insert / O(1) median Left = max-heap (lower half), right = min-heap (upper half).

import heapq
class MedianFinder:
    def __init__(self):
        self.lo = []   # max-heap (store negatives)
        self.hi = []   # min-heap

    def addNum(self, n):
        heapq.heappush(self.lo, -n)
        heapq.heappush(self.hi, -heapq.heappop(self.lo))
        if len(self.hi) > len(self.lo):
            heapq.heappush(self.lo, -heapq.heappop(self.hi))

    def findMedian(self):
        if len(self.lo) > len(self.hi):
            return -self.lo[0]
        return (-self.lo[0] + self.hi[0]) / 2

Quick reference table

#ProblemOptimal timeKey trick
1Two SumO(n)hash complement
2Best Time Buy/SellO(n)running min
3Contains DuplicateO(n)set
4Product Except SelfO(n)prefix × suffix
5Max SubarrayO(n)Kadane
6Max Product SubarrayO(n)min+max DP
7Min in RotatedO(log n)binary search
8Search RotatedO(log n)binary search
93SumO(n²)sort + 2 ptrs
10Container WaterO(n)2 ptrs
11Sum 2 IntsO(1)XOR + carry
12Number of 1 BitsO(b)n & (n-1)
13Counting BitsO(n)dp[i>>1]
14Missing NumberO(n)gauss sum
15Reverse BitsO(32)shift loop
16Climb StairsO(n)fib
17Coin ChangeO(amt·k)DP
18LISO(n log n)patience
19LCSO(mn)2D DP
20Word BreakO(n²)DP + set
21Combination SumO(2^n)backtrack
22House RobberO(n)rolling DP
23House Robber IIO(n)run I twice
24Decode WaysO(n)DP
25Unique PathsO(mn)DP / C(n,k)
26Jump GameO(n)greedy reach
27Clone GraphO(V+E)DFS+map
28Course ScheduleO(V+E)Kahn topo
29Pac Atl WaterO(mn)reverse BFS
30Num IslandsO(mn)flood fill
31Longest ConsecO(n)set starts
32Alien DictO(C)topo sort
33Valid TreeO(n α)union-find
34Connected CompO(n α)union-find
35Insert IntervalO(n)3 phase
36Merge IntervalsO(n log n)sort sweep
37Non-overlapO(n log n)greedy end
38Meeting RoomsO(n log n)sort
39Meeting Rooms IIO(n log n)min-heap
40Reverse ListO(n)iter prev
41Cycle DetectO(n)Floyd
42Merge 2 ListsO(n)2 ptrs
43Merge K ListsO(N log k)heap
44Remove Nth EndO(n)2 ptrs offset
45Reorder ListO(n)mid+rev+merge
46Set ZeroesO(mn)first row/col
47Spiral MatrixO(mn)layer walk
48Rotate ImageO(n²)transpose+rev
49Word SearchO(mn·4^L)DFS+backtrack
50Longest Sub No RepO(n)sliding window
51Char ReplacementO(n)window+max-cnt
52Min Window SubO(n)window + counts
53Valid AnagramO(n)counter
54Group AnagramsO(nk log k)sort-key hash
55Valid ParensO(n)stack
56Valid PalindromeO(n)2 ptrs
57Longest Palin SubO(n²)expand center
58Count PalindromesO(n²)expand center
59Encode/Decode StrO(n)length-prefix
60Max DepthO(n)DFS
61Same TreeO(n)DFS
62Invert TreeO(n)swap rec
63Max Path SumO(n)DFS + nonlocal
64Level OrderO(n)BFS
65Serialize TreeO(n)preorder + #
66SubtreeO(m+n)serialize match
67Build from Pre+InO(n)inorder index
68Valid BSTO(n)inorder mono
69Kth Smallest BSTO(h+k)iter inorder
70LCA of BSTO(h)BST descent
71TrieO(L)dict of dicts
72Add+Search WordO(L)trie + DFS
73Word Search IIO(mn·4^L)trie + DFS
74Top K FrequentO(n)bucket sort
75Median StreamO(log n)2 heaps