Data Structures & Algorithms: Complete Guide
Master fundamental data structures and algorithms with executable code examples in TypeScript and Kotlin. From Big O complexity to dynamic programming, each concept includes visualizations, practical implementations, and interactive challenges. The editor supports maximize and font size adjustment.

Table of Contents
This comprehensive guide covers everything you need to master data structures and algorithms. Each section includes detailed explanations, visual diagrams, and executable code in both TypeScript and Kotlin.
1. Big O Notation & Time Complexity

Big O notation is the language we use to describe how efficient an algorithm is. It tells us how the runtime or space requirements grow as the input size increases. Understanding Big O is crucial for writing efficient code and acing technical interviews.
The difference between O(n) and O(n²) can mean the difference between your code running in 1 second vs 11 days for 1 million elements. Companies like Google and Amazon specifically test candidates on their ability to analyze and optimize algorithm complexity.
Common Time Complexities (Best to Worst)
| Notation | Name | Example | n=1000 |
|---|---|---|---|
| O(1) | Constant | Array access, Hash lookup | 1 |
| O(log n) | Logarithmic | Binary search | ~10 |
| O(n) | Linear | Simple loop | 1,000 |
| O(n log n) | Linearithmic | Merge sort, Quick sort | ~10,000 |
| O(n²) | Quadratic | Nested loops, Bubble sort | 1,000,000 |
| O(2ⁿ) | Exponential | Recursive Fibonacci | HUGE! |
O(1) - Constant Time
The operation takes the same time regardless of input size. Whether your array has 10 elements or 10 million, accessing an element by index is always one operation. Hash table lookups are also O(1) on average because the hash function directly computes the location.
// O(1) - Constant Time// The operation takes the same time regardless of input size// Think of it like looking up a word in a dictionary by page numberfunction getFirstElement<T>(arr: T[]): T | undefined {return arr[0]; // Always one operation, no matter if array has 10 or 10 million elements}function getLastElement<T>(arr: T[]): T | undefined {return arr[arr.length - 1]; // Still O(1) - direct index access}// Hash table lookup is O(1) on averageconst hashMap = new Map<string, number>();hashMap.set("apple", 5);hashMap.set("banana", 3);console.log(hashMap.get("apple")); // O(1) lookup// Array access by indexconst numbers = [10, 20, 30, 40, 50];console.log("First:", getFirstElement(numbers)); // 10console.log("Last:", getLastElement(numbers)); // 50console.log("Direct access numbers[2]:", numbers[2]); // 30 - O(1)
O(n) - Linear Time
Time grows proportionally with input size. If you need to examine every element at least once (like finding a maximum or summing all values), you cannot do better than O(n). This is often the baseline for many problems.
// O(n) - Linear Time// Time grows proportionally with input size// If input doubles, time roughly doublesfunction findMax(arr: number[]): number {if (arr.length === 0) throw new Error("Empty array");let max = arr[0];let comparisons = 0;for (let i = 1; i < arr.length; i++) {comparisons++;if (arr[i] > max) {max = arr[i];}}console.log(`Array size: ${arr.length}, Comparisons: ${comparisons}`);return max;}// Linear search - must check each elementfunction linearSearch<T>(arr: T[], target: T): number {for (let i = 0; i < arr.length; i++) {if (arr[i] === target) return i;}return -1; // Not found}// Sum all elements - must visit each onefunction sumArray(arr: number[]): number {return arr.reduce((sum, num) => sum + num, 0);}const data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5];console.log("Max:", findMax(data));console.log("Index of 9:", linearSearch(data, 9));console.log("Sum:", sumArray(data));
O(log n) - Logarithmic Time
Halves the problem size with each step. Binary search is the classic example. For 1 billion elements, you need only about 30 comparisons! This is incredibly powerful. Any algorithm that divides the problem in half each iteration is O(log n).
// O(log n) - Logarithmic Time// Halves the problem size with each step// EXTREMELY efficient for large datasets!// 1 billion elements? Only ~30 steps needed!function binarySearch(arr: number[], target: number): number {let left = 0;let right = arr.length - 1;let steps = 0;while (left <= right) {steps++;const mid = Math.floor((left + right) / 2);console.log(`Step ${steps}: Checking index ${mid}, value ${arr[mid]}`);if (arr[mid] === target) {console.log(`Found in ${steps} steps!`);return mid;}if (arr[mid] < target) {left = mid + 1; // Discard left half} else {right = mid - 1; // Discard right half}}console.log(`Not found after ${steps} steps`);return -1;}// IMPORTANT: Array must be sorted for binary search!const sortedArray = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29];console.log("Searching for 19 in array of", sortedArray.length, "elements:");console.log("Result index:", binarySearch(sortedArray, 19));console.log("\nSearching for 10 (not in array):");binarySearch(sortedArray, 10);
O(n²) - Quadratic Time
Nested loops over the data = n × n operations. This is where algorithms start becoming slow for large inputs. Bubble sort, selection sort, and checking all pairs are O(n²). For 10,000 elements, that's 100 million operations!
// O(n²) - Quadratic Time// Nested loops over the data = n * n operations// AVOID for large inputs! 10,000 elements = 100,000,000 operations!function bubbleSort(arr: number[]): number[] {const result = [...arr];const n = result.length;let swaps = 0;let comparisons = 0;for (let i = 0; i < n; i++) {for (let j = 0; j < n - i - 1; j++) {comparisons++;if (result[j] > result[j + 1]) {[result[j], result[j + 1]] = [result[j + 1], result[j]];swaps++;}}}console.log(`Array size: ${n}`);console.log(`Total comparisons: ${comparisons}`);console.log(`Total swaps: ${swaps}`);return result;}// Finding all pairs is also O(n²)function findAllPairs(arr: number[]): [number, number][] {const pairs: [number, number][] = [];for (let i = 0; i < arr.length; i++) {for (let j = i + 1; j < arr.length; j++) {pairs.push([arr[i], arr[j]]);}}return pairs;}const unsorted = [64, 34, 25, 12, 22, 11, 90];console.log("Original:", unsorted);console.log("Sorted:", bubbleSort(unsorted));console.log("\nAll pairs from [1,2,3]:", findAllPairs([1, 2, 3]));
O(n log n) - Linearithmic Time
The sweet spot for comparison-based sorting. Merge sort and quick sort achieve this complexity. It's proven that you cannot sort faster using only comparisons. This is why knowing these algorithms is essential for interviews.
// O(n log n) - Linearithmic Time// The "sweet spot" for comparison-based sorting// Merge Sort, Quick Sort, Heap Sort all achieve thisfunction mergeSort(arr: number[]): number[] {if (arr.length <= 1) return arr;const mid = Math.floor(arr.length / 2);const left = mergeSort(arr.slice(0, mid)); // log n levels of recursionconst right = mergeSort(arr.slice(mid)); // log n levels of recursionreturn merge(left, right); // n operations at each level}function merge(left: number[], right: number[]): number[] {const result: number[] = [];let i = 0, j = 0;while (i < left.length && j < right.length) {if (left[i] <= right[j]) {result.push(left[i++]);} else {result.push(right[j++]);}}return [...result, ...left.slice(i), ...right.slice(j)];}const data = [38, 27, 43, 3, 9, 82, 10];console.log("Original:", data);console.log("Merge Sorted:", mergeSort(data));// Comparison with O(n²):// n = 1,000,000// O(n²) = 1,000,000,000,000 operations (1 trillion!)// O(n log n) = 20,000,000 operations (20 million)// That's 50,000x faster!
Always state the time AND space complexity of your solution. For example: "This solution is O(n) time and O(1) space." Interviewers specifically look for this analysis. If you can explain why your solution has that complexity, you'll stand out.
2. Arrays - The Foundation

Arrays are the most fundamental data structure. They store elements in contiguous (adjacent) memory locations, which makes them incredibly cache-friendly and fast for access. Most other data structures are built on top of arrays!
Arrays give O(1) random access because memory addresses are calculated directly: address = base + (index × element_size). This formula works instantly regardless of array size. This is why arrays are so powerful!
Time Complexity Summary
| Operation | Time | Why? |
|---|---|---|
| Access by index | O(1) | Direct address calculation |
| Search (unsorted) | O(n) | Must check each element |
| Search (sorted) | O(log n) | Binary search |
| Insert at end | O(1)* | *Amortized - occasionally O(n) to resize |
| Insert at beginning | O(n) | Must shift all elements |
| Delete at end | O(1) | Just update length |
| Delete at beginning | O(n) | Must shift all elements |
Array Basics & Operations
Understanding array operations and their complexities is essential. Pay attention to which operations are O(1) vs O(n) - this knowledge helps you write efficient code.
// Arrays: The Foundation of All Data Structures// Contiguous memory, O(1) random access, cache-friendly// Creating arraysconst numbers: number[] = [1, 2, 3, 4, 5];const strings: string[] = ["apple", "banana", "cherry"];const mixed: (number | string)[] = [1, "two", 3];// ========== BASIC OPERATIONS ==========// ACCESS - O(1): Direct index accessconsole.log("First element:", numbers[0]); // 1console.log("Last element:", numbers[numbers.length - 1]); // 5console.log("Middle element:", numbers[Math.floor(numbers.length / 2)]); // 3// SEARCH - O(n): Must check each elementconst index = numbers.indexOf(3);console.log("Index of 3:", index); // 2console.log("Includes 4?", numbers.includes(4)); // true// INSERT at end - O(1) amortizednumbers.push(6);console.log("After push(6):", numbers); // [1,2,3,4,5,6]// INSERT at beginning - O(n): shifts all elements!numbers.unshift(0);console.log("After unshift(0):", numbers); // [0,1,2,3,4,5,6]// INSERT at index - O(n)numbers.splice(3, 0, 99); // Insert 99 at index 3console.log("After splice:", numbers); // [0,1,2,99,3,4,5,6]// DELETE from end - O(1)const popped = numbers.pop();console.log("Popped:", popped, "Array:", numbers);// DELETE from beginning - O(n)const shifted = numbers.shift();console.log("Shifted:", shifted, "Array:", numbers);// DELETE at index - O(n)numbers.splice(2, 1); // Remove 1 element at index 2console.log("After delete at index 2:", numbers);
Essential Array Methods
These methods (map, filter, reduce, find, etc.) are your daily tools. Master them and you can solve most array problems elegantly. Chaining these methods is a powerful pattern.
// Array Methods Every Developer Must Know// These are your daily tools for array manipulationconst nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];// ========== TRANSFORMATION METHODS ==========// MAP: Transform each element - O(n)const doubled = nums.map(n => n * 2);console.log("Doubled:", doubled); // [2,4,6,8,10,12,14,16,18,20]// FILTER: Keep elements matching condition - O(n)const evens = nums.filter(n => n % 2 === 0);console.log("Evens:", evens); // [2,4,6,8,10]// REDUCE: Accumulate to single value - O(n)const sum = nums.reduce((acc, n) => acc + n, 0);console.log("Sum:", sum); // 55const product = nums.reduce((acc, n) => acc * n, 1);console.log("Product:", product); // 3628800// FIND: Get first matching element - O(n)const firstEven = nums.find(n => n % 2 === 0);console.log("First even:", firstEven); // 2// FINDINDEX: Get index of first match - O(n)const firstEvenIndex = nums.findIndex(n => n % 2 === 0);console.log("First even index:", firstEvenIndex); // 1// SOME: Check if any element matches - O(n)const hasEven = nums.some(n => n % 2 === 0);console.log("Has even?", hasEven); // true// EVERY: Check if all elements match - O(n)const allPositive = nums.every(n => n > 0);console.log("All positive?", allPositive); // true// ========== CHAINING METHODS ==========// Powerful pattern: chain multiple operationsconst result = nums.filter(n => n % 2 === 0) // Keep evens: [2,4,6,8,10].map(n => n * n) // Square: [4,16,36,64,100].reduce((a, b) => a + b, 0); // Sum: 220console.log("Evens squared summed:", result);
Interview Problem: Two Sum
This is the #1 most asked interview question! Given an array and a target, find two numbers that add up to the target. The brute force is O(n²), but using a hash map gives us O(n). This optimization pattern appears constantly in interviews.
// ===== INTERVIEW CLASSIC: Two Sum =====// Given an array and target, find two numbers that add up to target// Return their indices// BRUTE FORCE: O(n²) - Check all pairsfunction twoSumBrute(nums: number[], target: number): number[] {for (let i = 0; i < nums.length; i++) {for (let j = i + 1; j < nums.length; j++) {if (nums[i] + nums[j] === target) {return [i, j];}}}return [];}// OPTIMAL: O(n) - Use hash map to find complementfunction twoSum(nums: number[], target: number): number[] {const seen = new Map<number, number>(); // value -> indexfor (let i = 0; i < nums.length; i++) {const complement = target - nums[i];if (seen.has(complement)) {return [seen.get(complement)!, i];}seen.set(nums[i], i);}return [];}// Test casesconsole.log("Example 1:", twoSum([2, 7, 11, 15], 9)); // [0, 1]console.log("Example 2:", twoSum([3, 2, 4], 6)); // [1, 2]console.log("Example 3:", twoSum([3, 3], 6)); // [0, 1]// Why hash map works:// For each number, we ask: "Have I seen its complement before?"// If yes, we found our pair!// If no, store current number for future lookups
Interview Problem: Maximum Subarray (Kadane's Algorithm)
Another must-know algorithm! Find the contiguous subarray with the largest sum. Kadane's algorithm solves this in O(n) time with O(1) space. The key insight is deciding at each position: extend the current subarray or start fresh?
// ===== INTERVIEW CLASSIC: Maximum Subarray (Kadane's Algorithm) =====// Find the contiguous subarray with the largest sum// This is THE algorithm to know for array problems!function maxSubArray(nums: number[]): number {if (nums.length === 0) return 0;let maxSum = nums[0]; // Best sum found so farlet currentSum = nums[0]; // Sum of current subarray// Track the actual subarray (bonus)let start = 0, end = 0, tempStart = 0;for (let i = 1; i < nums.length; i++) {// Key insight: Should we extend current subarray or start fresh?// If currentSum + nums[i] < nums[i], start fresh!if (currentSum + nums[i] < nums[i]) {currentSum = nums[i];tempStart = i;} else {currentSum = currentSum + nums[i];}// Update max if current is betterif (currentSum > maxSum) {maxSum = currentSum;start = tempStart;end = i;}}console.log(`Max subarray: [${nums.slice(start, end + 1)}]`);console.log(`From index ${start} to ${end}`);return maxSum;}// Test casesconsole.log("Max sum:", maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6console.log("\nMax sum:", maxSubArray([1])); // 1console.log("\nMax sum:", maxSubArray([5, 4, -1, 7, 8])); // 23// Visual walkthrough for [-2, 1, -3, 4, -1, 2, 1, -5, 4]:// i=0: current=-2, max=-2// i=1: current=1 (start fresh), max=1// i=2: current=-2, max=1// i=3: current=4 (start fresh), max=4// i=4: current=3, max=4// i=5: current=5, max=5// i=6: current=6, max=6 ← Winner: [4,-1,2,1]// i=7: current=1, max=6// i=8: current=5, max=6
1. Two Pointers: Start from both ends, move toward middle
2. Sliding Window: Track a window of elements as you iterate
3. Hash Map: Trade space for time by storing seen elements
4. Prefix Sum: Precompute cumulative sums for range queries
3. Linked Lists


Linked Lists store elements in nodes connected by pointers. Unlike arrays, elements are not stored contiguously in memory. Each node contains data and a reference to the next node. This structure enables O(1) insertions and deletions at known positions.
Arrays win: Random access, cache locality, less memory overhead
Linked Lists win: Insert/delete at beginning, dynamic size, no resizing
In practice, arrays (dynamic arrays/vectors) are used more often due to cache performance.
Time Complexity Comparison
| Operation | Array | Linked List |
|---|---|---|
| Access by index | O(1) | O(n) |
| Insert at beginning | O(n) | O(1) |
| Insert at end | O(1)* | O(1) with tail |
| Insert at middle | O(n) | O(1) if node known |
| Delete at beginning | O(n) | O(1) |
| Search | O(n) | O(n) |
Singly Linked List Implementation
A complete linked list implementation with all common operations. Understanding this code deeply will help you solve many interview problems involving linked lists.
// ===== SINGLY LINKED LIST =====// Each node points to the next node// Advantages: O(1) insert/delete at known position// Disadvantages: No random access, extra memory for pointersclass ListNode<T> {value: T;next: ListNode<T> | null = null;constructor(value: T) {this.value = value;}}class SinglyLinkedList<T> {head: ListNode<T> | null = null;tail: ListNode<T> | null = null;length: number = 0;// Add to end - O(1) with tail pointerappend(value: T): this {const node = new ListNode(value);if (!this.head) {this.head = node;this.tail = node;} else {this.tail!.next = node;this.tail = node;}this.length++;return this;}// Add to beginning - O(1)prepend(value: T): this {const node = new ListNode(value);node.next = this.head;this.head = node;if (!this.tail) {this.tail = node;}this.length++;return this;}// Get at index - O(n)getAt(index: number): T | null {if (index < 0 || index >= this.length) return null;let current = this.head;for (let i = 0; i < index; i++) {current = current!.next;}return current!.value;}// Insert at index - O(n)insertAt(index: number, value: T): boolean {if (index < 0 || index > this.length) return false;if (index === 0) { this.prepend(value); return true; }if (index === this.length) { this.append(value); return true; }const node = new ListNode(value);let current = this.head;for (let i = 0; i < index - 1; i++) {current = current!.next;}node.next = current!.next;current!.next = node;this.length++;return true;}// Remove at index - O(n)removeAt(index: number): T | null {if (index < 0 || index >= this.length) return null;let removed: ListNode<T>;if (index === 0) {removed = this.head!;this.head = this.head!.next;if (this.length === 1) this.tail = null;} else {let current = this.head;for (let i = 0; i < index - 1; i++) {current = current!.next;}removed = current!.next!;current!.next = removed.next;if (index === this.length - 1) this.tail = current;}this.length--;return removed.value;}// Print listprint(): string {const values: T[] = [];let current = this.head;while (current) {values.push(current.value);current = current.next;}return values.join(" -> ") + " -> null";}}// Democonst list = new SinglyLinkedList<number>();list.append(1).append(2).append(3).append(4);console.log("Initial:", list.print());list.prepend(0);console.log("After prepend(0):", list.print());list.insertAt(2, 99);console.log("After insertAt(2, 99):", list.print());console.log("Get at index 3:", list.getAt(3));list.removeAt(2);console.log("After removeAt(2):", list.print());console.log("Length:", list.length);
Interview Classic: Reverse a Linked List
This is THE most common linked list interview question! You must be able to write this in your sleep. Both iterative and recursive approaches are important to know. The iterative solution is preferred (O(1) space).
// ===== INTERVIEW CLASSIC: Reverse a Linked List =====// THE most common linked list interview question!// Learn this pattern cold - it appears everywhereclass ListNode {constructor(public val: number,public next: ListNode | null = null) {}}// ITERATIVE APPROACH - O(n) time, O(1) space// This is the preferred solutionfunction reverseList(head: ListNode | null): ListNode | null {let prev: ListNode | null = null;let current = head;while (current !== null) {// 1. Save the next node (we'll lose reference otherwise)const nextTemp = current.next;// 2. Reverse the pointercurrent.next = prev;// 3. Move prev and current forwardprev = current;current = nextTemp;}// prev is now the new headreturn prev;}// RECURSIVE APPROACH - O(n) time, O(n) space (call stack)function reverseListRecursive(head: ListNode | null): ListNode | null {// Base case: empty or single nodeif (head === null || head.next === null) {return head;}// Recursively reverse the restconst newHead = reverseListRecursive(head.next);// The magic: make the next node point back to ushead.next.next = head;head.next = null;return newHead;}// Helper to print listfunction printList(head: ListNode | null): string {const values: number[] = [];while (head) {values.push(head.val);head = head.next;}return values.join(" -> ");}// Create: 1 -> 2 -> 3 -> 4 -> 5let head: ListNode | null = new ListNode(1,new ListNode(2,new ListNode(3,new ListNode(4,new ListNode(5)))));console.log("Original:", printList(head));head = reverseList(head);console.log("Reversed:", printList(head));// Visual walkthrough:// Initial: 1 -> 2 -> 3 -> 4 -> 5 -> null// ^// current, prev=null//// Step 1: null <- 1 2 -> 3 -> 4 -> 5 -> null// ^ ^// prev current//// Step 2: null <- 1 <- 2 3 -> 4 -> 5 -> null// ^ ^// prev current// ... continue until current is null
Interview Classic: Detect Cycle (Floyd's Algorithm)
Floyd's Tortoise and Hare algorithm is a brilliant O(n) time, O(1) space solution to detect cycles. The slow pointer moves 1 step, fast moves 2 steps. If there's a cycle, they will eventually meet!
// ===== INTERVIEW CLASSIC: Detect Cycle in Linked List =====// Floyd's Cycle Detection (Tortoise and Hare)// Brilliant algorithm using two pointers at different speedsclass ListNode {constructor(public val: number,public next: ListNode | null = null) {}}// Detect if cycle exists - O(n) time, O(1) spacefunction hasCycle(head: ListNode | null): boolean {if (!head || !head.next) return false;let slow = head; // Moves 1 step at a timelet fast = head.next; // Moves 2 steps at a timewhile (slow !== fast) {// If fast reaches end, no cycleif (!fast || !fast.next) {return false;}slow = slow.next!;fast = fast.next.next;}// They met - there's a cycle!return true;}// Find where cycle begins - O(n) time, O(1) spacefunction detectCycle(head: ListNode | null): ListNode | null {if (!head || !head.next) return null;let slow = head;let fast = head;// Phase 1: Detect if cycle existswhile (fast && fast.next) {slow = slow.next!;fast = fast.next.next;if (slow === fast) {// Phase 2: Find cycle start// Mathematical proof: distance from head to cycle start// equals distance from meeting point to cycle startlet pointer = head;while (pointer !== slow) {pointer = pointer.next!;slow = slow.next!;}return pointer;}}return null; // No cycle}// Create a list with cycle: 1 -> 2 -> 3 -> 4 -> 5// ^ |// |_________|const node1 = new ListNode(1);const node2 = new ListNode(2);const node3 = new ListNode(3);const node4 = new ListNode(4);const node5 = new ListNode(5);node1.next = node2;node2.next = node3;node3.next = node4;node4.next = node5;node5.next = node3; // Creates cycle back to node3console.log("Has cycle:", hasCycle(node1)); // trueconst cycleStart = detectCycle(node1);console.log("Cycle starts at node with value:", cycleStart?.val); // 3// Why does Floyd's algorithm work?// If there's a cycle, fast will eventually "lap" slow// They MUST meet because fast gains 1 node per iteration// Meeting point math proves the cycle start location
1. Two Pointers (Fast/Slow): Cycle detection, find middle, nth from end
2. Dummy Node: Simplifies edge cases for head modifications
3. Reverse in Groups: Reverse k nodes at a time
4. Merge Lists: Combine two sorted lists
4. Stacks - LIFO Structure

Stacks follow Last In, First Out (LIFO). Think of a stack of plates - you can only add or remove from the top. This simple constraint makes stacks incredibly useful for problems involving nested structures, undo operations, and expression evaluation.
push: Add to top | pop: Remove from top |peek: View top | isEmpty: Check if empty
Stack Implementation
// ===== STACK: Last In, First Out (LIFO) =====// Think of a stack of plates - you can only add/remove from the top// Operations: push, pop, peek - all O(1)!class Stack<T> {private items: T[] = [];// Add to top - O(1)push(item: T): void {this.items.push(item);}// Remove from top - O(1)pop(): T | undefined {return this.items.pop();}// View top without removing - O(1)peek(): T | undefined {return this.items[this.items.length - 1];}// Check if empty - O(1)isEmpty(): boolean {return this.items.length === 0;}// Get size - O(1)size(): number {return this.items.length;}// Clear all elementsclear(): void {this.items = [];}// Print stack (for debugging)print(): string {return `Stack (top->bottom): [${this.items.slice().reverse().join(", ")}]`;}}// Democonst stack = new Stack<number>();console.log("=== Stack Operations ===");stack.push(10);stack.push(20);stack.push(30);console.log(stack.print()); // [30, 20, 10]console.log("Peek:", stack.peek()); // 30console.log("Pop:", stack.pop()); // 30console.log("After pop:", stack.print()); // [20, 10]console.log("Size:", stack.size()); // 2console.log("Is empty?", stack.isEmpty()); // false// Real-world uses of stacks:// 1. Undo/Redo functionality// 2. Browser back button// 3. Function call stack// 4. Expression evaluation// 5. Syntax parsing
Interview Classic: Valid Parentheses
The classic stack problem! Check if brackets are properly balanced. Opening brackets go on stack, closing brackets must match the top.
// ===== INTERVIEW CLASSIC: Valid Parentheses =====// Check if brackets are properly balanced and nested// This is a MUST-KNOW stack problem!function isValid(s: string): boolean {const stack: string[] = [];const pairs: Record<string, string> = {')': '(',']': '[','}': '{'};for (const char of s) {if (char === '(' || char === '[' || char === '{') {// Opening bracket: push to stackstack.push(char);} else {// Closing bracket: must match top of stackif (stack.length === 0 || stack.pop() !== pairs[char]) {return false;}}}// Stack must be empty (all brackets matched)return stack.length === 0;}// Test casesconsole.log("'()' =>", isValid("()")); // trueconsole.log("'()[]{}' =>", isValid("()[]{}")); // trueconsole.log("'(]' =>", isValid("(]")); // falseconsole.log("'([)]' =>", isValid("([)]")); // falseconsole.log("'{[]}' =>", isValid("{[]}")); // trueconsole.log("'(((' =>", isValid("(((")); // false (unclosed)console.log("')))' =>", isValid(")))")); // false (no opening)// Why this works:// - Opening brackets go on stack// - Closing brackets must match most recent opening (LIFO!)// - If we can't match or stack isn't empty, it's invalid
Interview Classic: Min Stack
Design a stack that supports getMin() in O(1). The trick is using an auxiliary stack to track minimums at each level!
// ===== INTERVIEW CLASSIC: Min Stack =====// Design a stack that supports push, pop, top, and getMin in O(1)// The trick: maintain a second stack to track minimums!class MinStack {private stack: number[] = [];private minStack: number[] = []; // Tracks minimum at each levelpush(val: number): void {this.stack.push(val);// Push to minStack if empty or val <= current minif (this.minStack.length === 0 || val <= this.getMin()) {this.minStack.push(val);}}pop(): void {const popped = this.stack.pop();// If popped value was the min, remove from minStack tooif (popped === this.getMin()) {this.minStack.pop();}}top(): number {return this.stack[this.stack.length - 1];}getMin(): number {return this.minStack[this.minStack.length - 1];}}// Democonst minStack = new MinStack();minStack.push(-2);minStack.push(0);minStack.push(-3);console.log("Min:", minStack.getMin()); // -3minStack.pop();console.log("Top:", minStack.top()); // 0console.log("Min:", minStack.getMin()); // -2minStack.push(-1);console.log("Min:", minStack.getMin()); // -2 (still!)// Why two stacks?// When we pop, we need to know what the min was BEFORE that element// The minStack keeps a history of minimums
5. Queues - FIFO Structure

Queues follow First In, First Out (FIFO). Like a line at a store - the first person in line is served first. Queues are essential for BFS traversal, task scheduling, and handling requests in order.
Queue Implementation
// ===== QUEUE: First In, First Out (FIFO) =====// Think of a line at a store - first person in line is first served// Operations: enqueue, dequeue, front - all O(1)!class Queue<T> {private items: T[] = [];private head: number = 0; // Index of front element// Add to back - O(1)enqueue(item: T): void {this.items.push(item);}// Remove from front - O(1) amortized// Note: Using head pointer instead of shift() for O(1)dequeue(): T | undefined {if (this.isEmpty()) return undefined;const item = this.items[this.head];this.head++;// Clean up when head gets too far aheadif (this.head > this.items.length / 2) {this.items = this.items.slice(this.head);this.head = 0;}return item;}// View front without removing - O(1)front(): T | undefined {return this.items[this.head];}// View back - O(1)back(): T | undefined {return this.items[this.items.length - 1];}isEmpty(): boolean {return this.head >= this.items.length;}size(): number {return this.items.length - this.head;}print(): string {return `Queue (front->back): [${this.items.slice(this.head).join(", ")}]`;}}// Democonst queue = new Queue<string>();console.log("=== Queue Operations ===");queue.enqueue("Alice");queue.enqueue("Bob");queue.enqueue("Charlie");console.log(queue.print()); // [Alice, Bob, Charlie]console.log("Front:", queue.front()); // Aliceconsole.log("Dequeue:", queue.dequeue()); // Aliceconsole.log("After dequeue:", queue.print()); // [Bob, Charlie]console.log("Size:", queue.size()); // 2// Real-world uses of queues:// 1. Print job scheduling// 2. Task scheduling (CPU)// 3. Breadth-first search// 4. Message queues// 5. Handling requests (web servers)
6. Hash Tables - O(1) Lookup

Hash tables are THE optimization tool for interviews. They provide O(1) average-case lookup, insert, and delete. When you see a brute-force O(n²) solution, ask yourself: "Can I use a hash table to make this O(n)?"
• Counting frequencies
• Finding duplicates
• Two Sum and similar "find complement" problems
• Caching/memoization
• Grouping items by key
Hash Table Implementation
// ===== HASH TABLE: O(1) Average Lookup! =====// The most important data structure for interview optimization// Maps keys to values using a hash functionclass HashTable<K, V> {private buckets: [K, V][][];private size: number;private count: number = 0;constructor(size = 53) { // Prime numbers reduce collisionsthis.buckets = new Array(size).fill(null).map(() => []);this.size = size;}// Hash function: converts key to array indexprivate hash(key: K): number {const str = String(key);let hash = 0;// Simple polynomial rolling hashfor (let i = 0; i < str.length; i++) {hash = (hash * 31 + str.charCodeAt(i)) % this.size;}return hash;}// Set key-value pair - O(1) averageset(key: K, value: V): void {const index = this.hash(key);const bucket = this.buckets[index];// Check if key exists (update)for (let i = 0; i < bucket.length; i++) {if (bucket[i][0] === key) {bucket[i][1] = value;return;}}// Add new key-value pairbucket.push([key, value]);this.count++;}// Get value by key - O(1) averageget(key: K): V | undefined {const index = this.hash(key);const bucket = this.buckets[index];for (const [k, v] of bucket) {if (k === key) return v;}return undefined;}// Check if key exists - O(1) averagehas(key: K): boolean {return this.get(key) !== undefined;}// Delete key - O(1) averagedelete(key: K): boolean {const index = this.hash(key);const bucket = this.buckets[index];for (let i = 0; i < bucket.length; i++) {if (bucket[i][0] === key) {bucket.splice(i, 1);this.count--;return true;}}return false;}getSize(): number {return this.count;}}// Democonst table = new HashTable<string, number>();table.set("apple", 5);table.set("banana", 3);table.set("orange", 7);table.set("apple", 10); // Update existingconsole.log("apple:", table.get("apple")); // 10console.log("banana:", table.get("banana")); // 3console.log("grape:", table.get("grape")); // undefinedconsole.log("has orange?", table.has("orange")); // trueconsole.log("size:", table.getSize()); // 3table.delete("banana");console.log("after delete banana:", table.get("banana")); // undefined
Interview Classic: Group Anagrams
Group words that are anagrams. Key insight: anagrams have the same sorted characters! Use the sorted string as a hash key.
// ===== INTERVIEW CLASSIC: Group Anagrams =====// Group words that are anagrams of each other// Key insight: anagrams have the same sorted characters!function groupAnagrams(strs: string[]): string[][] {const map = new Map<string, string[]>();for (const str of strs) {// Create a key by sorting characters// "eat" -> "aet", "tea" -> "aet", "ate" -> "aet"const key = str.split('').sort().join('');if (!map.has(key)) {map.set(key, []);}map.get(key)!.push(str);}return Array.from(map.values());}// Alternative: use character count as key (faster for long strings)function groupAnagramsCount(strs: string[]): string[][] {const map = new Map<string, string[]>();for (const str of strs) {// Count each characterconst count = new Array(26).fill(0);for (const char of str) {count[char.charCodeAt(0) - 'a'.charCodeAt(0)]++;}// Use count as key: "2#1#0#..." means 2 a's, 1 b, 0 c's...const key = count.join('#');if (!map.has(key)) {map.set(key, []);}map.get(key)!.push(str);}return Array.from(map.values());}// Testconst words = ["eat", "tea", "tan", "ate", "nat", "bat"];console.log("Input:", words);console.log("Grouped:", groupAnagrams(words));// [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]// Time: O(n * k log k) where n = number of strings, k = max string length// Space: O(n * k)
7. Trees & Binary Search Trees


Trees are hierarchical data structures. Binary Search Trees (BST) maintain a special property: left subtree < node < right subtree. This allows O(log n) search, insert, and delete on average.
• In-order: Left → Root → Right (gives sorted output for BST!)
• Pre-order: Root → Left → Right (copy tree, prefix expression)
• Post-order: Left → Right → Root (delete tree, postfix expression)
• Level-order: BFS, level by level
Binary Search Tree Implementation
// ===== BINARY SEARCH TREE (BST) =====// Left subtree < Node < Right subtree// Average O(log n) for search, insert, deleteclass TreeNode {value: number;left: TreeNode | null = null;right: TreeNode | null = null;constructor(value: number) {this.value = value;}}class BST {root: TreeNode | null = null;// Insert - O(log n) average, O(n) worstinsert(value: number): void {const node = new TreeNode(value);if (!this.root) {this.root = node;return;}let current = this.root;while (true) {if (value < current.value) {if (!current.left) {current.left = node;return;}current = current.left;} else {if (!current.right) {current.right = node;return;}current = current.right;}}}// Search - O(log n) averagesearch(value: number): boolean {let current = this.root;while (current) {if (value === current.value) return true;current = value < current.value ? current.left : current.right;}return false;}// In-order traversal: Left -> Root -> Right (sorted!)inOrder(node: TreeNode | null = this.root): number[] {if (!node) return [];return [...this.inOrder(node.left),node.value,...this.inOrder(node.right)];}// Pre-order: Root -> Left -> RightpreOrder(node: TreeNode | null = this.root): number[] {if (!node) return [];return [node.value,...this.preOrder(node.left),...this.preOrder(node.right)];}// Post-order: Left -> Right -> RootpostOrder(node: TreeNode | null = this.root): number[] {if (!node) return [];return [...this.postOrder(node.left),...this.postOrder(node.right),node.value];}// Find minimum (leftmost node)findMin(): number | null {if (!this.root) return null;let current = this.root;while (current.left) {current = current.left;}return current.value;}// Find maximum (rightmost node)findMax(): number | null {if (!this.root) return null;let current = this.root;while (current.right) {current = current.right;}return current.value;}}// Democonst bst = new BST();[8, 3, 10, 1, 6, 14, 4, 7, 13].forEach(v => bst.insert(v));console.log("In-order (sorted):", bst.inOrder());// [1, 3, 4, 6, 7, 8, 10, 13, 14]console.log("Pre-order:", bst.preOrder());// [8, 3, 1, 6, 4, 7, 10, 14, 13]console.log("Search 6:", bst.search(6)); // trueconsole.log("Search 5:", bst.search(5)); // falseconsole.log("Min:", bst.findMin()); // 1console.log("Max:", bst.findMax()); // 14
8. Heaps & Priority Queues

Heaps are complete binary trees where each parent is smaller (min-heap) or larger (max-heap) than its children. They provide O(1) access to the minimum/maximum and O(log n) insertion and deletion. Perfect for "find K largest/smallest" problems!
| Operation | Time |
|---|---|
| Get min/max | O(1) |
| Insert | O(log n) |
| Delete min/max | O(log n) |
| Build heap | O(n) |
• Find K largest/smallest elements
• Merge K sorted lists
• Median of a stream
• Dijkstra's shortest path
• Task scheduling by priority
9. Graphs & Traversals

Graphs model relationships between entities. They consist of vertices (nodes) connected by edges. Graphs can be directed/undirected, weighted/unweighted, cyclic/acyclic. Master BFS and DFS - they're the foundation for most graph problems!
BFS (Breadth-First): Level by level, uses Queue, finds shortest path
DFS (Depth-First): Go deep first, uses Stack/Recursion, detects cycles
Graph BFS & DFS Implementation
// ===== GRAPH TRAVERSAL: BFS & DFS =====// Two fundamental ways to explore a graphclass Graph {private adjacencyList = new Map<number, number[]>();addVertex(vertex: number): void {if (!this.adjacencyList.has(vertex)) {this.adjacencyList.set(vertex, []);}}addEdge(v1: number, v2: number): void {this.adjacencyList.get(v1)?.push(v2);this.adjacencyList.get(v2)?.push(v1); // Undirected}// BFS: Level by level (uses Queue)// Good for: shortest path, level-orderbfs(start: number): number[] {const visited = new Set<number>();const result: number[] = [];const queue: number[] = [start];visited.add(start);while (queue.length > 0) {const vertex = queue.shift()!;result.push(vertex);for (const neighbor of this.adjacencyList.get(vertex) || []) {if (!visited.has(neighbor)) {visited.add(neighbor);queue.push(neighbor);}}}return result;}// DFS: Go deep first (uses Stack/Recursion)// Good for: cycle detection, topological sort, path findingdfs(start: number): number[] {const visited = new Set<number>();const result: number[] = [];const traverse = (vertex: number) => {visited.add(vertex);result.push(vertex);for (const neighbor of this.adjacencyList.get(vertex) || []) {if (!visited.has(neighbor)) {traverse(neighbor);}}};traverse(start);return result;}// DFS Iterative (using explicit stack)dfsIterative(start: number): number[] {const visited = new Set<number>();const result: number[] = [];const stack: number[] = [start];while (stack.length > 0) {const vertex = stack.pop()!;if (!visited.has(vertex)) {visited.add(vertex);result.push(vertex);// Add neighbors to stack (reverse for consistent order)const neighbors = this.adjacencyList.get(vertex) || [];for (let i = neighbors.length - 1; i >= 0; i--) {if (!visited.has(neighbors[i])) {stack.push(neighbors[i]);}}}}return result;}}// Build graph:// 0// / \// 1 2// | |// 3 4// \ /// 5const graph = new Graph();[0, 1, 2, 3, 4, 5].forEach(v => graph.addVertex(v));graph.addEdge(0, 1);graph.addEdge(0, 2);graph.addEdge(1, 3);graph.addEdge(2, 4);graph.addEdge(3, 5);graph.addEdge(4, 5);console.log("BFS from 0:", graph.bfs(0));// Level by level: [0, 1, 2, 3, 4, 5]console.log("DFS from 0:", graph.dfs(0));// Go deep first: [0, 1, 3, 5, 4, 2]console.log("DFS Iterative:", graph.dfsIterative(0));
10. Sorting Algorithms


Sorting is fundamental to computer science. Understanding different sorting algorithms, their trade-offs, and when to use each is crucial for interviews. Quick Sort and Merge Sort are the most important to master.
| Algorithm | Best | Average | Worst | Space | Stable? |
|---|---|---|---|---|---|
| Quick Sort | O(n log n) | O(n log n) | O(n²) | O(log n) | No |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No |
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
Quick Sort
Quick Sort is often the fastest in practice due to cache efficiency and in-place sorting. It uses a pivot to partition the array: elements smaller than pivot go left, larger go right.
// ===== QUICK SORT: Divide and Conquer =====// Average O(n log n), Worst O(n²)// In-place, not stablefunction quickSort(arr: number[]): number[] {// Create a copy to avoid mutating originalconst result = [...arr];quickSortHelper(result, 0, result.length - 1);return result;}function quickSortHelper(arr: number[], low: number, high: number): void {if (low < high) {const pivotIndex = partition(arr, low, high);quickSortHelper(arr, low, pivotIndex - 1);quickSortHelper(arr, pivotIndex + 1, high);}}function partition(arr: number[], low: number, high: number): number {// Choose last element as pivotconst pivot = arr[high];let i = low - 1;for (let j = low; j < high; j++) {if (arr[j] < pivot) {i++;[arr[i], arr[j]] = [arr[j], arr[i]];}}// Place pivot in correct position[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];return i + 1;}// Simpler version (not in-place, easier to understand)function quickSortSimple(arr: number[]): number[] {if (arr.length <= 1) return arr;const pivot = arr[Math.floor(arr.length / 2)];const left = arr.filter(x => x < pivot);const middle = arr.filter(x => x === pivot);const right = arr.filter(x => x > pivot);return [...quickSortSimple(left), ...middle, ...quickSortSimple(right)];}const unsorted = [64, 34, 25, 12, 22, 11, 90, 42];console.log("Original:", unsorted);console.log("Quick Sort:", quickSort(unsorted));console.log("Simple version:", quickSortSimple(unsorted));// Quick Sort is often the fastest in practice because:// 1. In-place (low memory overhead)// 2. Cache-friendly (sequential access)// 3. Average case is common
11. Searching Algorithms

Binary Search is the most important searching algorithm. It finds elements in O(log n) time by repeatedly halving the search space. The key requirement is that the data must be sorted. Binary search patterns appear in many interview problems!
• Find exact element
• Find first occurrence (leftmost)
• Find last occurrence (rightmost)
• Find insertion point
• Search in rotated sorted array
12. Dynamic Programming

Dynamic Programming breaks complex problems into overlapping subproblems.The key insight is to store solutions to subproblems (memoization) instead of recomputing them. DP is challenging but mastering it will help you solve many interview problems!
1. Identify: Can the problem be broken into subproblems?
2. Define state: What information do we need to track?
3. Recurrence: How do subproblems relate to each other?
4. Base cases: What are the smallest subproblems?
5. Order: Bottom-up (tabulation) or top-down (memoization)?
Fibonacci: The Classic DP Example
Fibonacci perfectly illustrates the power of DP. Naive recursion is O(2^n), but with memoization it becomes O(n). This is the speedup DP provides!
// ===== DYNAMIC PROGRAMMING: Fibonacci =====// Classic example showing memoization vs tabulation// NAIVE RECURSIVE: O(2^n) - TERRIBLE!function fibNaive(n: number): number {if (n <= 1) return n;return fibNaive(n - 1) + fibNaive(n - 2);}// MEMOIZATION (Top-Down): O(n) - Much better!function fibMemo(n: number, memo: Map<number, number> = new Map()): number {if (n <= 1) return n;if (memo.has(n)) return memo.get(n)!;const result = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);memo.set(n, result);return result;}// TABULATION (Bottom-Up): O(n) - Even better for iteration!function fibTab(n: number): number {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];}// SPACE OPTIMIZED: O(1) space!function fibOptimized(n: number): number {if (n <= 1) return n;let prev2 = 0;let prev1 = 1;for (let i = 2; i <= n; i++) {const current = prev1 + prev2;prev2 = prev1;prev1 = current;}return prev1;}console.log("Fibonacci Examples:");console.log("fib(10) Memo:", fibMemo(10)); // 55console.log("fib(10) Tab:", fibTab(10)); // 55console.log("fib(10) Optimized:", fibOptimized(10)); // 55console.log("fib(40) Optimized:", fibOptimized(40)); // 102334155// Don't try fibNaive(40) - it will take forever!
Interview Classic: Coin Change
Given coins of different denominations, find the minimum number of coins needed to make a target amount. This is a classic bottom-up DP problem.
// ===== INTERVIEW CLASSIC: Coin Change =====// Given coins and amount, find minimum coins needed// Classic DP problem - bottom-up approachfunction coinChange(coins: number[], amount: number): number {// dp[i] = minimum coins to make amount iconst dp = new Array(amount + 1).fill(Infinity);dp[0] = 0; // 0 coins needed for amount 0for (let i = 1; i <= amount; i++) {for (const coin of coins) {if (coin <= i && dp[i - coin] !== Infinity) {dp[i] = Math.min(dp[i], dp[i - coin] + 1);}}}return dp[amount] === Infinity ? -1 : dp[amount];}// Test casesconsole.log("Coins [1,2,5], Amount 11:", coinChange([1, 2, 5], 11));// Output: 3 (5 + 5 + 1)console.log("Coins [2], Amount 3:", coinChange([2], 3));// Output: -1 (impossible)console.log("Coins [1], Amount 0:", coinChange([1], 0));// Output: 0console.log("Coins [1,2,5], Amount 100:", coinChange([1, 2, 5], 100));// Output: 20 (20 × 5)// Visual for amount=11, coins=[1,2,5]:// dp[0]=0, dp[1]=1, dp[2]=1, dp[3]=2, dp[4]=2// dp[5]=1, dp[6]=2, dp[7]=2, dp[8]=3, dp[9]=3// dp[10]=2, dp[11]=3
13. Interview Challenges
Test your skills with these interview-style challenges! Try to solve each problem before looking at the hints. The code editor supports maximize and font size adjustment - use it to code your solutions.
1. Understand the problem - ask clarifying questions
2. Work through examples by hand
3. Identify the pattern (two pointers, sliding window, DP, etc.)
4. Start with brute force, then optimize
5. Test with edge cases
Challenge 1: Product of Array Except Self
Given an array, return an array where each element is the product of all other elements. Constraint: O(n) time, no division!
// ===== CHALLENGE 1: Product of Array Except Self =====// Return array where each element is product of all OTHER elements// Constraint: O(n) time, NO division allowed!function productExceptSelf(nums: number[]): number[] {const n = nums.length;const result = new Array(n).fill(1);// TODO: Your solution here// Hint: Use prefix and suffix productsreturn result;}// Test casesconsole.log(productExceptSelf([1, 2, 3, 4]));// Expected: [24, 12, 8, 6]// Explanation: 24=2*3*4, 12=1*3*4, 8=1*2*4, 6=1*2*3console.log(productExceptSelf([-1, 1, 0, -3, 3]));// Expected: [0, 0, 9, 0, 0]
Challenge 2: Longest Substring Without Repeating
Find the length of the longest substring without repeating characters. Hint: Sliding window pattern!
// ===== CHALLENGE 2: Longest Substring Without Repeating =====// Find length of longest substring without repeating characters// Classic sliding window problem!function lengthOfLongestSubstring(s: string): number {// TODO: Your solution here// Hint: Use a Set to track characters in current window// Use two pointers (left and right)return 0;}// Test casesconsole.log(lengthOfLongestSubstring("abcabcbb"));// Expected: 3 ("abc")console.log(lengthOfLongestSubstring("bbbbb"));// Expected: 1 ("b")console.log(lengthOfLongestSubstring("pwwkew"));// Expected: 3 ("wke")console.log(lengthOfLongestSubstring(""));// Expected: 0
Challenge 3: Merge Intervals
Given an array of intervals, merge all overlapping intervals. Hint: Sort first!
// ===== CHALLENGE 3: Merge Intervals =====// Given array of intervals, merge overlapping ones// Key: Sort by start time first!function merge(intervals: number[][]): number[][] {// TODO: Your solution here// 1. Sort intervals by start time// 2. Iterate and merge overlapping intervalsreturn [];}// Test casesconsole.log(merge([[1,3],[2,6],[8,10],[15,18]]));// Expected: [[1,6],[8,10],[15,18]]// Explanation: [1,3] and [2,6] overlap -> [1,6]console.log(merge([[1,4],[4,5]]));// Expected: [[1,5]]// Explanation: [1,4] and [4,5] touch -> mergeconsole.log(merge([[1,4],[0,4]]));// Expected: [[0,4]]
DSA mastery comes from practice. Work through these challenges, understand the patterns, and apply them to new problems. The more you practice, the faster you'll recognize which technique to use!