Onlytwentyfive
A curated collection of essential algorithmic patterns. Each section focuses on why the pattern exists, when to use it, and how it is applied through real problems.
Arrays / Two Pointers / Hashing
Think of arrays as a long table where you can scan, compare, and move pointers strategically instead of checking every possibility. Two pointers work like two hands moving from different sides to meet in the middle efficiently, while hashing acts like instant lookup notes that let you know whether you’ve seen something before without searching again.
3Sumfunction threeSum(nums: number[]): number[][] {
nums.sort((a, b) => a - b);
const result: number[][] = [];
for (let i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue;
if (nums[i] > 0) break;
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
result.push([nums[i], nums[left], nums[right]]);
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
Container With Most Water
function maxArea(height: number[]): number {
let left = 0;
let right = height.length - 1;
let maxVolume = 0;
while (left < right) {
const minHeight = Math.min(height[left], height[right]);
const width = right - left;
maxVolume = Math.max(maxVolume, minHeight * width);
height[left] < height[right] ? left++ : right--;
}
return maxVolume;
}
Longest Consecutive Sequence
function longestConsecutive(nums: number[]): number {
const set = new Set(nums);
let longest = 0;
for (const num of set) {
if (!set.has(num - 1)) {
let currentNum = num;
let currentStreak = 1;
while (set.has(currentNum + 1)) {
currentNum++;
currentStreak++;
}
longest = Math.max(longest, currentStreak);
}
}
return longest;
}
Strings
String problems are like reading a sentence through a moving window: you slide forward, keep track of what’s inside, and adjust when something invalid appears. Instead of restarting every time, you reuse previous information to find valid substrings efficiently.
Longest Substring Without Repeating Charactersfunction lengthOfLongestSubstring(s) {
let left = 0;
let seen = new Set();
let maxLength = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
while (seen.has(char)) {
seen.delete(s[left]);
left++;
}
seen.add(char);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
Minimum Window Substring
function minWindow(s: string, t: string): string {
if (!s || !t || s.length < t.length) return "";
const map = new Array(128).fill(0);
let count = t.length;
let start = 0;
let end = 0;
let minLen = Infinity;
let startIndex = 0;
for (let i = 0; i < t.length; i++) {
map[t.charCodeAt(i)]++;
}
while (end < s.length) {
if (map[s.charCodeAt(end)]-- > 0) count--;
end++;
while (count === 0) {
if (end - start < minLen) {
minLen = end - start;
startIndex = start;
}
if (map[s.charCodeAt(start)]++ === 0) count++;
start++;
}
}
return minLen === Infinity ? "" : s.substring(startIndex, startIndex + minLen);
}
Dynamic Programming
Dynamic Programming is like solving a big problem by remembering the answers to smaller ones so you never recompute the same thing twice. Once a result is known, it’s stored and reused, allowing you to build the final solution step by step with optimal efficiency.
Climbing Stairsfunction climbStairs(n: number): number {
if (n <= 2) return n;
let prev = 1, current = 2;
for (let i = 3; i <= n; i++) {
const next = prev + current;
prev = current;
current = next;
}
return current;
}
House Robber
function rob(nums: number[]): number {
let preview = 0;
let current = 0;
for (const num of nums) {
const temporal = Math.max(current, preview + num);
preview = current;
current = temporal;
}
return current;
}
Longest Palindromic Substring
function longestPalindrome(s: string): string {
if (s.length < 1) return "";
let start = 0;
let end = 0;
const expandAroundCenter = (l: number, r: number) => {
while (l >= 0 && r < s.length && s[l] === s[r]) {
l--; r++;
}
return r - l - 1;
};
for (let i = 0; i < s.length; i++) {
const len = Math.max(
expandAroundCenter(i, i),
expandAroundCenter(i, i + 1)
);
if (len > end - start) {
start = i - Math.floor((len - 1) / 2);
end = i + Math.floor(len / 2);
}
}
return s.substring(start, end + 1);
}
Coin Change
function coinChange(coins: number[], amount: number): number {
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let coin of coins) {
for (let i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
Partition Equal Subset Sum
function canPartition(nums: number[]): boolean {
const total = nums.reduce((a, b) => a + b, 0);
if (total % 2 !== 0) return false;
const target = total / 2;
const dp = new Array(target + 1).fill(false);
dp[0] = true;
for (const num of nums) {
for (let s = target; s >= num; s--) {
dp[s] = dp[s] || dp[s - num];
}
}
return dp[target];
}
Trees / Binary Trees / BST
Trees work like organizational charts where each decision splits into smaller decisions. Traversing a tree means visiting nodes in a structured order, and understanding parent-child relationships allows you to efficiently search, compare, and aggregate information.
Binary Tree Level Order Traversalfunction levelOrder(root: TreeNode | null): number[][] {
if (!root) return [];
const result: number[][] = [];
const queue: TreeNode[] = [root];
while (queue.length > 0) {
const levelSize = queue.length;
const level: number[] = [];
for (let i = 0; i < levelSize; i++) {
const node = queue.shift()!;
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level);
}
return result;
}
Diameter of Binary Tree
function diameterOfBinaryTree(root: TreeNode | null): number {
let diameter = 0;
function depth(node: TreeNode | null): number {
if (!node) return 0;
const left = depth(node.left);
const right = depth(node.right);
diameter = Math.max(diameter, left + right);
return Math.max(left, right) + 1;
}
depth(root);
return diameter;
}
Lowest Common Ancestor of a BST
function lowestCommonAncestor(
root: TreeNode | null,
p: TreeNode | null,
q: TreeNode | null
): TreeNode | null {
let current = root;
while (current) {
if (p!.val < current.val && q!.val < current.val) {
current = current.left;
} else if (p!.val > current.val && q!.val > current.val) {
current = current.right;
} else {
return current;
}
}
return null;
}
Backtracking
Backtracking is like exploring a maze by trying a path, and if it fails, stepping back to try another route. You build partial solutions, abandon them when they stop working, and continue until all valid combinations are discovered.
Subsetsfunction subsets(nums: number[]): number[][] {
const results: number[][] = [];
const backtrack = (index: number, path: number[]) => {
results.push([...path]);
for (let i = index; i < nums.length; i++) {
path.push(nums[i]);
backtrack(i + 1, path);
path.pop();
}
};
backtrack(0, []);
return results;
}
Combination
Sum
function combinationSum(candidates: number[], target: number): number[][] {
const result: number[][] = [];
const backtrack = (start: number, sum: number, path: number[]) => {
if (sum === target) {
result.push([...path]);
return;
}
if (sum > target) return;
for (let i = start; i < candidates.length; i++) {
path.push(candidates[i]);
backtrack(i, sum + candidates[i], path);
path.pop();
}
};
backtrack(0, 0, []);
return result;
}
Word Search
function exist(board: string[][], word: string): boolean {
const rows = board.length;
const cols = board[0].length;
const dfs = (x: number, y: number, index: number): boolean => {
if (index === word.length) return true;
if (x < 0 || y < 0 || x >= rows || y >= cols || board[x][y] !== word[index]) {
return false;
}
const temp = board[x][y];
board[x][y] = "#";
const found =
dfs(x + 1, y, index + 1) ||
dfs(x - 1, y, index + 1) ||
dfs(x, y + 1, index + 1) ||
dfs(x, y - 1, index + 1);
board[x][y] = temp;
return found;
};
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (board[i][j] === word[0] && dfs(i, j, 0)) return true;
}
}
return false;
}
Linked List
Backtracking is like exploring a maze by trying a path, and if it fails, stepping back to try another route. You build partial solutions, abandon them when they stop working, and continue until all valid combinations are discovered.
Reverse Linked Listfunction reverseList(head: ListNode | null): ListNode | null {
let prev = null;
let current = head;
while (current !== null) {
const savenode = current.next;
current.next = prev;
prev = current;
current = savenode;
}
return prev;
}
Linked List Cycle
function hasCycle(head: ListNode | null): boolean {
if (!head || !head.next) return false;
let slow = head;
let fast = head.next;
while (fast && fast.next) {
if (slow === fast) return true;
slow = slow.next!;
fast = fast.next.next!;
}
return false;
}
Merge Two Sorted Lists
function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null {
let dummy = new ListNode(-1);
let current = dummy;
while (list1 !== null && list2 !== null) {
if (list1.val < list2.val) {
current.next = list1;
list1 = list1.next;
} else {
current.next = list2;
list2 = list2.next;
}
current = current.next;
}
if (list1 !== null) current.next = list1;
if (list2 !== null) current.next = list2;
return dummy.next;
}
Stack
Linked lists are like train cars connected one by one, where each car only knows the next. Since there’s no random access, problems focus on pointer manipulation, careful traversal, and maintaining structure while reversing, merging, or detecting cycles.
Valid Parenthesesfunction isValid(s: string): boolean {
if (s.length % 2 !== 0) return false;
const openingSet = new Set(['(', '{', '[']);
const brackets: Record = {
')': '(',
'}': '{',
']': '['
};
let stack: string[] = [];
for (let char of s) {
if (openingSet.has(char)) {
stack.push(char);
} else {
if (stack.length === 0 || stack.pop() !== brackets[char]) {
return false;
}
}
}
return stack.length === 0;
}
Daily Temperatures
function dailyTemperatures(temperatures: number[]): number[] {
const result = new Array(temperatures.length).fill(0);
const stack: number[] = [];
for (let i = 0; i < temperatures.length; i++) {
while (stack.length > 0 &&
temperatures[i] > temperatures[stack[stack.length - 1]]) {
const prevIndex = stack.pop()!;
result[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return result;
}
Graphs / BFS / DFS / Union-Find
Graphs represent networks where everything is connected in multiple ways. Traversal techniques explore these connections systematically, while Union-Find helps determine whether nodes belong together, detect cycles, or manage connected components efficiently.
Number of Islandsfunction numIslands(grid: string[][]): number {
if (!grid.length) return 0;
let count = 0;
const dfs = (x: number, y: number) => {
if (
x < 0 || x >= grid.length ||
y < 0 || y >= grid[0].length ||
grid[x][y] === '0'
) return;
grid[x][y] = '0';
dfs(x + 1, y);
dfs(x - 1, y);
dfs(x, y + 1);
dfs(x, y - 1);
};
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[0].length; j++) {
if (grid[i][j] === '1') {
count++;
dfs(i, j);
}
}
}
return count;
}
Clone Graph
function cloneGraph(node: Node | null): Node | null {
if (!node) return null;
const map = new Map();
const clone = (n: Node): Node => {
if (!map.has(n)) {
map.set(n, new Node(n.val));
map.get(n)!.neighbors = n.neighbors.map(clone);
}
return map.get(n)!;
};
return clone(node);
}
Course Schedule
function canFinish(numCourses: number, prerequisites: number[][]): boolean {
const graph: number[][] = Array.from({ length: numCourses }, () => []);
const state = new Array(numCourses).fill(0);
for (const [course, prereq] of prerequisites) {
graph[prereq].push(course);
}
const dfs = (course: number): boolean => {
if (state[course] === 1) return false;
if (state[course] === 2) return true;
state[course] = 1;
for (const next of graph[course]) {
if (!dfs(next)) return false;
}
state[course] = 2;
return true;
};
for (let i = 0; i < numCourses; i++) {
if (!dfs(i)) return false;
}
return true;
}
Redundant Connection
function findRedundantConnection(edges: number[][]): number[] {
const parent = new Array(edges.length + 1).fill(0).map((_, i) => i);
const find = (x: number): number => {
if (parent[x] !== x) parent[x] = find(parent[x]);
return parent[x];
};
const union = (a: number, b: number): boolean => {
const rootA = find(a);
const rootB = find(b);
if (rootA === rootB) return false;
parent[rootB] = rootA;
return true;
};
for (const [a, b] of edges) {
if (!union(a, b)) return [a, b];
}
return [];
}