DSA Cheat Sheet: Patterns & Problem Identification

A quick reference guide for recognizing and solving common Data Structures & Algorithms patterns.

Table of Contents

I. DSA Patterns Explained

Two Pointers

Description: Using two pointers (e.g., left and right) to traverse an array or string. Pointers can move in the same direction or opposite directions. This often helps reduce time complexity from quadratic to linear.

General Approach:

  1. Initialize left = 0 (or start) and right = array.length - 1 (or end), or left = 0 and right = 1 depending on problem.
  2. Loop while pointers meet specific conditions (e.g., left < right, right < array.length).
  3. Inside the loop, move pointers based on comparison or condition.
  4. Update result or perform in-place modification.
// Example: Check if string is palindrome
function isPalindrome(s) {
    let left = 0;
    let right = s.length - 1;
    while (left < right) {
        if (s[left] !== s[right]) {
            return false;
        }
        left++;
        right--;
    }
    return true;
}
Back to Top

Sliding Window

Description: Used on arrays or strings to find a subarray/substring that satisfies a condition, often optimizing problems that would otherwise require nested loops. It involves maintaining a "window" of elements and moving it across the data.

General Approach:

  1. Initialize left = 0 (start of window), currentSum/count = 0, result = 0/Infinity.
  2. Loop right from 0 to array.length - 1 (expand window).
    • Add array[right] to currentSum/count.
    • While currentSum/count violates a condition (or window size is fixed):
      • Subtract array[left] from currentSum/count (shrink window).
      • Increment left.
    • Update result with current window properties (e.g., `max/min length`, `max/min sum`).
  3. Return result.
// Example: Max sum subarray of size K
function maxSubarraySum(arr, k) {
    let maxSum = 0;
    let windowSum = 0;
    let windowStart = 0;

    for (let windowEnd = 0; windowEnd < arr.length; windowEnd++) {
        windowSum += arr[windowEnd]; // Expand window
        if (windowEnd >= k - 1) { // Window size reached
            maxSum = Math.max(maxSum, windowSum);
            windowSum -= arr[windowStart]; // Shrink window
            windowStart++;
        }
    }
    return maxSum;
}
Back to Top

Prefix Sum

Description: Precomputing sums of elements up to each index in an array (or matrix) to quickly find the sum of any subarray/range in $O(1)$ time. This avoids re-calculating sums for every query.

General Approach:

  1. Create a `prefixSum` array where `prefixSum[i]` stores the sum of elements from index 0 to `i-1`. (Or `prefixSum[i]` stores sum up to `i`, depending on problem definition).
  2. Calculate `prefixSum` array: `prefixSum[i] = prefixSum[i-1] + arr[i-1]`.
  3. To find sum of `arr[i...j]`: `prefixSum[j+1] - prefixSum[i]`.
// Example: Calculate prefix sums
function getPrefixSums(arr) {
    let prefixSums = new Array(arr.length + 1).fill(0);
    for (let i = 0; i < arr.length; i++) {
        prefixSums[i + 1] = prefixSums[i] + arr[i];
    }
    return prefixSums;
}
// Sum of arr[i...j] is prefixSums[j+1] - prefixSums[i]
Back to Top

Hash Map / Set

Description: Using hash-based data structures (objects/Maps/Sets) for efficient $O(1)$ average time lookups, insertions, and deletions. They provide quick access to data based on a key.

General Approach:

  1. Choose `Map` for key-value pairs (e.g., frequency counting, custom mappings) or `Set` for storing unique elements (e.g., checking for existence, removing duplicates).
  2. Iterate through the input data.
  3. Store elements or their properties in the hash structure.
  4. Use `has()`, `get()`, `set()`, `delete()` methods for efficient operations.
// Example: Two Sum
function twoSum(nums, target) {
    const numMap = new Map(); // Stores number -> index
    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];
        if (numMap.has(complement)) {
            return [numMap.get(complement), i];
        }
        numMap.set(nums[i], i);
    }
    return [];
}
Back to Top

Stack & Queue

Description:

  • Stack (LIFO - Last In, First Out): Elements are added and removed from the same end (the "top"). Think of a stack of plates.
  • Queue (FIFO - First In, First Out): Elements are added at one end (the "rear") and removed from the other end (the "front"). Think of a line at a store.

General Approach (Stack):

  1. Initialize an empty stack (e.g., using a JavaScript array and `push`/`pop`).
  2. Iterate through the input.
  3. If an "opening" or relevant element is encountered, `push` it onto the stack.
  4. If a "closing" or processing element is encountered, `pop` from the stack and perform necessary checks/operations.
  5. Check the final state of the stack at the end of processing.
// Example: Valid Parentheses (Stack)
function isValid(s) {
    const stack = [];
    const map = { '(': ')', '[': ']', '{': '}' };
    for (let char of s) {
        if (map[char]) { // If it's an opening bracket
            stack.push(char);
        } else { // If it's a closing bracket
            if (stack.length === 0 || map[stack.pop()] !== char) {
                return false;
            }
        }
    }
    return stack.length === 0;
}

General Approach (Queue):

  1. Initialize an empty queue (e.g., using a JavaScript array and `push`/`shift`).
  2. Add starting elements to the queue.
  3. Loop while the queue is not empty.
  4. `Dequeue` an element, process it, and `enqueue` its neighbors/next elements.
// Example: Basic BFS (for graph traversal)
function bfs(startNode) {
    const queue = [startNode];
    const visited = new Set();
    visited.add(startNode);

    while (queue.length > 0) {
        const node = queue.shift(); // Dequeue
        // Process node (e.g., print node.value)
        
        for (let neighbor of node.neighbors) { // Assuming node has a 'neighbors' array
            if (!visited.has(neighbor)) {
                visited.add(neighbor);
                queue.push(neighbor); // Enqueue
            }
        }
    }
}
Back to Top

Linked List: Fast & Slow Pointers

Description: Using two pointers that traverse a linked list (or sometimes an array/sequence) at different speeds (e.g., one moves one step at a time, the other two steps). This is effective for problems where you need to find relative positions or detect cycles.

General Approach:

  1. Initialize `slow = head`, `fast = head` (or `head.next` depending on problem).
  2. Loop while `fast` and `fast.next` exist (adjust termination condition based on goal).
  3. Move `slow` by 1 step (`slow = slow.next`).
  4. Move `fast` by 2 steps (`fast = fast.next.next`).
  5. Check conditions (e.g., `slow === fast` for cycle detection, or `fast.next === null` for middle).
// Example: Detect Cycle in Linked List
function hasCycle(head) {
    if (!head || !head.next) return false; // Need at least two nodes for a cycle
    let slow = head;
    let fast = head.next; // Start fast one step ahead

    while (slow !== fast) {
        if (!fast || !fast.next) { // If fast reaches end, no cycle
            return false;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    return true; // Cycle detected
}
Back to Top

Trie (Prefix Tree)

Description: A tree-like data structure used to store a dynamic set of strings, where nodes represent characters and paths from the root represent prefixes. It's highly optimized for prefix-based searches and string operations.

General Approach:

  1. Define a `TrieNode` class (or object structure) with `children` (a map/object of char to TrieNode) and an `isEndOfWord` boolean flag.
  2. Implement `insert(word)`: Traverse/create nodes for each character, marking `isEndOfWord` at the end.
  3. Implement `search(word)`: Traverse, return true if path exists and `isEndOfWord` is true.
  4. Implement `startsWith(prefix)`: Traverse, return true if path exists.
  5. Build the Trie by inserting all relevant strings (e.g., a dictionary of words).
  6. Query the Trie based on problem requirements (e.g., finding shortest matching prefix).
// Basic TrieNode structure
class TrieNode {
    constructor() {
        this.children = {}; // Map of char -> TrieNode
        this.isEndOfWord = false;
    }
}
// (Insert/Search methods would be added to a Trie class that holds the root node)
Back to Top

Heap (Priority Queue)

Description: A specialized tree-based data structure that satisfies the heap property: parent nodes are always greater than or equal to (Max Heap) or less than or equal to (Min Heap) their children. This structure enables efficient extraction of the minimum or maximum element ($O(1)$ peek, $O(\log N)$ extract/insert).

General Approach:

  1. Choose Min-Heap or Max-Heap based on whether you need the smallest or largest element to be easily accessible.
  2. Insert elements into the heap.
  3. Repeatedly extract elements based on priority (e.g., extract min for smallest K elements, or extract max for largest K elements).
Back to Top

III. Tree & Graph Patterns

Depth-First Search (DFS)

Description: Traverses a graph or tree by exploring as far as possible along each branch before backtracking. It goes deep into one path before exploring other branches. Typically implemented with recursion (which uses the call stack) or an explicit stack data structure.

General Approach (Recursive):

function dfs(node, visited, target) {
    // Base Case 1: If node already visited in current path (prevents cycles in undirected graphs)
    if (visited.has(node)) return false;
    visited.add(node); // Mark current node as visited

    // Base Case 2: If target found
    if (node === target) return true;

    // Recursive Step: Explore neighbors
    for (let neighbor of node.neighbors) { // Assuming graph nodes have a 'neighbors' property
        if (dfs(neighbor, visited, target)) {
            return true; // If target found in any child branch, propagate true upwards
        }
    }
    // Backtrack (if needed for state changes, like in Word Search)
    // visited.delete(node); // Uncomment if problem requires backtracking on visited state
    return false; // Target not found in this branch
}
Back to Top

Breadth-First Search (BFS)

Description: Traverses a graph or tree level by level. It explores all neighbors at the current depth before moving on to nodes at the next depth level. Typically implemented with a queue.

General Approach:

function bfs(startNode, target) {
    const queue = [{ node: startNode, distance: 0 }]; // Store node and its distance/level
    const visited = new Set();
    visited.add(startNode);

    while (queue.length > 0) {
        const { node, distance } = queue.shift(); // Dequeue
        
        if (node === target) return distance; // Target found, return distance/level

        for (let neighbor of node.neighbors) { // Assuming node has a 'neighbors' array
            if (!visited.has(neighbor)) {
                visited.add(neighbor);
                queue.push({ node: neighbor, distance: distance + 1 }); // Enqueue with incremented distance
            }
        }
    }
    return -1; // Target not reachable
}
Back to Top

IV. Algorithmic Paradigms

Dynamic Programming (DP)

Description: Solving complex problems by breaking them down into simpler, overlapping subproblems and storing the results of these subproblems (memoization or tabulation) to avoid redundant calculations. It's about optimizing recursive solutions.

General Approach:

  1. Identify overlapping subproblems: Can you break the problem into smaller, identical subproblems that repeat?
  2. Identify optimal substructure: Does an optimal solution to the overall problem depend on optimal solutions to its subproblems?
  3. Define a recurrence relation: How does the solution to a problem `dp[i]` (or `dp[i][j]`) depend on previously solved subproblems?
  4. Base cases: What are the smallest, trivial solutions that can be solved directly?
  5. Memoization (Top-down): Implement a recursive solution and cache the results of subproblems.
  6. Tabulation (Bottom-up): Implement an iterative solution that fills up a DP table (1D or 2D array) from base cases upwards.
// Example: Fibonacci (Tabulation)
function fib(n) {
    if (n <= 1) return n;
    const dp = new Array(n + 1);
    dp[0] = 0;
    dp[1] = 1;
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}
Back to Top

Greedy Algorithms

Description: Making the locally optimal choice at each step with the hope that this sequence of local optimal choices will lead to a globally optimal solution. This strategy does not always work, and often requires proof of correctness.

General Approach:

  1. Identify the "greedy choice property": Can a global optimum be reached by making a locally optimal choice?
  2. Identify "optimal substructure": Does an optimal solution to the problem contain optimal solutions to subproblems?
  3. Sort the input if order helps in making greedy choices.
  4. Iterate and make the "best" local choice at each step, adding it to the result and updating the remaining problem state.
Back to Top

Backtracking

Description: A refined brute-force approach used to explore all possible solutions to a problem. It builds a solution incrementally, and if a partial solution cannot lead to a valid complete solution, it "backs up" (undoes the last choice) and tries another path. Often implemented using recursion.

General Approach:

// Pseudo-code structure for Backtracking
function backtrack(combination, candidates, results) {
    // 1. Base Case: If a complete solution is found
    if (isSolution(combination)) {
        results.push([...combination]); // Add a copy of the solution
        return;
    }

    // 2. Explore choices
    for (let i = 0; i < candidates.length; i++) { // Or iterate through available choices
        let choice = candidates[i];
        
        // Pruning: Check if the current choice can lead to a valid solution
        if (isValid(choice, combination)) { 
            // 3. Make choice
            combination.push(choice);
            // (Optionally: adjust candidates for next recursion, e.g., candidates.slice(i+1))

            // 4. Recurse
            backtrack(combination, candidates, results);

            // 5. Unmake choice (backtrack): Restore state for next iteration
            combination.pop();
            // (Optionally: restore candidates if they were modified)
        }
    }
}
Back to Top

II. Identifying Patterns from Problem Statements (Clues)

Two Pointers Clues

  • Keywords: "sorted array", "in-place", "remove duplicates", "find pairs/triplets with sum X", "reverse string/array".
  • Problem asks for: Comparing elements at two different positions, modifying array/string without extra space, finding specific elements in sorted data.
  • Data Structure: Often arrays or strings.
Back to Top

Sliding Window Clues

  • Keywords: "subarray", "substring", "contiguous", "longest/shortest subarray/substring with X property", "max sum of subarray of size K", "distinct characters".
  • Problem asks for: Finding a range (subarray/substring) that satisfies a condition.
  • Data Structure: Arrays or strings.
Back to Top

Prefix Sum Clues

  • Keywords: "sum of subarray/submatrix", "range sum queries", "number of subarrays with sum X", "even/odd sums".
  • Problem asks for: Repeatedly calculating sums over different ranges.
  • Data Structure: Arrays or matrices.
Back to Top

Hash Map / Set Clues

  • Keywords: "distinct", "unique", "frequency", "count occurrences", "mapping", "contains", "is present", "pair sum", "cycle detection" (in sequences).
  • Problem asks for: Tracking counts, checking for element existence quickly, storing relationships between items, eliminating duplicates.
  • Data Structure: Any data that needs quick lookups or uniqueness checks.
Back to Top

Stack & Queue Clues

  • Stack Keywords: "valid parentheses", "balanced symbols", "reverse order", "undo operations", "next greater/smaller element", "syntax parsing", "function call order".
  • Queue Keywords: "level-order traversal" (BFS), "task scheduling", "printer queue", "shortest path on unweighted graphs", "first-in, first-out processing".
  • Problem asks for: Processing elements in a specific order (LIFO/FIFO), managing nested structures, shortest path on unweighted graphs.
  • Data Structure: Often strings (for parentheses), arrays (for next greater), graphs/trees (for traversals).
Back to Top

Linked List: Fast & Slow Pointers Clues

  • Keywords: "linked list cycle", "find middle node", "Nth node from end", "intersection of two linked lists".
  • Problem asks for: Detecting loops, finding specific nodes relative to the end or middle, or comparing positions in linked lists without knowing total length.
  • Data Structure: Linked Lists (explicitly).
Back to Top

Trie (Prefix Tree) Clues

  • Keywords: "prefix search", "autocomplete", "spell checker", "dictionary of words", "shortest/longest common prefix", "word search with roots/derivatives".
  • Problem asks for: Efficiently storing and searching strings based on their prefixes, finding words with common prefixes.
  • Data Structure: Collections of strings.
Back to Top

Heap (Priority Queue) Clues

  • Keywords: "K-th largest/smallest element", "top K elements", "priority queue", "merge K sorted lists/arrays", "scheduling tasks by priority".
  • Problem asks for: Efficiently retrieving the minimum or maximum element from a collection, maintaining a sorted order of a subset of elements.
  • Data Structure: Collections of numbers or objects with priorities.
Back to Top

Depth-First Search (DFS) Clues

  • Keywords: "all paths", "explore every branch", "connectivity", "reachability", "cycle detection" (in directed graphs), "topological sort", "backtracking problems" (DFS is the core mechanism).
  • Problem asks for: Exploring all possible paths, finding if a path exists, traversing all nodes in a connected component, problems on trees (pre/in/post-order traversals).
  • Data Structure: Graphs, Trees, Grids/Matrices (as implicit graphs).
Back to Top

Breadth-First Search (BFS) Clues

  • Keywords: "shortest path on unweighted graphs", "minimum steps", "level by level traversal", "nearest element", "all nodes at a certain distance".
  • Problem asks for: Finding the shortest path (in terms of number of edges/steps) in an unweighted graph, exploring nodes layer by layer.
  • Data Structure: Graphs, Trees, Grids/Matrices.
Back to Top

Dynamic Programming (DP) Clues

  • Keywords: "Minimum/Maximum", "Longest/Shortest", "Count ways", "number of ways", "can you reach X", "longest common subsequence/substring", "knapsack".
  • Problem asks for: Optimization (min/max), counting, or existence, where the solution can be built from solutions to smaller, *overlapping* subproblems, and there's an *optimal substructure*.
  • Data Structure: Often involves a 1D or 2D array/table to store intermediate results.
Back to Top

Greedy Algorithms Clues

  • Keywords: "maximize profit", "minimize cost", "smallest number of items", "activity selection", "coin change" (sometimes greedy, sometimes DP depending on constraints).
  • Problem asks for: Making a sequence of locally optimal choices to achieve a global optimum.
  • Check: Does making the best choice *now* always lead to the best overall solution? (Requires proof or intuition).
Back to Top

Backtracking Clues

  • Keywords: "all possible arrangements", "find all combinations/permutations", "generate all subsets", "solve a puzzle" (e.g., Sudoku, N-Queens), "find if path exists by trying all options".
  • Problem asks for: Exploring all possible decision paths, generating all valid solutions, or finding if *any* solution exists under complex constraints by trial and error.
  • Data Structure: Often recursive, using a temporary list/array to build the current solution.
Back to Top

III. General DSA Test Hints

  • Read the Problem Carefully:
    • Understand input/output format, constraints (N size, value range, time/memory limits).
    • Pay attention to edge cases (empty input, single element, max/min values).
    • Clarify ambiguities with the interviewer.
  • How to Approach Brute Force:
    • Purpose: A brute-force solution is your first step if you're stuck. It clarifies the problem, helps you understand constraints, and provides a baseline for correctness (and later, optimization).
    • Strategy:
      • Try all possibilities: If you need to find something, try every single way to find it.
      • Nested loops: Often involves multiple nested loops (e.g., $O(N^2)$, $O(N^3)$) to check all pairs, triplets, or combinations.
      • Recursion without memoization: For problems with choices, a simple recursive solution that explores all branches without storing results.
    • Example: For "Two Sum", brute force is two nested loops checking every pair. For "Longest Substring without Repeating Characters", brute force is checking every possible substring.
    • Don't optimize prematurely: Get the brute force working correctly first.
  • Think about Time & Space Complexity:
    • Always analyze your solution's complexity. $O(N^2)$ vs $O(N \log N)$ vs $O(N)$ vs $O(1)$.
    • Mention trade-offs (e.g., "I'm using $O(N)$ space for $O(1)$ lookup speed").
  • Draw Diagrams:
    • For Linked Lists, Trees, Graphs, or array manipulations. Visualizing helps immensely!
  • Test with Examples:
    • Walk through your algorithm with small custom examples and given examples.
    • Check edge cases (empty, single element, full capacity).
  • Break Down Complex Problems:
    • If a problem looks daunting, try to identify smaller, solvable subproblems.
  • Master Core Data Structures:
    • Arrays: $O(1)$ access, $O(N)$ insert/delete (unless at end). Fixed size.
    • Linked Lists: $O(1)$ insert/delete (if node known), $O(N)$ access. Dynamic size.
    • Hash Tables (Maps/Sets): $O(1)$ average time for insert, delete, lookup. $O(N)$ worst case (collisions).
    • Stacks/Queues: $O(1)$ push/pop/peek.
    • Trees (Binary Search Trees): $O(\log N)$ average for search, insert, delete. $O(N)$ worst case (skewed tree).
    • Heaps (Priority Queues): $O(\log N)$ for insert/delete min/max. $O(1)$ peek.
  • Practice, Practice, Practice:
    • The more problems you solve, the better you get at pattern recognition.
    • Don't just solve, understand *why* a particular solution is optimal.
  • Talk Through Your Thought Process:
    • Explain your assumptions, alternative ideas, and why you chose a particular approach.
    • Communicate if you get stuck or change your mind.
Back to Top