← Coding Interview Prep ← DS & Algorithms

Advanced DSA Patterns

Graphs, Dynamic Programming, Backtracking, Tries, Greedy, Sliding Window, Bit Manipulation — the patterns that separate good from great.

9
Topics
30+
Algorithms
50+
Code Snippets

Graphs

Representation, traversal, shortest path, topological sort, Union-Find

Graph Representation
EasyFoundation
Adjacency list (sparse) vs adjacency matrix (dense). Know when to use each.
click to expand
Graph: Adjacency List: Adjacency Matrix: 0 --- 1 0: [1, 2] 0 1 2 3 | / | 1: [0, 2, 3] 0 [0, 1, 1, 0] | / | 2: [0, 1] 1 [1, 0, 1, 1] 2 3 3: [1] 2 [1, 1, 0, 0] 3 [0, 1, 0, 0]
# Adjacency List (most common for interviews)
from collections import defaultdict

graph = defaultdict(list)
edges = [[0,1], [0,2], [1,2], [1,3]]
for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)    # remove for directed graph

# Weighted graph
graph[u].append((v, weight))

# Adjacency Matrix
n = 4
matrix = [[0] * n for _ in range(n)]
for u, v in edges:
    matrix[u][v] = 1
    matrix[v][u] = 1
Adjacency ListAdjacency Matrix
SpaceO(V + E)O(V²)
Check edge existsO(degree)O(1)
Get neighborsO(degree)O(V)
Best forSparse graphsDense graphs
Key: Use adjacency list for interviews (almost always). Use defaultdict(list) for clean code.
Graph DFS & BFS
MediumTraversal
DFS for path finding & cycle detection. BFS for shortest path in unweighted graphs.
click to expand
DFS (Recursive)
def dfs(graph, node, visited):
    visited.add(node)
    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

# Count connected components
def count_components(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)
    visited = set()
    count = 0
    for i in range(n):
        if i not in visited:
            dfs(graph, i, visited)
            count += 1
    return count
BFS (Shortest Path)
from collections import deque

def bfs_shortest(graph, start, end):
    queue = deque([(start, 0)])
    visited = {start}
    while queue:
        node, dist = queue.popleft()
        if node == end:
            return dist
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, dist+1))
    return -1  # not reachable
Key: BFS gives shortest path in unweighted graphs. Always add to visited WHEN ENQUEUEING, not when dequeuing (avoids duplicates).
Topological Sort
MediumDAG Only
Linear ordering of vertices where u comes before v for every edge (u→v). Only for DAGs.
click to expand
Course prerequisites: Topological order: 0 → 1 → 3 0, 2, 1, 3, 4 0 → 2 → 3 (take 0 first, then 1 or 2, etc.) 2 → 4
# Kahn's Algorithm (BFS - preferred for interviews)
from collections import deque, defaultdict

def topological_sort(num_courses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * num_courses

    for course, prereq in prerequisites:
        graph[prereq].append(course)
        in_degree[course] += 1

    # Start with nodes that have no prerequisites
    queue = deque(i for i in range(num_courses) if in_degree[i] == 0)
    order = []

    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    if len(order) == num_courses:
        return order       # valid ordering
    return []              # cycle exists!
Time
O(V + E)
Space
O(V + E)
Key: If result length < total nodes → cycle exists. This is the "Course Schedule" problem on LeetCode. In-degree = number of incoming edges.
Dijkstra's Shortest Path
MediumWeighted Graph
Shortest path in weighted graph with non-negative edges. Uses min-heap (priority queue).
click to expand
1 A ——→ B | ↑ \ 4| 2| \1 ↓ | ↓ C ——→ D → E 3 2 Shortest from A: A→B(1), A→B→E(2), A→B→D(3)... not A→C→D(7)
import heapq
from collections import defaultdict

def dijkstra(graph, start):
    # graph[u] = [(v, weight), ...]
    dist = {start: 0}
    heap = [(0, start)]       # (distance, node)

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist.get(u, float('inf')):
            continue           # skip outdated entry
        for v, w in graph[u]:
            new_dist = d + w
            if new_dist < dist.get(v, float('inf')):
                dist[v] = new_dist
                heapq.heappush(heap, (new_dist, v))

    return dist  # {node: shortest_distance}

# Example: Network Delay Time
def network_delay(times, n, k):
    graph = defaultdict(list)
    for u, v, w in times:
        graph[u].append((v, w))
    dist = dijkstra(graph, k)
    if len(dist) == n:
        return max(dist.values())
    return -1
Time
O((V+E) log V)
Space
O(V)
Limitation: Does NOT work with negative edge weights. Use Bellman-Ford for negative weights.
Union-Find (Disjoint Set)
MediumConnected Components
Efficient grouping and connectivity queries. Path compression + union by rank = near O(1).
click to expand
Union(0,1) Union(2,3) Union(1,3) Find(0)==Find(3)? {0,1} {2,3} → {0,1} {2,3} → {0,1,2,3} → YES, connected!
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.components = n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # path compression
        return self.parent[x]

    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py: return False  # already connected
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px           # union by rank
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        self.components -= 1
        return True

    def connected(self, x, y):
        return self.find(x) == self.find(y)

# Detect cycle in undirected graph
def has_cycle(n, edges):
    uf = UnionFind(n)
    for u, v in edges:
        if not uf.union(u, v):
            return True   # already connected = cycle
    return False
Find/Union
O(α(n)) ≈ O(1)
Space
O(n)
Key: Path compression flattens the tree on find(). Union by rank keeps tree balanced. Use for: connected components, cycle detection, Kruskal's MST.
Cycle Detection in Directed Graph
Medium3-Color DFS
Use 3 colors: WHITE (unvisited), GRAY (in current path), BLACK (done). Back edge to GRAY = cycle.
click to expand
def has_cycle_directed(graph, n):
    WHITE, GRAY, BLACK = 0, 1, 2
    color = [WHITE] * n

    def dfs(node):
        color[node] = GRAY        # currently exploring
        for neighbor in graph[node]:
            if color[neighbor] == GRAY:
                return True    # back edge = cycle!
            if color[neighbor] == WHITE and dfs(neighbor):
                return True
        color[node] = BLACK       # done with this node
        return False

    for i in range(n):
        if color[i] == WHITE and dfs(i):
            return True
    return False
Key: For undirected graphs, use Union-Find. For directed graphs, use 3-color DFS. GRAY node meeting GRAY = cycle.

Dynamic Programming Patterns

The 6 DP patterns that cover 90% of interview problems

DP Framework & Approach
MediumFoundation
Top-down (memoization) vs Bottom-up (tabulation). Same idea, different direction.
click to expand
When to use DP? 1. Optimal substructure: optimal solution uses optimal sub-solutions 2. Overlapping subproblems: same subproblem solved multiple times Steps: 1. Define state: what info do I need? → dp[i] = ... 2. Recurrence: how does dp[i] relate to smaller subproblems? 3. Base case: what is dp[0]? 4. Order: compute smaller states before larger 5. Answer: which dp[?] is the final answer?
Top-Down (Memoization)
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)

# Or manual memo
memo = {}
def fib(n):
    if n in memo: return memo[n]
    if n <= 1: return n
    memo[n] = fib(n-1) + fib(n-2)
    return memo[n]
Bottom-Up (Tabulation)
def fib(n):
    if n <= 1: return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

# Space-optimized
def fib(n):
    if n <= 1: return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b
Key: Start with top-down (easier), then convert to bottom-up if needed. Use @lru_cache for quick memoization.
0/1 Knapsack Pattern
MediumInclude or Exclude
For each item: include it or skip it. Variants: subset sum, partition equal subset, target sum.
click to expand
# Classic 0/1 Knapsack
def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(capacity + 1):
            dp[i][w] = dp[i-1][w]               # skip item
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i][w],
                    dp[i-1][w - weights[i-1]] + values[i-1])  # take item
    return dp[n][capacity]

# Subset Sum (can we make target?)
def can_sum(nums, target):
    dp = [False] * (target + 1)
    dp[0] = True
    for num in nums:
        for t in range(target, num - 1, -1):  # reverse!
            dp[t] = dp[t] or dp[t - num]
    return dp[target]

# Partition Equal Subset Sum
def can_partition(nums):
    total = sum(nums)
    if total % 2: return False
    return can_sum(nums, total // 2)
Key: 0/1 = each item used at most once. Iterate capacity in REVERSE for 1D optimization. Unbounded knapsack = iterate forward.
LCS & Edit Distance
MediumTwo Strings DP
2D DP on two strings. LCS, edit distance, and longest common substring.
click to expand
LCS("ABCBDAB", "BDCAB") = "BCAB" (length 4) "" B D C A B "" 0 0 0 0 0 0 A 0 0 0 0 1 1 B 0 1 1 1 1 2 C 0 1 1 2 2 2 B 0 1 1 2 2 3 D 0 1 2 2 2 3 A 0 1 2 2 3 3 B 0 1 2 2 3 4
# Longest Common Subsequence
def lcs(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[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[m][n]

# Edit Distance (Levenshtein)
def edit_distance(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(m + 1): dp[i][0] = i
    for j in range(n + 1): dp[0][j] = j
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                dp[i][j] = dp[i-1][j-1]
            else:
                dp[i][j] = 1 + min(
                    dp[i-1][j],      # delete
                    dp[i][j-1],      # insert
                    dp[i-1][j-1]    # replace
                )
    return dp[m][n]
Time
O(m × n)
Space
O(m × n)
Key: LCS: match = diagonal+1, no match = max(left, up). Edit distance: match = diagonal, no match = 1 + min(left, up, diagonal).
Longest Increasing Subsequence
MediumLIS Pattern
O(n²) DP or O(n log n) with binary search + patience sort.
click to expand
# O(n²) DP
def lis(nums):
    n = len(nums)
    dp = [1] * n
    for i in range(1, n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)

# O(n log n) with bisect
import bisect
def lis_fast(nums):
    tails = []
    for num in nums:
        pos = bisect.bisect_left(tails, num)
        if pos == len(tails):
            tails.append(num)
        else:
            tails[pos] = num
    return len(tails)
Key: tails array maintains the smallest possible tail for each LIS length. bisect_left finds position to replace.
Grid DP & House Robber
MediumLinear & Grid DP
Grid: move right/down. House Robber: can't rob adjacent houses.
click to expand
# Unique Paths (m×n grid, only right or down)
def unique_paths(m, n):
    dp = [[1] * n for _ in range(m)]
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i-1][j] + dp[i][j-1]
    return dp[m-1][n-1]

# Minimum Path Sum
def min_path_sum(grid):
    m, n = len(grid), len(grid[0])
    for i in range(m):
        for j in range(n):
            if i == 0 and j == 0: continue
            elif i == 0: grid[i][j] += grid[i][j-1]
            elif j == 0: grid[i][j] += grid[i-1][j]
            else: grid[i][j] += min(grid[i-1][j], grid[i][j-1])
    return grid[m-1][n-1]

# House Robber (can't rob adjacent)
def rob(nums):
    if not nums: return 0
    if len(nums) <= 2: return max(nums)
    prev2 = nums[0]
    prev1 = max(nums[0], nums[1])
    for i in range(2, len(nums)):
        curr = max(prev1, prev2 + nums[i])  # skip or rob
        prev2 = prev1
        prev1 = curr
    return prev1
Key: Grid DP: dp[i][j] = f(dp[i-1][j], dp[i][j-1]). House Robber: dp[i] = max(dp[i-1], dp[i-2] + nums[i]).

Backtracking

Generate all possibilities — permutations, combinations, subsets, N-Queens

Backtracking Framework
MediumChoose-Explore-Unchoose
The universal template: choose → explore → unchoose. All backtracking follows this pattern.
click to expand
Template: [] / | \ [1] [2] [3] ← choose / \ | [1,2] [1,3] [2,3] ← explore | [1,2,3] ← base case → add to result ← unchoose (backtrack)
def backtrack(result, current, choices, start):
    if is_solution(current):         # base case
        result.append(current[:])    # add copy!
        return

    for i in range(start, len(choices)):
        # 1. Choose
        current.append(choices[i])
        # 2. Explore
        backtrack(result, current, choices, i + 1)  # or i for reuse
        # 3. Unchoose (backtrack)
        current.pop()
Critical: Always append a COPY (current[:]), not the reference. Without [:], all results point to the same empty list.
Subsets, Permutations, Combinations
MediumThe Big 3
Master these three — every backtracking problem is a variation.
click to expand
# SUBSETS — all possible subsets (power set)
# [1,2,3] → [],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]
def subsets(nums):
    result = []
    def backtrack(start, current):
        result.append(current[:])     # add every state
        for i in range(start, len(nums)):
            current.append(nums[i])
            backtrack(i + 1, current)
            current.pop()
    backtrack(0, [])
    return result

# PERMUTATIONS — all orderings
# [1,2,3] → [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
def permutations(nums):
    result = []
    def backtrack(current, remaining):
        if not remaining:
            result.append(current[:])
            return
        for i in range(len(remaining)):
            current.append(remaining[i])
            backtrack(current, remaining[:i] + remaining[i+1:])
            current.pop()
    backtrack([], nums)
    return result

# COMBINATIONS — choose k from n
# n=4, k=2 → [1,2],[1,3],[1,4],[2,3],[2,4],[3,4]
def combinations(n, k):
    result = []
    def backtrack(start, current):
        if len(current) == k:
            result.append(current[:])
            return
        for i in range(start, n + 1):
            current.append(i)
            backtrack(i + 1, current)
            current.pop()
    backtrack(1, [])
    return result
PatternStart next fromBase caseOutput size
Subsetsi + 1Add at every node2ⁿ
Permutations0 (use remaining)remaining is emptyn!
Combinationsi + 1len(current) == kC(n,k)
N-Queens & Word Search
HardConstraint Backtracking
Place constraints, explore, and prune invalid paths early.
click to expand
# N-Queens
def solve_n_queens(n):
    result = []
    cols = set()
    diag1 = set()   # row - col
    diag2 = set()   # row + col

    def backtrack(row, board):
        if row == n:
            result.append(["." * c + "Q" + "." * (n-c-1) for c in board])
            return
        for col in range(n):
            if col in cols or row-col in diag1 or row+col in diag2:
                continue         # prune!
            cols.add(col); diag1.add(row-col); diag2.add(row+col)
            backtrack(row + 1, board + [col])
            cols.discard(col); diag1.discard(row-col); diag2.discard(row+col)

    backtrack(0, [])
    return result

# Word Search in Grid
def exist(board, word):
    rows, cols = len(board), len(board[0])
    def dfs(r, c, idx):
        if idx == len(word): return True
        if r < 0 or r >= rows or c < 0 or c >= cols: return False
        if board[r][c] != word[idx]: return False
        temp = board[r][c]
        board[r][c] = "#"              # mark visited
        found = (dfs(r+1,c,idx+1) or dfs(r-1,c,idx+1) or
                 dfs(r,c+1,idx+1) or dfs(r,c-1,idx+1))
        board[r][c] = temp               # unchoose!
        return found
    for r in range(rows):
        for c in range(cols):
            if dfs(r, c, 0): return True
    return False
Key: N-Queens uses sets for O(1) conflict checking. Word search marks visited with "#" and restores after (backtrack!).

Stacks & Queues

Monotonic stack, min stack, and queue tricks

Monotonic Stack
MediumNext Greater/Smaller
Stack that maintains increasing or decreasing order. Solves "next greater element" in O(n).
click to expand
Next Greater Element: [2, 1, 2, 4, 3] ↓ ↓ ↓ ↓ ↓ → [4, 2, 4, -1, -1] Stack (decreasing): Process 2: stack=[2] Process 1: 1<2, push → stack=[2,1] Process 2: 2>1, pop 1→answer[1]=2; 2=2, push → stack=[2,2] Process 4: 4>2, pop 2→answer[2]=4; 4>2, pop 2→answer[0]=4 → stack=[4] Process 3: 3<4, push → stack=[4,3] End: remaining get -1
# Next Greater Element
def next_greater(nums):
    n = len(nums)
    result = [-1] * n
    stack = []  # stores indices
    for i in range(n):
        while stack and nums[i] > nums[stack[-1]]:
            idx = stack.pop()
            result[idx] = nums[i]
        stack.append(i)
    return result

# Daily Temperatures (days until warmer)
def daily_temperatures(temps):
    n = len(temps)
    result = [0] * n
    stack = []
    for i in range(n):
        while stack and temps[i] > temps[stack[-1]]:
            idx = stack.pop()
            result[idx] = i - idx  # days difference
        stack.append(i)
    return result
Key: Store INDICES, not values. Each element pushed and popped at most once → O(n). Decreasing stack for next greater, increasing stack for next smaller.
Min Stack & Queue with Stacks
MediumDesign
MinStack: O(1) getMin. Queue using two stacks: amortized O(1) dequeue.
click to expand
Min Stack
class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, val):
        self.stack.append(val)
        if not self.min_stack or val <= self.min_stack[-1]:
            self.min_stack.append(val)

    def pop(self):
        val = self.stack.pop()
        if val == self.min_stack[-1]:
            self.min_stack.pop()

    def get_min(self):
        return self.min_stack[-1]
Queue Using 2 Stacks
class MyQueue:
    def __init__(self):
        self.push_stack = []
        self.pop_stack = []

    def enqueue(self, val):
        self.push_stack.append(val)

    def dequeue(self):
        if not self.pop_stack:
            while self.push_stack:
                self.pop_stack.append(
                    self.push_stack.pop())
        return self.pop_stack.pop()

    def peek(self):
        if not self.pop_stack:
            while self.push_stack:
                self.pop_stack.append(
                    self.push_stack.pop())
        return self.pop_stack[-1]
Key: MinStack uses auxiliary stack tracking current minimum. Queue uses lazy transfer — only move to pop_stack when empty.

Tries (Prefix Trees)

Efficient string prefix operations — autocomplete, spell check, word search

Trie Implementation
MediumPrefix Search
Tree where each node is a character. Shared prefixes share paths.
click to expand
Words: "app", "apple", "api", "bat" root / \ a b | | p a / \ | p i* t* | l | e* * = end of word
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_end = True

    def search(self, word):
        node = self._find(word)
        return node is not None and node.is_end

    def starts_with(self, prefix):
        return self._find(prefix) is not None

    def _find(self, prefix):
        node = self.root
        for char in prefix:
            if char not in node.children:
                return None
            node = node.children[char]
        return node

    # Autocomplete: find all words with prefix
    def autocomplete(self, prefix):
        node = self._find(prefix)
        if not node: return []
        results = []
        def dfs(node, path):
            if node.is_end:
                results.append(prefix + path)
            for char, child in node.children.items():
                dfs(child, path + char)
        dfs(node, "")
        return results
Insert/Search
O(word length)
Space
O(total chars)
Key: Trie beats hash set for prefix queries. Use dict for children (flexible) or [None]*26 for lowercase-only (faster). is_end marks complete words.

Greedy Algorithms

Make the locally optimal choice at each step — sometimes that's globally optimal

Interval Problems & Jump Game
MediumSort + Greedy
Merge intervals, non-overlapping intervals, meeting rooms, jump game.
click to expand
# Merge Intervals
def merge_intervals(intervals):
    intervals.sort()
    merged = [intervals[0]]
    for start, end in intervals[1:]:
        if start <= merged[-1][1]:       # overlapping
            merged[-1][1] = max(merged[-1][1], end)
        else:
            merged.append([start, end])
    return merged

# Non-Overlapping Intervals (min removals)
def erase_overlap(intervals):
    intervals.sort(key=lambda x: x[1])  # sort by END
    count = 0
    prev_end = float('-inf')
    for start, end in intervals:
        if start >= prev_end:
            prev_end = end            # keep this interval
        else:
            count += 1                # remove (overlap)
    return count

# Meeting Rooms II (min rooms needed)
import heapq
def min_meeting_rooms(intervals):
    intervals.sort()
    heap = []   # tracks end times of active meetings
    for start, end in intervals:
        if heap and heap[0] <= start:
            heapq.heappop(heap)       # reuse room
        heapq.heappush(heap, end)
    return len(heap)

# Jump Game (can reach end?)
def can_jump(nums):
    max_reach = 0
    for i, jump in enumerate(nums):
        if i > max_reach: return False
        max_reach = max(max_reach, i + jump)
    return True
Key: Merge → sort by start. Non-overlap → sort by END (greedy: pick earliest finish). Meeting rooms → min-heap of end times.

Sliding Window & Two Pointers

Fixed window, variable window, and two-pointer patterns

Sliding Window Patterns
MediumWindow Technique
Fixed window: move both ends together. Variable window: expand right, shrink left.
click to expand
Fixed Window (k=3): Variable Window: [1, 3, 2, 6, -1, 4] [a, b, c, a, b, c, b, b] [---] sum=6 Find shortest substring with all chars [---] sum=11 ← shrink left when condition met [---] sum=7 → expand right until condition met
# FIXED window: max sum of k elements
def max_sum_k(arr, k):
    window_sum = sum(arr[:k])
    max_sum = window_sum
    for i in range(k, len(arr)):
        window_sum += arr[i] - arr[i - k]  # slide: add right, remove left
        max_sum = max(max_sum, window_sum)
    return max_sum

# VARIABLE window: min length subarray with sum >= target
def min_subarray_len(target, nums):
    left = 0
    curr_sum = 0
    min_len = float('inf')
    for right in range(len(nums)):
        curr_sum += nums[right]          # expand
        while curr_sum >= target:
            min_len = min(min_len, right - left + 1)
            curr_sum -= nums[left]       # shrink
            left += 1
    return min_len if min_len != float('inf') else 0

# Minimum Window Substring (all chars of t in s)
from collections import Counter
def min_window(s, t):
    need = Counter(t)
    missing = len(t)
    left = start = 0
    min_len = float('inf')
    for right, char in enumerate(s):
        if need[char] > 0:
            missing -= 1
        need[char] -= 1
        while missing == 0:              # all chars found
            if right - left + 1 < min_len:
                min_len = right - left + 1
                start = left
            need[s[left]] += 1
            if need[s[left]] > 0:
                missing += 1
            left += 1
    return s[start:start + min_len] if min_len != float('inf') else ""
Key: Fixed window: O(n), slide by adding right and removing left. Variable window: expand right first, shrink left while valid. Window size = right - left + 1.
Two Pointers Patterns
MediumOpposites & Same Direction
Opposite ends (sorted array), same direction (fast/slow), and 3Sum.
click to expand
# 3Sum — find all triplets that sum to 0
def three_sum(nums):
    nums.sort()
    result = []
    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i-1]: continue  # skip dups
        left, right = i + 1, len(nums) - 1
        while left < right:
            total = nums[i] + nums[left] + nums[right]
            if total == 0:
                result.append([nums[i], nums[left], nums[right]])
                while left < right and nums[left] == nums[left+1]: left += 1
                while left < right and nums[right] == nums[right-1]: right -= 1
                left += 1; right -= 1
            elif total < 0: left += 1
            else: right -= 1
    return result

# Container With Most Water
def max_area(height):
    left, right = 0, len(height) - 1
    max_water = 0
    while left < right:
        area = min(height[left], height[right]) * (right - left)
        max_water = max(max_water, area)
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1
    return max_water

# Remove Duplicates in-place (sorted)
def remove_dups(nums):
    if not nums: return 0
    slow = 0
    for fast in range(1, len(nums)):
        if nums[fast] != nums[slow]:
            slow += 1
            nums[slow] = nums[fast]
    return slow + 1
Key: 3Sum = sort + fix one + two pointers. Skip duplicates! Container = move the shorter side inward. Remove dups = slow/fast pointers.

Bit Manipulation

XOR tricks, single number, power of 2 — fast and elegant

Bit Operations & Tricks
MediumXOR / AND / Shift
Essential bit operations and the most common interview problems.
click to expand
OperationSyntaxExampleUse Case
ANDa & b1010 & 1100 = 1000Check if bit is set
ORa | b1010 | 1100 = 1110Set a bit
XORa ^ b1010 ^ 1100 = 0110Toggle / find unique
NOT~a~1010 = 0101Flip all bits
Left shifta << n1 << 3 = 8Multiply by 2ⁿ
Right shifta >> n8 >> 2 = 2Divide by 2ⁿ
# Single Number (every element appears twice except one)
def single_number(nums):
    result = 0
    for num in nums:
        result ^= num      # XOR: a ^ a = 0, a ^ 0 = a
    return result

# Power of 2
def is_power_of_2(n):
    return n > 0 and (n & (n - 1)) == 0
    # 8 = 1000, 7 = 0111 → 1000 & 0111 = 0000 → True!

# Count set bits (number of 1s)
def count_bits(n):
    count = 0
    while n:
        count += n & 1    # check last bit
        n >>= 1           # shift right
    return count
    # Or: bin(n).count('1')

# Missing Number (0 to n, one missing)
def missing_number(nums):
    result = len(nums)
    for i, num in enumerate(nums):
        result ^= i ^ num  # XOR index with value
    return result

# Reverse Bits
def reverse_bits(n):
    result = 0
    for _ in range(32):
        result = (result << 1) | (n & 1)
        n >>= 1
    return result
Key: XOR is the star of bit manipulation. a ^ a = 0 (cancels). a ^ 0 = a (identity). n & (n-1) removes lowest set bit.

String Algorithms

Palindromes, pattern matching, and common string manipulation patterns

Palindrome Patterns
MediumExpand from Center
Check palindrome, longest palindromic substring, valid palindrome with removals.
click to expand
# Check if palindrome
def is_palindrome(s):
    return s == s[::-1]

# Valid Palindrome (ignore non-alphanumeric)
def is_valid_palindrome(s):
    cleaned = [c.lower() for c in s if c.isalnum()]
    return cleaned == cleaned[::-1]

# Longest Palindromic Substring — expand from center
def longest_palindrome(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]

    result = ""
    for i in range(len(s)):
        # Odd length: "aba"
        odd = expand(i, i)
        # Even length: "abba"
        even = expand(i, i + 1)
        result = max(result, odd, even, key=len)
    return result

# Count Palindromic Substrings
def count_palindromes(s):
    count = 0
    for i in range(len(s)):
        for l, r in [(i,i), (i,i+1)]:  # odd & even
            while l >= 0 and r < len(s) and s[l] == s[r]:
                count += 1
                l -= 1; r += 1
    return count
Expand from Center
O(n²)
Space
O(1)
Key: Expand from each center (2n-1 centers: n odd + n-1 even). Much simpler than DP approach for interviews.
Common String Problems
MediumManipulation
Reverse words, string to integer, encode/decode, longest common prefix.
click to expand
# Reverse Words in String
def reverse_words(s):
    return " ".join(s.split()[::-1])

# String to Integer (atoi)
def my_atoi(s):
    s = s.strip()
    if not s: return 0
    sign = -1 if s[0] == '-' else 1
    start = 1 if s[0] in '+-' else 0
    result = 0
    for i in range(start, len(s)):
        if not s[i].isdigit(): break
        result = result * 10 + int(s[i])
    result *= sign
    return max(-2**31, min(2**31 - 1, result))

# Longest Common Prefix
def longest_prefix(strs):
    if not strs: return ""
    for i, chars in enumerate(zip(*strs)):
        if len(set(chars)) > 1:
            return strs[0][:i]
    return min(strs)

# Check if string is rotation of another
def is_rotation(s1, s2):
    return len(s1) == len(s2) and s2 in s1 + s1

# Encode/Decode Strings
def encode(strs):
    return "".join(f"{len(s)}#{s}" for s in strs)

def decode(s):
    result, i = [], 0
    while i < len(s):
        j = s.index('#', i)
        length = int(s[i:j])
        result.append(s[j+1:j+1+length])
        i = j + 1 + length
    return result
Key: s.split() handles multiple spaces. zip(*strs) transposes to compare char by char. Rotation check: s2 in s1+s1 is elegant O(n).