Algorithmic Problem-Solving Techniques & Patterns

Great software engineers aren’t the ones who know every algorithm, they’re the ones who know how to approach new problems.

Algorithmic Problem-Solving Techniques & Patterns When people talk about algorithms, they often focus on individual problems: “How do I solve this one? What’s the trick?”. But the real skill isn’t memorizing solutions. It’s learning how to model problems: breaking them down, choosing the right abstractions, and mapping real-world constraints to data structures and algorithms that make sense. Pattern recognition, not memorization, is what separates someone who finishes problems from someone who understands them. The moment you learn to see an array as a sliding window, a set of options as a search tree, or a relationship as a graph, the problem stops being a riddle and starts being engineering. Step Zero: Understand the Problem Before you reach for an algorithm, reach for clarity. Most failed solutions aren’t wrong because the code was buggy, they’re wrong because the problem was misunderstood or tackled without thought. Rewrite the problem in your own words: if you can’t explain it simply, you don’t understand it well enough. Restating the problem forces you to extract what really matters and ignore the noise. Identify Inputs, Outputs & Constraints: what data do I get?, What do I need to return or compute?, How big can the input get?, What are the edge cases? Constraints aren’t trivial, they define the solution space. A problem that runs in 1 second for 10⁴ operations is completely different from the same one running on 10⁷. Model Before You Solve: this is where modeling comes in. Ask yourself: What structure does this really represent? Is it a graph in disguise? Are we searching, counting, grouping, optimizing? What operations do we need to perform efficiently? Choosing the right structure often is the solution. Once you realize “this is a graph problem,” or “this is basically a sliding window,” the rest becomes mechanical. Think About Test Cases First: before coding try simple examples and edge cases. Techniques & Patterns Once you understand the problem and have a model in mind, the next step is to choose the right approach. People often jump straight into “Which algorithm?” but algorithm choice is the last step. First, you need the right technique. You’re not trying to memorize all techniques, you’re trying to develop that “sense” of which technique fits a new problem. And that comes from seeing the same patterns again, again and again. Patterns are powerful because once you’ve learned them, you start recognizing problems not by their words, but by their shape. Below are some of the most commonly useful patterns across interviews, competitions, and real-world engineering work. Sliding Window When to use it: streams, substrings, variable ranges, continuous subarrays. [Example: Longest Substring Without Repeating Characters] The sliding window pattern is about maintaining a subset of data that satisfies a condition while “sliding” through the input. Instead of checking every possible subarray or substring, you expand and shrink the window as needed, updating your answer incrementally. This turns O(n^2) brute-force solutions into efficient O(n) algorithms in many cases. function slidingWindow<T>(arr: T[]): number { let windowStart = 0; let result = 0; for (let windowEnd = 0; windowEnd < arr.length; windowEnd++) { // add currentElement to your window state if needed const currentElement = arr[windowEnd]; // shrink the window if it violates constraints while (windowIsInvalid(windowStart, windowEnd, arr)) { // remove arr[windowStart] from state if needed windowStart++; } // update result based on current window result = updateResult(result, windowStart, windowEnd, arr); } return result; } Two Pointers When to use it: sorted data, pairs, intersections, merging. [Example: Container with Most Water] The two pointers pattern is all about using two indices to traverse a dataset, often moving in opposite directions or at different speeds. It’s particularly useful for problems involving sorted arrays, pairs, or contiguous sequences. By carefully advancing the pointers based on the problem’s constraints, you can reduce nested loops and achieve linear or near-linear performance. function twoPointers<T>(arr: T[]): number { let left = 0; let right = arr.length - 1; let result = 0; // or some initial value depending on the problem while (left < right) { // check current pair / window const leftElement = arr[left]; const rightElement = arr[right]; // update result or state based on arr[left] and arr[right] // move pointers based on problem constraints if (shouldMoveLeft(leftElement, rightElement)) { left++; } else if (shouldMoveRight(leftElement, rightElement)) { right--; } else { // optional: move both pointers if needed left++; right--; } } return result; } Hashing for O(1) Lookup When to use it: fast membership checks, deduplication, caching. [Example: Two Sum] Hashing is one of the most powerful tools in your problem-solving toolkit. By storing elements in a hash set or hash map, you can check for existence, count frequencies, or group items in constant time. This pattern is especially useful for problems involving duplicates, pair sums, or quick membership checks. DFS, BFS & Graph Traversal When to use it: anything where things are connected, even if it’s not called a graph. [Example: Number of Islands] Graphs show up everywhere, even when a problem doesn’t explicitly call itself a graph. Depth-First Search (DFS) and Breadth-First Search (BFS) are foundational patterns for exploring connections between nodes, whether you’re working on grids, trees, social networks, or dependency graphs. DFS is great for exploring all possible paths, detecting cycles, or backtracking, while BFS is ideal for finding the shortest path or levels of separation. function dfs(graph: Graph, start: number, visited = new Set<number>()): void { if (visited.has(start)) return; visited.add(start); console.log(start); for (const neighbor of graph[start] || []) { dfs(graph, neighbor, visited); } } function bfs(graph: Graph, start: number): void { const visited = new Set<number>(); const queue: number[] = [start]; while (queue.length > 0) { const node = queue.shift()!; if (visited.has(node)) continue; visited.add(node); console.log(node); for (const neighbor of graph[node] || []) { if (!visited.has(neighbor)) { queue.push(neighbor); } } } } Recursion & Backtracking When to use it: combination generation, decision trees, searching all valid paths. [Example: N-Queens] Recursion is a natural way to explore problems that involve nested decisions, combinations, or sequences, while backtracking adds a layer of control to prune paths that can’t lead to a solution. This pattern shines in problems like generating permutations, solving puzzles, or navigating grids with constraints. By breaking a problem into smaller subproblems and exploring options systematically, recursion with backtracking allows you to traverse large solution spaces efficiently, avoiding redundant work while still covering all valid possibilities. function backtrack(path: any[] = []): void { if (shouldStop(path)) { console.log(path); return; } // Explore all possible choices for (const choice of getChoices(path)) { path.push(choice); backtrack(path); path.pop(); } } Dynamic Programming When to use it: overlapping subproblems and optimal structure. [Example: Climbing Stairs] Backtracking is a systematic way of exploring all possible solutions to a problem while pruning invalid paths early. It’s especially useful in constraint-heavy problems like N-Queens, Sudoku, combination generation, or word search. The key idea is to make a choice, explore the consequences recursively, and then undo the choice before trying the next option. By “going back” whenever a path cannot yield a valid solution, backtracking navigates large solution spaces without blindly trying every possibility. function dp(state: any, memo: Map<string, number> = new Map()): number { const key = serializeState(state); if (memo.has(key)) return memo.get(key)!; // base case if (isBaseCase(state)) { return baseCaseValue(state); } // compute result using smaller subproblems let result = 0; for (const subState of getSubStates(state)) { result = combine(result, dp(subState, memo)); } memo.set(key, result); return result; } The Pattern Mindset You don’t need to know every pattern. You need to know enough to say: “This reminds me of ___.” That moment of recognition is worth more than memorizing 500 solutions. The Transformation Trick Some of the best solutions don’t solve the problem as stated, they turn it into a different problem that’s easier to reason about. Convert text to numbers, numbers to graphs, graphs to grids Replace recursion with dynamic programming Solve the complement of the problem instead of the original Sort first → simplify everything Turn 2D into 1D or vice versa Move from event-based thinking to state-based thinking You need to learn to ask: “What is this if I stop looking at it literally?”. Modeling is not optional, it is the problem-solving process. Debugging Your Thinking Most people debug after writing code. Strong problem-solvers debug before writing it. Quick self-checks: Does my idea handle edge cases? Does complexity match the constraints? What assumptions might be false? If you can’t convince yourself on paper that your idea works, your code definitely won’t. Final Thoughts Algorithmic problem-solving isn’t about memorizing every trick or knowing every edge-case solution, it’s about thinking clearly, modeling correctly, and recognizing patterns. When you approach problems by breaking them down, mapping them to the right structures, and applying core techniques like sliding windows, two pointers, recursion, or dynamic programming, you gain a toolkit that works across any challenge. The real skill lies in seeing the underlying structure of a problem and knowing how to navigate it. Every problem solved this way reinforces your mental models, making the next one easier to crack. Focus on understanding, modeling, and pattern recognition, and the “tricks” will follow naturally.