🧮

Top 72 Data Structures & Algorithms Interview Questions & Answers (2026)

72 Questions 33 Beginner 24 Intermediate 15 Advanced

About Data Structures & Algorithms

Top 100 Data Structures and Algorithms interview questions covering arrays, linked lists, trees, graphs, sorting, searching, dynamic programming, and complexity analysis. Companies hiring for Data Structures & Algorithms roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.

What to Expect in a Data Structures & Algorithms Interview

Expect a mix of conceptual and practical Data Structures & Algorithms questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.

How to Use This Guide

Work through the Data Structures & Algorithms questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.

Curated by Tech Baithak Editorial Team  ·  Last updated: May 2026

Beginner 33 questions

Core concepts every Data Structures & Algorithms developer must know.

01

What is a data structure?

A data structure is a way of organizing, storing, and managing data in a computer so that it can be accessed and modified efficiently. Different data structures are suited to different kinds of tasks — the choice of data structure directly affects the performance of algorithms that use it. Categories: Linear data structures: elements are arranged sequentially — Array, Linked List, Stack, Queue; Non-linear data structures: elements are not sequential — Trees, Graphs; Hash-based: Hash Table / Hash Map; Heap-based: Priority Queue / Heap. Primitive vs abstract: Primitive data structures (int, float, char) are provided by programming languages. Abstract Data Types (ADTs) define the operations (interface) without specifying implementation — Stack (push, pop, peek), Queue (enqueue, dequeue), List (insert, delete, find). Choosing the right data structure is one of the most impactful decisions in software engineering — a hash map gives O(1) lookup vs O(n) for an array, which can be the difference between a program running in milliseconds or minutes on large data.

Open this question on its own page
02

What is an array?

An array is a linear data structure that stores elements of the same type in contiguous memory locations, each accessible by an index (0-based in most languages). Characteristics: Fixed size (static arrays) — size declared at creation; Random access — any element accessible in O(1) by index since the address = base_address + (index × element_size); Cache-friendly — contiguous memory means CPU cache lines are used efficiently. Operations: Access by index: O(1); Search (unsorted): O(n); Search (sorted, binary search): O(log n); Insert at end: O(1) amortized (dynamic arrays); Insert at middle: O(n) (must shift elements); Delete at middle: O(n) (must shift); Append: O(1). Dynamic arrays (ArrayList in Java, list in Python, vector in C++): automatically resize when full — typically double the capacity (amortized O(1) append). Pros: fast access, cache-friendly, simple. Cons: fixed size (static), expensive insertion/deletion in middle, wasted space if many deletions. Arrays are the foundation for most other data structures and are ubiquitous in algorithms.

Open this question on its own page
03

What is a linked list?

A linked list is a linear data structure where elements (nodes) are stored in non-contiguous memory. Each node contains data and a pointer (reference) to the next node. Types: Singly linked list: each node points to the next; traversal is one-directional; Doubly linked list: each node has pointers to both next and previous nodes; enables bidirectional traversal, O(1) deletion given a node reference; Circular linked list: the last node points back to the first. Operations: Access by index: O(n) (must traverse from head); Search: O(n); Insert at head: O(1); Insert at tail: O(1) with tail pointer; Insert at middle: O(n) to find position, then O(1) for pointer manipulation; Delete: O(n) to find, O(1) to delete (given the node or previous node). Pros: dynamic size, O(1) insert/delete at head/tail, efficient for frequent insertions/deletions. Cons: no random access (O(n) to reach index n), extra memory per node for pointers, poor cache performance (non-contiguous). Real use: LRU cache (doubly linked list + hash map), undo/redo in editors, browser history, adjacency lists in graphs.

Open this question on its own page
04

What is a stack?

A stack is a linear data structure following the LIFO (Last In, First Out) principle — the last element inserted is the first one removed, like a stack of plates. Operations: push(item) — add to top: O(1); pop() — remove and return top element: O(1); peek()/top() — view top without removing: O(1); isEmpty() — check if empty: O(1); size(): O(1). Implementation: using an array (with a top pointer) or a linked list (push/pop at head). Applications: (1) Function call stack — programming languages use a call stack to track function invocations, return addresses, and local variables; (2) Undo/redo operations in editors; (3) Browser back button (history stack); (4) Expression evaluation — postfix/infix evaluation; (5) Balanced parentheses validation; (6) DFS traversal (iterative); (7) Backtracking algorithms. Stack overflow: when a recursive function calls itself too many times, exhausting the call stack (recursion depth limit). Use stacks when you need to reverse things, match brackets, or process things in LIFO order.

Open this question on its own page
05

What is a queue?

A queue is a linear data structure following the FIFO (First In, First Out) principle — the first element inserted is the first one removed, like a line of people at a counter. Operations: enqueue(item) — add to rear: O(1); dequeue() — remove from front: O(1); peek()/front() — view front without removing: O(1); isEmpty(): O(1). Implementation: array with two pointers (front/rear) — circular buffer to avoid shifting; linked list (enqueue at tail, dequeue at head). Variants: Circular queue — wraps around using modulo; Double-ended queue (Deque) — insert/remove from both ends; Priority queue — elements dequeued by priority (not insertion order), implemented with a heap; Blocking queue — used in concurrent programming, blocks when empty or full. Applications: (1) BFS traversal of graphs and trees; (2) Process scheduling in OS (CPU queue); (3) Print spooler; (4) Message queues (RabbitMQ, Kafka); (5) Level-order tree traversal; (6) Cache eviction (FIFO). Use queues when order of processing must match order of arrival.

Open this question on its own page
06

What is Big O notation?

Big O notation describes the upper bound of an algorithm's time or space complexity as the input size n grows toward infinity — it characterizes the worst-case growth rate, ignoring constants and lower-order terms. Common complexities (best to worst): O(1) — constant time: doesn't grow with input size. Hash map lookup, array index access; O(log n) — logarithmic: input is halved each step. Binary search, balanced BST operations; O(n) — linear: proportional to input. Linear search, traversing a list; O(n log n) — linearithmic: efficient sorting (merge sort, heapsort, quicksort average); O(n²) — quadratic: nested loops. Bubble sort, selection sort, naive matrix multiplication; O(2ⁿ) — exponential: brute-force combinatorial problems. Fibonacci naive recursion; O(n!) — factorial: permutations. Rules: drop constants (O(2n) → O(n)), drop lower-order terms (O(n² + n) → O(n²)), consider worst case. Space complexity: same notation for memory usage. Common interview question: "What is the time/space complexity of your solution?" Always analyze before and after optimization.

Open this question on its own page
07

What is binary search?

Binary search is an efficient search algorithm for finding a target value in a sorted array in O(log n) time by repeatedly halving the search space. Algorithm: compare target to the middle element; if equal, found; if target < middle, search left half; if target > middle, search right half; repeat until found or search space is empty. Implementation: int left = 0, right = n-1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) return mid; if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1;. Note: use left + (right - left) / 2 instead of (left + right) / 2 to avoid integer overflow. Time: O(log n). Space: O(1) iterative, O(log n) recursive (call stack). Precondition: array MUST be sorted. Variants: find first/last occurrence of a value (handle duplicates), find insertion position (lower_bound/upper_bound), search in rotated sorted array, binary search on the answer (find minimum speed, capacity, etc. using binary search on the answer space). Binary search reduces 1 billion elements to 30 comparisons (log₂(10⁹) ≈ 30).

Open this question on its own page
08

What is bubble sort?

Bubble sort is a simple sorting algorithm that repeatedly passes through the list, compares adjacent elements, and swaps them if they're in the wrong order. Largest elements "bubble up" to the end with each pass. Algorithm: for each pass (n-1 passes total), compare each adjacent pair; if arr[j] > arr[j+1], swap them. After each pass, the largest unsorted element is in its correct position. Optimized: if no swaps occur in a pass, the array is sorted — exit early. Time: O(n²) worst/average, O(n) best (already sorted with early exit). Space: O(1) — in-place. Stable: yes (equal elements maintain relative order). Practical use: almost none — bubble sort is pedagogically simple but practically the worst sorting algorithm. Never use in production. Only advantage over selection sort: it's stable and detects sorted input in O(n). Comparison: selection sort also O(n²) but makes fewer swaps; insertion sort O(n²) worst but O(n) for nearly-sorted; merge sort O(n log n) but O(n) space; quicksort O(n log n) average, in-place. Know bubble sort for interviews to demonstrate understanding, but explain why you'd never use it.

Open this question on its own page
09

What is selection sort?

Selection sort divides the array into sorted (left) and unsorted (right) portions. In each pass, it finds the minimum element in the unsorted portion and swaps it with the first unsorted element, growing the sorted portion by one. Algorithm: for i from 0 to n-2, find index of minimum in arr[i..n-1], swap arr[i] with arr[minIndex]. Time: O(n²) all cases (always scans remaining elements even if sorted). Space: O(1) in-place. Stable: NO (swapping can change relative order of equal elements — can be made stable with insertion instead of swap). Advantage over bubble sort: fewer swaps — exactly n-1 swaps always. Useful when write operations are expensive (flash memory). Disadvantage: never terminates early — always O(n²) even on sorted input. Comparison: insertion sort is generally better than selection sort for partially sorted data (O(n) best case vs O(n²)). Both are O(n²) average/worst. Selection sort is useful as an educational example of in-place sorting and demonstrates finding minimums, but is not used in practice for general sorting.

Open this question on its own page
10

What is insertion sort?

Insertion sort builds the sorted array one element at a time by inserting each new element into its correct position among the already-sorted elements — like sorting playing cards in your hand. Algorithm: for i from 1 to n-1, take arr[i] as the key, compare with elements to its left, shift larger elements right, insert key in its correct position. Time: O(n²) worst (reverse sorted), O(n) best (already sorted — only n-1 comparisons, 0 swaps). Space: O(1) in-place. Stable: yes. Advantages: efficient for small arrays (typically used for n ≤ 10-20); efficient for nearly-sorted data; simple implementation; online algorithm (can sort as elements arrive); no extra memory. Used as the base case in hybrid sorts: Timsort (Python, Java) uses insertion sort for small subarrays (< ~64 elements) then merge sort; Introsort (C++ std::sort) uses insertion sort for small subarrays. In competitive programming: insertion sort is preferred over other O(n²) sorts for small or nearly-sorted inputs. The O(n) best case makes it excel at sorting nearly-sorted data, which is common in practice.

Open this question on its own page
11

What is merge sort?

Merge sort is a divide-and-conquer sorting algorithm that recursively divides the array into halves, sorts each half, then merges the sorted halves. It guarantees O(n log n) performance in all cases. Algorithm: (1) If the array has 0 or 1 elements, return (base case); (2) Divide: split array in half; (3) Conquer: recursively sort each half; (4) Combine: merge the two sorted halves into one sorted array. Merge operation: use two pointers on each half, compare, and copy the smaller element to the result; O(n). Time: O(n log n) — n levels of recursion, O(n) work per level. Space: O(n) auxiliary space for the merge step (not in-place). Stable: yes. Best for: linked lists (O(1) extra space possible), external sorting (sorting data too large for RAM — merge sorted chunks from disk), when O(n log n) worst case is required (unlike quicksort). Java's Arrays.sort() for objects uses TimSort (merge sort + insertion sort). Disadvantage: requires O(n) extra space; slightly slower than quicksort in practice due to extra memory operations. Merge sort demonstrates divide-and-conquer, recurrence relations T(n) = 2T(n/2) + O(n) → O(n log n), and the master theorem.

Open this question on its own page
12

What is quicksort?

Quicksort is a highly efficient divide-and-conquer sorting algorithm that picks a pivot element, partitions the array into elements less than, equal to, and greater than the pivot, then recursively sorts the sub-arrays. Partition step: rearrange so all elements < pivot come before it, all > pivot come after. Algorithm: (1) Pick a pivot (first, last, middle, or random); (2) Partition: put elements < pivot left, > pivot right; (3) Recursively sort left and right partitions. Time: O(n log n) average and best case, O(n²) worst case (when pivot is always the smallest/largest — e.g., sorted array with first-element pivot). Space: O(log n) average for call stack, O(n) worst. Stable: NO (standard implementation). Pivot selection: random pivot or median-of-three avoids O(n²) in practice. Practical performance: quicksort is generally fastest for in-memory sorting — excellent cache behavior, in-place, small constant factors. C++ std::sort, Python's Timsort (for primitives), Java's primitive arrays use quicksort-based algorithms. Three-way partitioning (Dutch National Flag) handles many equal elements efficiently: O(n) for all-same-element input. Introsort: quicksort that switches to heapsort when recursion depth exceeds O(log n) — avoids O(n²) worst case.

Open this question on its own page
13

What is a hash table / hash map?

A hash table (hash map) is a data structure that provides O(1) average time for insert, delete, and lookup by using a hash function to map keys to array indices. The hash function converts a key to an integer, then maps it to a bucket/slot (index = hash(key) % capacity). Collisions occur when two keys hash to the same index. Collision resolution: (1) Chaining — each bucket is a linked list; O(1) average, O(n) worst (all keys in one chain); (2) Open addressing — find the next available slot: linear probing, quadratic probing, double hashing. Load factor: n/m (n = elements, m = buckets). When load factor exceeds a threshold (~0.75), resize (rehash) — O(n) amortized. Operations: Insert O(1) avg, O(n) worst; Lookup O(1) avg; Delete O(1) avg. Hash functions: should distribute keys uniformly and be fast to compute. Examples: DJB2, MurmurHash, SipHash. Java HashMap, Python dict, JavaScript Map are all hash maps. Use cases: counting frequencies, detecting duplicates, caching, implementing sets, graph adjacency lists, indexing. Hash maps are arguably the most useful and widely-used data structure in software engineering.

Open this question on its own page
14

What is a binary tree?

A binary tree is a hierarchical data structure where each node has at most two children, referred to as the left child and the right child. Terminology: Root — top node with no parent; Leaf — node with no children; Internal node — node with at least one child; Height — longest path from root to leaf; Depth — length of path from root to a node; Subtree — a node and all its descendants. Types: Full binary tree — every node has 0 or 2 children; Complete binary tree — all levels filled except possibly the last, filled left to right; Perfect binary tree — all internal nodes have 2 children, all leaves at same depth; Balanced binary tree — height is O(log n); Degenerate/skewed tree — every node has only one child (like a linked list, O(n) height). Properties of a perfect binary tree with height h: nodes = 2^(h+1) - 1, leaves = 2^h. Binary trees are the basis for BSTs, heaps, tries, segment trees, and expression trees.

Open this question on its own page
15

What is a binary search tree (BST)?

A Binary Search Tree (BST) is a binary tree with the BST property: for every node, all values in the left subtree are less than the node's value, and all values in the right subtree are greater. This property enables efficient search. Operations: Search: compare target with current node, go left (smaller) or right (larger), O(h) where h = height; Insert: search for the position, insert as a leaf, O(h); Delete: three cases — leaf (just remove), one child (replace with child), two children (replace with in-order successor/predecessor), O(h); In-order traversal: visits nodes in sorted order (left, root, right). Average case h = O(log n) for random data. Worst case h = O(n) for sorted input (degenerates to linked list). Balanced BSTs: AVL trees and Red-Black trees maintain O(log n) height through rotations. Java TreeMap, C++ std::map use Red-Black trees. Time: O(log n) average, O(n) worst for search/insert/delete. In-order traversal produces sorted output in O(n). BSTs are used in databases (indexed columns), file systems, and anywhere sorted order + fast search is needed.

Open this question on its own page
16

What are tree traversal methods?

Tree traversal visits every node exactly once. Four standard traversals: (1) In-order (Left, Root, Right): visits left subtree, then root, then right subtree. For BSTs, produces sorted output. Used for: sorted output, validating BST; (2) Pre-order (Root, Left, Right): visits root first, then left, then right. Used for: copying/cloning a tree, serializing a tree (can reconstruct with pre-order + in-order), expression trees (prefix notation); (3) Post-order (Left, Right, Root): visits root last. Used for: deleting a tree (delete children before parent), evaluating expression trees, computing directory sizes; (4) Level-order (BFS): visits level by level from root to leaves using a queue. Used for: shortest path in unweighted trees, printing by level, building a balanced BST. Iterative traversal: use a stack for DFS traversals (avoids recursion stack overflow). Level-order uses a queue. Time: O(n) for all traversals (every node visited once). Space: O(h) for recursive DFS (call stack height), O(w) for BFS (w = max width of tree). For a balanced tree, h = O(log n); for a skewed tree, h = O(n).

Open this question on its own page
17

What is a heap?

A heap is a complete binary tree satisfying the heap property. Two types: (1) Max-heap: every parent is ≥ its children — root is the maximum element; (2) Min-heap: every parent is ≤ its children — root is the minimum element. Implementation: typically stored in an array (no explicit tree structure needed for a complete binary tree). For node at index i: left child = 2i+1, right child = 2i+2, parent = (i-1)/2. Operations: Insert: add at end, then sift up (bubble up) to restore heap property: O(log n); Extract-max/min: remove root, move last element to root, sift down: O(log n); Peek: O(1) — root is always max/min; Build heap from array: O(n) — use sift-down starting from last non-leaf. Applications: Priority queue ADT; Heap sort — build max-heap, repeatedly extract max: O(n log n), O(1) space; Top K elements problem; Merge K sorted lists; Dijkstra's/Prim's algorithms. Python: heapq (min-heap). Java: PriorityQueue. C++: priority_queue.

Open this question on its own page
18

What is a graph?

A graph is a non-linear data structure consisting of vertices (nodes) and edges (connections between nodes). Formally G = (V, E). Types: Undirected — edges have no direction (Facebook friends); Directed (digraph) — edges have direction (Twitter follows, website links); Weighted — edges have a numerical weight/cost (road distances, flight costs); Unweighted — edges are equal; Cyclic — contains at least one cycle; Acyclic — no cycles; DAG (Directed Acyclic Graph) — directed, no cycles (dependency graphs, task scheduling). Representations: (1) Adjacency Matrix: V×V matrix, adj[i][j]=1 if edge exists. Space: O(V²). Edge lookup O(1), but wastes space for sparse graphs; (2) Adjacency List: array of lists, each containing neighbors. Space: O(V+E). Most common for sparse graphs; (3) Edge List: list of all edges as pairs. Useful for algorithms like Kruskal's. Real-world graphs: social networks, internet routing, maps, dependencies, recommendation systems.

Open this question on its own page
19

What is Breadth-First Search (BFS)?

BFS explores a graph level by level — visits all neighbors of the current node before moving to their neighbors. Uses a queue. Algorithm: (1) Start: enqueue the source node, mark it visited; (2) While queue not empty: dequeue a node, process it, enqueue all unvisited neighbors and mark them visited. Time: O(V+E) for adjacency list. Space: O(V) for the queue. Properties: Finds shortest path (in terms of number of edges) from source to all reachable nodes in an unweighted graph; processes nodes in order of distance from source. Applications: (1) Shortest path in unweighted graphs (word ladder, knight moves, maze); (2) Level-order tree traversal; (3) Finding connected components; (4) Web crawling; (5) Social network distance (degrees of separation); (6) Bipartite graph checking; (7) Garbage collection (find all reachable objects). BFS vs DFS: BFS uses more memory (stores entire frontier) but guarantees shortest path. DFS uses less memory O(h) but doesn't guarantee shortest path. Use BFS when you need shortest paths or layer-by-layer exploration.

Open this question on its own page
20

What is Depth-First Search (DFS)?

DFS explores a graph by going as deep as possible along a branch before backtracking. Uses a stack (or recursion). Algorithm (recursive): mark current node visited, process it, for each unvisited neighbor, recursively DFS. Iterative: use an explicit stack — push source, while stack not empty: pop, if unvisited mark visited and push all unvisited neighbors. Time: O(V+E). Space: O(V) for visited set and stack. Properties: explores one complete path before backtracking; order depends on neighbor order; not guaranteed shortest path. Applications: (1) Cycle detection in directed/undirected graphs; (2) Topological sort; (3) Connected components and SCCs (Tarjan's, Kosaraju's); (4) Maze solving; (5) Pathfinding (doesn't guarantee shortest); (6) Tree traversals (in/pre/post-order are all DFS); (7) Backtracking problems (N-Queens, Sudoku); (8) Finding bridges and articulation points; (9) Detecting all paths. DFS memory advantage: only needs O(h) space (recursion depth) vs BFS's O(w) (width). Choose DFS for exhaustive search, cycle detection, and connectivity.

Open this question on its own page
21

What is recursion?

Recursion is a programming technique where a function calls itself with a smaller or simpler version of the problem until it reaches a base case (termination condition). Every recursive function needs: (1) Base case: condition that stops recursion (no more self-calls); (2) Recursive case: function calls itself with a modified argument that moves toward the base case. Example — factorial: f(n) = 1 if n==0; n * f(n-1) otherwise. The call stack stores each recursive call's state. Space: O(n) stack depth for linear recursion. Tail recursion: the recursive call is the last operation — compilers can optimize to use O(1) space (tail call optimization). Problems well-suited to recursion: tree traversals, divide-and-conquer (merge sort, quicksort), dynamic programming, backtracking, fractals. Common pitfalls: (1) No base case → infinite recursion → stack overflow; (2) Base case never reached; (3) Deep recursion → stack overflow (use iteration or increase stack size). Every recursive solution has an iterative equivalent (using explicit stack), but recursion is often more elegant and readable. Think recursively: define the solution in terms of smaller subproblems.

Open this question on its own page
22

What is dynamic programming?

Dynamic programming (DP) is an optimization technique for problems with overlapping subproblems and optimal substructure. It solves each subproblem once and stores the result (memoization or tabulation), avoiding redundant recomputation. Two approaches: (1) Top-down (Memoization): recursion + caching. Solve the problem recursively, but store computed results in a hash map or array. First call computes; subsequent calls return cached result. More intuitive, may have stack overhead; (2) Bottom-up (Tabulation): iterative, fill a table from base cases upward. No recursion overhead, typically more space-efficient. Steps to solve DP: (1) Identify if DP applies (overlapping subproblems, optimal substructure); (2) Define state clearly (what does dp[i] represent?); (3) Write the recurrence relation (dp[i] in terms of previous states); (4) Identify base cases; (5) Determine computation order. Classic problems: Fibonacci, 0/1 Knapsack, Longest Common Subsequence, Longest Increasing Subsequence, Coin Change, Edit Distance, Matrix Chain Multiplication, Rod Cutting. DP transforms exponential brute force into polynomial time.

Open this question on its own page
23

What is a trie (prefix tree)?

A trie (prefix tree or digital tree) is a tree-shaped data structure for storing strings, where each node represents a character and paths from root to nodes represent prefixes. Nodes share common prefixes — "car" and "cat" share the "ca" prefix. Operations: Insert: O(m) where m = string length — walk/create nodes for each character; Search: O(m) — traverse characters from root; Prefix search (startsWith): O(p) where p = prefix length; Delete: O(m). Space: O(ALPHABET_SIZE × n × m) worst case. Implementation: each node has a children array/map (26 children for lowercase English, or hashmap for Unicode) and a boolean isEndOfWord. Applications: (1) Autocomplete / typeahead — find all words with given prefix; (2) Spell checker; (3) IP routing (longest prefix matching); (4) Word games (Boggle, Scrabble); (5) Phone contacts search; (6) Dictionary implementation. Advantages over hash map for strings: supports prefix queries; alphabetically sorted iteration; no hash collisions. Disadvantage: higher memory usage (many nodes). Compressed trie (Patricia trie, Radix tree) reduces nodes by compressing single-child chains.

Open this question on its own page
24

What is a set and how is it different from a list?

A set is an unordered collection of unique elements — it stores no duplicates. A list (array) is an ordered collection that can contain duplicates. Key differences: Uniqueness: sets enforce uniqueness; lists allow duplicates; Order: lists maintain insertion order (indices); sets typically have no defined order (hash sets) though some maintain insertion order (LinkedHashSet) or sorted order (TreeSet); Lookup: checking if an element exists in a hash set is O(1) avg; in a list O(n); Access by index: lists support O(1) index access; sets don't (no index concept). Implementations: HashSet/unordered_set — backed by hash table, O(1) avg operations; TreeSet/set — backed by BST (Red-Black tree), O(log n) operations, sorted order; LinkedHashSet — hash table + linked list, O(1) operations, insertion order. Operations: add(e), remove(e), contains(e), size(), iterate. Set operations: union, intersection, difference. Applications: (1) Finding unique elements; (2) Detecting duplicates; (3) Membership testing; (4) Graph visited nodes; (5) Set operations on data. Example: given an array, find all duplicate elements → add each to a set, check if add returns false (already present).

Open this question on its own page
25

What is the two-pointer technique?

The two-pointer technique uses two pointers (indices) that move through an array or string, often reducing O(n²) brute force to O(n). Patterns: (1) Opposite ends (converging): one pointer starts at left, one at right, move toward each other based on a condition. Examples: Two Sum in sorted array (if sum too large, move right pointer left; too small, move left pointer right), Valid Palindrome, Container With Most Water, 3Sum; (2) Same direction (fast/slow or window): both start at left, move at different speeds. Examples: Remove Duplicates from Sorted Array (slow pointer writes, fast reads), Linked List cycle detection (Floyd's), finding middle of linked list; (3) Sliding window: both pointers define a window that expands/contracts — Longest Substring Without Repeating Characters, Minimum Window Substring. Prerequisites: often requires sorted array or specific constraints. Examples in detail: Two Sum (sorted): while(l<r){sum=arr[l]+arr[r]; if(sum==target)return[l,r]; sum<target?l++:r--;}. Time: O(n). Two pointers is a fundamental pattern for array/string problems — recognizing when it applies is key to efficient solutions.

Open this question on its own page
26

What is the sliding window technique?

The sliding window technique maintains a "window" (subarray or substring) between two pointers that slides through the data, avoiding repeated computation by updating the window's result incrementally as it moves. Types: (1) Fixed-size window: window of size k slides one step at a time. Maximum sum of k consecutive elements: compute first window sum, then subtract leftmost and add new rightmost element — O(n) instead of O(n×k); (2) Variable-size window (expand/contract): right pointer expands, left pointer contracts when a condition is violated. Longest Substring Without Repeating Characters: expand right, if duplicate found, contract from left until no duplicate. Template: left=0; for(right=0; right<n; right++){ window.add(arr[right]); while(window is invalid){ window.remove(arr[left]); left++; } update result; }. Key insight: use a hash map to track window contents for O(1) add/remove. Time: O(n) — each element enters and exits the window at most once. Applications: Maximum of every subarray (use deque), Minimum Window Substring, Permutation in String, Fruit Into Baskets, Count of subarrays with product less than K. Sliding window is essential for substring/subarray optimization problems.

Open this question on its own page
27

What is a priority queue?

A priority queue is an abstract data type where each element has a priority, and elements are dequeued in priority order (not FIFO). The element with the highest (or lowest) priority is always dequeued first. Implementation: almost always backed by a binary heap. Operations: insert(element, priority): O(log n); extractMax/extractMin: O(log n); peek/top: O(1); decreaseKey/increaseKey: O(log n) — used in graph algorithms; buildHeap: O(n). Min-priority queue returns the element with the smallest priority (key) first. Max-priority queue returns the largest. Language implementations: Java: PriorityQueue (min-heap by default); Python: heapq (min-heap); C++: priority_queue (max-heap by default). Applications: (1) Dijkstra's shortest path — always process the unvisited node with smallest tentative distance; (2) Prim's MST; (3) Huffman encoding; (4) Task scheduling (process highest-priority jobs first); (5) Top K elements — maintain a heap of size K; (6) K-way merge of sorted lists; (7) A* pathfinding.

Open this question on its own page
28

What is the difference between a stack and a queue?

Both are linear, abstract data types, but they differ in how elements are removed: Stack (LIFO — Last In, First Out): elements are added to and removed from the same end (top). Push adds to top, pop removes from top. Like a stack of books — you can only take from the top. Use when: you need to reverse a sequence, match brackets, track function calls, handle backtracking. Queue (FIFO — First In, First Out): elements are added to the back (rear) and removed from the front. Enqueue adds to rear, dequeue removes from front. Like a queue of people — first in, first served. Use when: you need to process things in order of arrival, BFS traversal, scheduling. Key comparison: Stack = most recent first; Queue = oldest first. Both support O(1) push/enqueue and pop/dequeue. Can implement each with the other: (1) Queue using two stacks: enqueue pushes to stack1; dequeue: if stack2 empty, pop all from stack1 to stack2, then pop from stack2. Amortized O(1) per operation; (2) Stack using two queues: push to empty queue, then dequeue all from other to it. O(n) push, O(1) pop. Common interview question: implement one using the other.

Open this question on its own page
29

What is a circular linked list?

A circular linked list is a linked list where the last node points back to the first node (head), forming a circle. Types: Singly circular: last node's next pointer → head; Doubly circular: last node's next → head, head's prev → last node. Properties: (1) No null pointer at the end; (2) Can start traversal from any node; (3) Useful for cyclic/repeated processing; (4) Requires careful loop termination (stop when you reach the start again). Operations: traverse with do-while loop; insert/delete require updating circular pointers; detecting: check if node.next == head. Applications: (1) Round-robin scheduling — OS cycles through processes; (2) Multiplayer board games — cycle through players; (3) Media players — playlist looping; (4) Circular buffer/ring buffer — fixed-size buffer that wraps around; (5) Conway's Game of Life; (6) Josephus problem. Detecting a cycle in a general linked list (not necessarily circular from the start) uses Floyd's cycle detection algorithm (fast/slow pointers). Circular linked list differs from a linked list with a cycle — in circular, the cycle is intentional and known (last node → head).

Open this question on its own page
30

What is Floyd's cycle detection algorithm?

Floyd's cycle detection algorithm (Floyd's tortoise and hare) detects cycles in a linked list using two pointers moving at different speeds. The slow pointer advances one step at a time; the fast pointer advances two steps. If there is a cycle, the fast pointer eventually laps the slow pointer — they meet inside the cycle. If there is no cycle, the fast pointer reaches null. Cycle detection: slow = head; fast = head; while(fast != null && fast.next != null){ slow = slow.next; fast = fast.next.next; if(slow == fast) return true; } return false;. Finding cycle start: once slow and fast meet, reset slow to head; advance both one step at a time — they meet at the cycle start. Finding cycle length: after detection, keep fast still, advance slow until it reaches fast again, counting steps. Time: O(n). Space: O(1). Applications: (1) Linked list cycle detection (LeetCode 141/142); (2) Finding duplicate in array of n+1 integers using array as implicit linked list; (3) Rho algorithm for integer factorization; (4) Detecting periodic sequences in hashing. Key insight: in a cycle of length L, the two pointers must meet within L steps of entering the cycle.

Open this question on its own page
31

What is a deque (double-ended queue)?

A deque (double-ended queue, pronounced "deck") is a linear data structure that supports insertion and deletion from both ends in O(1). It combines the functionality of both a stack and a queue. Operations: addFront/pushFront: O(1); addBack/pushBack: O(1); removeFront/popFront: O(1); removeBack/popBack: O(1); peekFront/peekBack: O(1). Implementation: typically as a doubly linked list or a circular buffer (dynamic array with front/back pointers). Language implementations: Java: ArrayDeque (faster than Stack/LinkedList for stack/queue operations); Python: collections.deque; C++: std::deque. Applications: (1) Sliding window maximum — maintain a monotonic deque of indices (max element always at front); (2) Implementing stack + queue using one structure; (3) Palindrome checking — compare front and back; (4) Undo/redo in text editors; (5) Scheduling algorithms; (6) Deque-based BFS with weights (0-1 BFS). Java ArrayDeque is generally preferred over Stack and LinkedList for LIFO/FIFO operations due to better performance.

Open this question on its own page
32

What is heapsort?

Heapsort is a comparison-based sorting algorithm that uses a binary max-heap. Algorithm: (1) Build max-heap: rearrange array into a valid max-heap in O(n) (call siftDown on all non-leaf nodes from bottom-up); (2) Sort: repeatedly extract the max (root), swap it with the last element, reduce heap size by 1, and sift down the new root: O(n log n) for n extractions. Time: O(n log n) all cases (best, average, worst — unlike quicksort which is O(n²) worst). Space: O(1) — in-place (unlike merge sort which needs O(n)). Stable: NO — relative order of equal elements may change. Practical performance: slower than quicksort in practice due to poor cache behavior (heap accesses are non-sequential). Heapsort is used when: guaranteed O(n log n) worst case is needed AND O(1) space is required (Introsort uses heapsort as fallback when quicksort depth exceeds O(log n) to avoid O(n²)). Build heap in O(n): call siftDown from index n/2-1 to 0 — most nodes are at the bottom where siftDown is O(1), amortizing to O(n) total. This is better than n×insert (each O(log n)) = O(n log n).

Open this question on its own page
33

What is the difference between depth and height of a tree?

Depth of a node is the number of edges from the root to that node. The root has depth 0. A node at depth d has d edges between it and the root. Height of a node is the number of edges on the longest path from that node down to a leaf. A leaf node has height 0. The height of the tree is the height of the root node (equivalently, the depth of the deepest leaf). Example: in a tree with root A (depth 0, height 3), child B (depth 1, height 2), grandchild C (depth 2, height 1), great-grandchild D (depth 3, height 0 — leaf). Note: some sources define height as number of nodes instead of edges (height = depth + 1) — clarify in interviews. Height of a balanced binary tree with n nodes: O(log n). Height of a degenerate (skewed) tree: O(n). Level: depth + 1 (so root is at level 1). Calculating height recursively: height(null) = -1; height(node) = 1 + max(height(node.left), height(node.right)). A balanced tree: for every node, |height(left) - height(right)| ≤ 1. AVL trees maintain this property strictly; Red-Black trees maintain approximate balance.

Open this question on its own page
Intermediate 24 questions

Practical knowledge for developers with hands-on experience.

01

What is the Knapsack problem?

The Knapsack problem is a classic optimization problem: given n items each with weight w[i] and value v[i], and a knapsack with capacity W, maximize the total value of items that fit without exceeding the weight limit. Variants: 0/1 Knapsack: each item can be taken or not (0 or 1 times). DP solution: dp[i][w] = max value using first i items with capacity w. Recurrence: dp[i][w] = max(dp[i-1][w], v[i] + dp[i-1][w-w[i]]) if w[i] ≤ w. Time: O(n×W), Space: O(n×W), optimized to O(W). This is called pseudo-polynomial time — polynomial in n and W, but W can be exponential in its bit length. Unbounded Knapsack: each item can be taken unlimited times. dp[w] = max(dp[w], v[i] + dp[w-w[i]]). Fractional Knapsack: items can be broken (take a fraction). Greedy solution: sort by value/weight ratio, take as much of the highest ratio item as possible. O(n log n). Only the fractional variant has a greedy solution. Applications: financial portfolio optimization, cutting problems, resource allocation, cargo loading. The 0/1 knapsack DP solution demonstrates subset sum and bounded optimization patterns used in many other DP problems.

Open this question on its own page
02

What is the Longest Common Subsequence (LCS)?

The Longest Common Subsequence (LCS) problem finds the longest sequence that appears in the same relative order in both strings, but not necessarily consecutively. Subsequence vs Substring: subsequence elements don't need to be contiguous; substring elements must be. Example: LCS("ABCBDAB", "BDCAB") = "BCAB" or "BDAB", length 4. DP recurrence: let dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1]: if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1; else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]). Base: dp[0][j] = dp[i][0] = 0. Time: O(m×n). Space: O(m×n), optimized to O(min(m,n)) using two rows. Reconstruct the actual LCS by backtracking through the dp table. Applications: (1) Git diff — find common lines between two versions of a file; (2) Spell checking — edit distance variant; (3) Bioinformatics — DNA/protein sequence alignment; (4) File comparison tools. Related: Longest Common Substring (contiguous), Edit Distance (Levenshtein distance), Diff algorithm. LCS is a classic 2D DP problem that appears frequently in interviews and demonstrates the power of DP for string comparison problems.

Open this question on its own page
03

What is topological sorting?

Topological sort is a linear ordering of vertices in a DAG (Directed Acyclic Graph) such that for every directed edge u → v, vertex u comes before v in the ordering. Only applicable to DAGs (no cycles — a cycle would create a circular dependency with no valid ordering). Two algorithms: (1) Kahn's algorithm (BFS-based): compute in-degree of all nodes; add all zero in-degree nodes to queue; process: dequeue a node, add to result, decrement in-degree of neighbors, add newly zero in-degree nodes to queue; if result has fewer nodes than the graph, a cycle exists. Time: O(V+E); (2) DFS-based: run DFS; after visiting all neighbors of a node, push it to a stack; the stack reversed is the topological order. Time: O(V+E). Applications: (1) Build systems — determine compilation order (file A must compile before B if B depends on A); (2) Course prerequisite scheduling; (3) Task scheduling with dependencies; (4) Package manager dependency resolution (npm, pip); (5) Spreadsheet calculation order; (6) Pipeline stages. If a cycle is detected during topological sort, it indicates a circular dependency (error in build systems/course prerequisites).

Open this question on its own page
04

What is Dijkstra's algorithm?

Dijkstra's algorithm finds the shortest path from a source vertex to all other vertices in a weighted graph with non-negative edge weights. Algorithm: (1) Initialize distances: source = 0, all others = ∞; (2) Use a min-priority queue; add source; (3) While queue not empty: extract minimum distance vertex u; for each neighbor v: if dist[u] + weight(u,v) < dist[v], update dist[v] and add v to queue (or decrease key). Time: O((V+E) log V) with binary heap, O(V²) with array (for dense graphs). Space: O(V). Greedy algorithm — always expands the closest unvisited vertex. Correctness: guaranteed optimal because all edge weights ≥ 0, so once a node is finalized, its shortest path is found. Fails with negative weights: use Bellman-Ford instead. Variants: find shortest path to a specific target (stop early when target is extracted), bidirectional Dijkstra (start from source and target simultaneously — faster). Applications: (1) GPS navigation — shortest route between locations; (2) Network routing protocols (OSPF); (3) Game AI pathfinding; (4) Social network closest connection. A* algorithm: Dijkstra + heuristic to guide search direction — faster for single-target in spatial graphs.

Open this question on its own page
05

What is Bellman-Ford algorithm?

Bellman-Ford finds the shortest path from a source to all vertices in a weighted graph, including graphs with negative weight edges. It can also detect negative weight cycles (where Dijkstra fails). Algorithm: (1) Initialize distances: source = 0, all others = ∞; (2) Repeat V-1 times: relax all edges — for each edge (u,v,w): if dist[u] + w < dist[v], update dist[v]; (3) Check for negative cycles: run one more relaxation pass — if any distance can still be reduced, a negative cycle exists. Time: O(V×E). Space: O(V). Why V-1 iterations: the shortest path in a graph with V nodes has at most V-1 edges. If it still improves on the Vth pass, there's a negative cycle. Bellman-Ford vs Dijkstra: Bellman-Ford handles negative weights; Dijkstra doesn't but is faster (O((V+E)logV) vs O(VE)). SPFA (Shortest Path Faster Algorithm): queue-based Bellman-Ford optimization — processes only vertices whose distances changed. Average O(E), worst O(VE). Applications: (1) Network routing (RIP protocol); (2) Currency arbitrage detection (negative cycle = profitable arbitrage); (3) Constraint satisfaction; (4) Negative weight graphs. Bellman-Ford and Dijkstra are the two fundamental single-source shortest path algorithms.

Open this question on its own page
06

What is Floyd-Warshall algorithm?

Floyd-Warshall finds shortest paths between all pairs of vertices in a weighted graph (positive or negative weights, but no negative cycles). Algorithm: dp[i][j][k] = shortest path from i to j using only vertices {1..k} as intermediate nodes. Key insight: either vertex k is on the shortest path from i to j (so use k as intermediate), or it's not (path through vertices 1..k-1). Recurrence: dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]). Triple nested loop: for each intermediate vertex k, for each source i, for each destination j, update dist[i][j]. Initialize: dist[i][i] = 0; dist[i][j] = edge weight if edge exists, else ∞. Time: O(V³). Space: O(V²) — the 2D distance matrix (optimize by using k in-place). Negative cycle detection: if dist[i][i] < 0 after running, vertex i is on a negative cycle. Applications: (1) All-pairs shortest path in dense graphs; (2) Transitive closure of a graph (reachability); (3) Finding the diameter of a graph (max shortest path); (4) Inversion of real matrices. Comparison: for all-pairs in sparse graphs, run Dijkstra from every vertex: O(V × (V+E) log V); for dense graphs Floyd-Warshall O(V³) may be comparable and simpler.

Open this question on its own page
07

What is a minimum spanning tree (MST)?

A Minimum Spanning Tree (MST) of a connected weighted undirected graph is a subset of edges that connects all vertices with the minimum possible total edge weight, forming a tree (no cycles, V-1 edges). Two classic MST algorithms: (1) Kruskal's algorithm: Greedy. Sort all edges by weight. Add an edge if it doesn't create a cycle (use Union-Find to check). Repeat until V-1 edges added. Time: O(E log E) for sorting + O(E α(V)) for Union-Find operations ≈ O(E log E). Best for sparse graphs (few edges); (2) Prim's algorithm: Greedy. Start from any vertex. Maintain a set of visited vertices. Repeatedly add the minimum weight edge that connects a visited vertex to an unvisited one. Use a priority queue. Time: O((V+E) log V) with binary heap, O(V²) with adjacency matrix (better for dense graphs). Applications: (1) Network design — minimum cost to connect all nodes (network cabling, pipeline, road construction); (2) Cluster analysis; (3) Image segmentation; (4) Approximation algorithms for NP-hard problems (TSP); (5) Borůvka's algorithm for parallel MST computation.

Open this question on its own page
08

What is Union-Find (Disjoint Set Union)?

Union-Find (Disjoint Set Union, DSU) is a data structure that efficiently tracks which elements belong to the same group (set) and supports merging groups. Operations: find(x) — return the representative (root) of x's set; union(x, y) — merge the sets containing x and y. Optimizations: (1) Path compression: in find(), make every node on the path point directly to the root — flattens the tree for future finds; (2) Union by rank/size: in union(), attach the smaller tree under the root of the larger — keeps trees shallow. With both optimizations: nearly O(1) per operation — amortized O(α(n)) where α is the inverse Ackermann function (essentially constant for all practical n). Implementation: array where parent[i] = parent of i (root[i] == i for roots). Applications: (1) Kruskal's MST — detect cycle before adding edge; (2) Number of connected components in a graph; (3) Percolation theory; (4) Dynamic connectivity queries; (5) Social network grouping; (6) Redundant connections detection. DSU is one of the most useful and elegant data structures — simple to implement with massive impact.

Open this question on its own page
09

What is the longest increasing subsequence (LIS)?

The Longest Increasing Subsequence (LIS) problem finds the length of the longest strictly increasing subsequence in an array. Example: in [10, 9, 2, 5, 3, 7, 101, 18], LIS = [2, 3, 7, 101], length 4. DP solution (O(n²)): dp[i] = LIS ending at index i. For each i, dp[i] = max(dp[j] + 1) for all j < i where arr[j] < arr[i]. Base: dp[i] = 1. Answer: max(dp[i]). Optimal O(n log n) solution using binary search: maintain a sorted array tails where tails[i] is the smallest possible tail element of all increasing subsequences of length i+1. For each element x: binary search (lower_bound) for the first element in tails ≥ x; if found, replace it with x; otherwise append x. The length of tails is the LIS length. This gives the LIS length but not the actual sequence (requires additional tracking). Applications: (1) Patience sorting (O(n log n) deck of cards sort related to LIS); (2) Dilworth's theorem — minimum number of non-increasing subsequences to partition the array = LIS length; (3) Edit distance variant; (4) Stock trading problems. Often combined with other DP problems (LIS in 2D — the Russian doll envelopes problem).

Open this question on its own page
10

What is a segment tree?

A segment tree is a binary tree where each node stores information about a range of the array. Useful for answering range queries (sum, min, max) and supporting point updates efficiently. Structure: leaf nodes represent individual elements; internal nodes represent the aggregate (sum/min/max) of their children's ranges; root represents the entire array range [0, n-1]. Build: O(n). Operations: Range query: O(log n) — traverse only relevant segments; Point update: O(log n) — update leaf and propagate up. Implementation: array-based (similar to heap), node at index i has children at 2i and 2i+1. Space: O(n) (typically 4n array). Lazy propagation: for range updates (add 5 to all elements in [l, r]), push updates lazily — only propagate when the segment is queried. Enables O(log n) range updates. Applications: (1) Range sum queries with updates; (2) Range minimum/maximum queries; (3) Count inversions; (4) Computational geometry; (5) Interval scheduling. Fenwick Tree (Binary Indexed Tree) is simpler to implement and uses less memory for range sum queries but doesn't generalize to all segment tree use cases. Segment trees are powerful for competitive programming and database index structures.

Open this question on its own page
11

What is a Fenwick tree (Binary Indexed Tree)?

A Fenwick Tree (Binary Indexed Tree, BIT) is a data structure that efficiently computes prefix sums and supports point updates. More memory-efficient and simpler to implement than a segment tree for sum queries. Key idea: uses the binary representation of indices to determine what range each node stores. bit[i] stores the sum of elements from index i - lowbit(i) + 1 to i, where lowbit(i) = i & (-i) (the lowest set bit). Operations: Update(i, delta): O(log n) — propagate from i upward: while i ≤ n: bit[i] += delta; i += lowbit(i); PrefixSum(i): O(log n) — sum from 1 to i: while i > 0: sum += bit[i]; i -= lowbit(i); Range sum(l, r): prefixSum(r) - prefixSum(l-1). Space: O(n). BIT is initialized to all zeros; add elements via updates. Applications: (1) Count inversions in an array; (2) Dynamic prefix sum; (3) Counting smaller elements to the right; (4) 2D BIT for 2D range sum queries. BIT vs Segment Tree: BIT is simpler (15 lines vs 50), uses less memory (O(n) vs O(4n)), but only handles sum queries; Segment Tree handles sum, min, max, GCD, and more.

Open this question on its own page
12

What is a balanced binary search tree (AVL tree)?

An AVL tree (Adelson-Velsky and Landis, 1962) is a self-balancing binary search tree where the heights of the left and right subtrees of every node differ by at most 1 (balance factor ∈ {-1, 0, 1}). Operations take O(log n) guaranteed because height is always O(log n). Balance factor = height(left subtree) - height(right subtree). Rebalancing with rotations: after insert/delete, if balance factor becomes ±2, perform rotations to restore balance. Four cases: (1) Left-Left (LL): single right rotation; (2) Right-Right (RR): single left rotation; (3) Left-Right (LR): left rotation on left child, then right rotation; (4) Right-Left (RL): right rotation on right child, then left rotation. Time: O(log n) for search, insert, delete — guaranteed. Space: O(n) + O(1) per node (balance factor). Comparison with Red-Black Trees: AVL trees are more strictly balanced (faster lookups), but require more rotations during insert/delete; Red-Black trees allow slightly looser balance (max height 2log n), fewer rotations — better for write-heavy workloads. Java TreeMap/TreeSet use Red-Black trees. AVL trees are used when read-heavy operations dominate.

Open this question on its own page
13

What is a Red-Black tree?

A Red-Black tree is a self-balancing BST where each node has a color (red or black) and satisfies: (1) Root is black; (2) Leaves (NIL nodes) are black; (3) Red nodes have black children (no consecutive red nodes); (4) Every path from a node to its descendant NIL nodes has the same number of black nodes (black-height). These properties guarantee the tree height is at most 2log₂(n+1) — O(log n). Operations: O(log n) for search, insert, delete. Fewer rotations than AVL trees: at most 3 rotations per insert (0-2 for delete), vs AVL which may need O(log n) rotations. Rebalancing: insertion may require recoloring and at most 2 rotations; deletion may require recoloring and at most 3 rotations. Memory: 1 extra bit per node (color). Applications: Java TreeMap and TreeSet, C++ std::map, std::set, std::multimap. Linux kernel completely fair scheduler (CFS) uses red-black trees for task queuing. Linux memory management uses red-black trees for tracking virtual memory areas. Comparison: AVL = faster search (stricter balance), Red-Black = faster insert/delete (fewer rotations). Red-Black is preferred in standard library implementations due to better write performance.

Open this question on its own page
14

What is the edit distance (Levenshtein distance) problem?

The Edit Distance problem finds the minimum number of single-character operations (insert, delete, substitute) to transform one string into another. Operations: Insert a character; Delete a character; Substitute a character. Example: "kitten" → "sitting" = 3 operations (substitute k→s, substitute e→i, insert g). DP solution: dp[i][j] = min operations to convert s1[0..i-1] to s2[0..j-1]. Recurrence: if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] (no operation needed); else: dp[i][j] = 1 + min(dp[i-1][j] (delete), dp[i][j-1] (insert), dp[i-1][j-1] (substitute)). Base: dp[i][0] = i (delete i characters), dp[0][j] = j (insert j characters). Time: O(m×n). Space: O(m×n), optimized to O(min(m,n)) using two rows. Applications: (1) Spell checker — suggest corrections; (2) DNA sequence alignment; (3) Fuzzy string matching; (4) git diff; (5) Plagiarism detection; (6) Natural language processing. Hamming distance: edit distance considering only substitutions (strings must be equal length). Jaro-Winkler distance: better for short strings like names.

Open this question on its own page
15

What is the coin change problem?

The Coin Change problem has two variants: (1) Minimum coins: given coin denominations and a target amount, find the minimum number of coins to make the amount. DP: dp[i] = minimum coins for amount i. Recurrence: dp[i] = min(dp[i - coin] + 1) for each coin ≤ i. Base: dp[0] = 0. Time: O(amount × coins). Space: O(amount). Example: coins=[1,2,5], amount=11 → dp[11] = 3 (5+5+1); (2) Number of ways: count distinct combinations to make the target. DP: dp[i] = number of ways to make amount i. Recurrence: dp[i] += dp[i - coin] for each coin ≤ i. Base: dp[0] = 1. This is the unbounded knapsack variant. Greedy approach: works ONLY for canonical coin systems (like US coins: 25, 10, 5, 1) but fails for arbitrary denominations (e.g., coins=[1,3,4], amount=6: greedy picks 4+1+1=3 coins, DP finds 3+3=2 coins). Related: Coin Change with limited quantity (bounded knapsack). Applications: (1) Currency systems; (2) Change-making machines; (3) Dynamic programming pedagogy; (4) Combinatorics problems. The coin change DP pattern (unbounded knapsack) applies to: word break, integer break, and other "ways to make X from components" problems.

Open this question on its own page
16

What is memoization vs tabulation in dynamic programming?

Both are DP techniques to avoid recomputing overlapping subproblems, but they approach it differently: Memoization (Top-Down): start with the original recursive solution; add a cache (hash map or array) to store computed results; before computing, check if the result is already cached; if yes, return cached; if no, compute, store, return. Pros: only computes needed subproblems (lazy); natural recursive structure; easy to implement from brute force. Cons: recursive call stack overhead; may hit stack overflow for large inputs; hash map overhead if using map. Tabulation (Bottom-Up): identify the order to compute subproblems (from smallest to largest); fill a table iteratively; each entry depends only on previously filled entries. Pros: no recursion (no stack overflow); typically faster (no call overhead); often allows space optimization (only need last few rows). Cons: must compute all subproblems even if not needed; requires understanding the order of computation. When to use each: memoization is easier to implement initially; tabulation is generally more efficient. Space optimization: in many DP problems, you only need the last row(s) — reduce O(n²) space to O(n) or O(1). Example: Fibonacci with memoization: cache[n] = f(n-1) + f(n-2); with tabulation: dp[i] = dp[i-1] + dp[i-2], iterate from 2 to n.

Open this question on its own page
17

What is the activity selection / interval scheduling problem?

The Activity Selection problem: given n activities with start and end times, select the maximum number of non-overlapping activities. Greedy solution: (1) Sort activities by end time; (2) Select the first activity; (3) For each subsequent activity, select it if its start time ≥ the end time of the last selected activity. Greedy proof: always selecting the activity that ends earliest leaves maximum time for remaining activities. Time: O(n log n) for sorting + O(n) for selection. Applications: (1) Classroom/meeting room scheduling; (2) Processor task scheduling; (3) Bandwidth allocation. Related problems: Minimum meeting rooms: find the minimum number of meeting rooms needed to schedule all meetings. Greedy with a min-heap: sort by start time, for each meeting, if earliest-ending room is free (heap.peek().end ≤ current.start), reuse it; otherwise add a new room. Answer = max heap size. O(n log n). Interval merging: merge all overlapping intervals. Sort by start, iterate merging overlapping pairs. O(n log n). Non-overlapping intervals (minimum removals): n - maximum non-overlapping intervals = minimum to remove. Activity selection problems are the canonical greedy algorithm examples, demonstrating that greedy works when optimal substructure + greedy choice property hold.

Open this question on its own page
18

What is the sliding window maximum problem?

The Sliding Window Maximum problem: given an array and window size k, find the maximum in each window of size k as the window slides from left to right. Naive: O(n×k). Optimal with monotonic deque: O(n). Key insight: maintain a deque of indices in decreasing order of their values — the front is always the maximum of the current window. Algorithm: for each element i: (1) Remove from front indices that are out of the current window (index ≤ i-k); (2) Remove from back all indices with value ≤ arr[i] (they can never be the maximum in any future window where arr[i] is present); (3) Add i to back; (4) When i ≥ k-1, record arr[deque.front()] as the window maximum. The deque is always in decreasing order of values — this is a monotonic decreasing deque. Time: O(n) — each element is added and removed at most once. Space: O(k) for the deque. Applications: (1) This exact problem (Leetcode 239); (2) Jump Game VI (maximum score in k-window DP); (3) Constrained subset sum; (4) Finding spans in stock problems. The monotonic stack/deque technique is broadly applicable to "nearest greater/smaller" element problems, stock span, and sliding window optimization of DP recurrences.

Open this question on its own page
19

What is a monotonic stack and its applications?

A monotonic stack maintains elements in monotonically increasing or decreasing order by popping elements that violate the order before pushing new ones. The key insight: while we pop an element x because of a new element y (y > x for increasing monotonic stack), y is the "next greater element" of x. Monotonic decreasing stack: maintains elements in decreasing order from bottom to top. Used for "next greater element" problems. Monotonic increasing stack: used for "next smaller element" problems. Classic problems: (1) Next Greater Element: for each element, find the next element to its right that is greater. Traverse left to right, pop elements smaller than current — current is their answer; push current. O(n); (2) Largest Rectangle in Histogram: use monotonic increasing stack of bar indices; when a shorter bar is found, pop and compute rectangles. O(n); (3) Maximal Rectangle in binary matrix (builds on histogram); (4) Trapping Rainwater: monotonic stack or two-pointer; (5) Stock Span Problem; (6) Daily Temperatures; (7) Remove K Digits to minimize the number. Template: maintain stack, for each element check if it should trigger pops, process pops, push current. O(n) — each element pushed and popped at most once.

Open this question on its own page
20

What is the subset sum problem?

The Subset Sum problem: given a set of n positive integers and a target sum S, determine if there exists a subset with sum exactly S. DP solution: dp[i][s] = true if a subset of first i elements sums to s. Recurrence: dp[i][s] = dp[i-1][s] (exclude item i) OR dp[i-1][s-arr[i]] (include item i, if arr[i] ≤ s). Base: dp[0][0] = true, dp[i][0] = true (empty subset sums to 0). Time: O(n×S). Space: O(n×S), optimized to O(S) using 1D DP (traverse right to left: dp[s] = dp[s] || dp[s-arr[i]]). This is a 0/1 Knapsack variant (value = weight). Variants: (1) Count subsets with given sum; (2) Minimum difference partition — partition into two subsets with minimum sum difference; (3) Equal sum partition — can we partition into two equal halves? Reduce to subset sum with target = totalSum/2; (4) Number of subsets with given difference. NP-completeness: Subset Sum is NP-complete in general (no known polynomial algorithm for arbitrary sizes). The DP solution is pseudo-polynomial (polynomial in n and S, but S can be exponential in its bit length). Applications: cryptography, resource allocation, scheduling, target sum (allow +/-) problems.

Open this question on its own page
21

What is a graph coloring problem?

Graph coloring assigns colors to vertices such that no two adjacent vertices share the same color, using the minimum number of colors. Chromatic number χ(G): the minimum number of colors needed. This is NP-hard in general (no polynomial algorithm for optimal coloring). Special cases: 2-colorable (Bipartite): checkable in O(V+E) with BFS/DFS — color alternately; if you need the same color for adjacent vertices, it's not bipartite; Greedy coloring: iterate vertices, assign the smallest color not used by neighbors. Not optimal but fast — uses at most Δ+1 colors (Δ = max degree); Backtracking: try each color for each vertex, backtrack if conflict — exponential worst case but works for small graphs; Welsh-Powell algorithm: sort vertices by degree descending, greedily color. Applications: (1) Register allocation in compilers (variables are nodes, conflicts are edges — minimize CPU registers needed); (2) Map coloring (four color theorem: any planar map needs ≤ 4 colors); (3) Scheduling (time slot assignment — same-time exams can't conflict); (4) Sudoku solving; (5) Frequency assignment in wireless networks. Interval graph coloring (intervals as nodes, overlapping as edges) is solvable in O(n log n) greedily.

Open this question on its own page
22

What is the N-Queens problem?

The N-Queens problem places N queens on an N×N chessboard such that no two queens attack each other (no same row, column, or diagonal). It's the classic backtracking problem. Algorithm: place queens row by row; for each row, try each column; check if safe (no conflict with previously placed queens); if safe, place and recurse to next row; if no valid column, backtrack (remove last queen, try next column). Safety check: O(n) — check column, both diagonals. Optimization: use sets to track occupied columns and diagonals (row-col = const for one diagonal, row+col = const for the other). Backtracking implementation: def solve(row): if row == n: record solution; return; for col in range(n): if col not in cols and (row-col) not in diag1 and (row+col) not in diag2: place, recurse(row+1), remove. Time: O(n!) worst case (pruning makes it much faster in practice). Space: O(n) recursion depth. Solutions: N=8 has 92 solutions. Generalizations: (1) Count solutions vs find one; (2) N-Rooks (only row/column constraints); (3) N-Knights; (4) Sudoku solver. Backtracking pattern: make a choice → recurse → undo the choice (backtrack). Used in all constraint satisfaction problems: Sudoku, crosswords, map coloring, Hamiltonian paths.

Open this question on its own page
23

What is Kadane's algorithm?

Kadane's algorithm finds the maximum sum contiguous subarray in O(n) time and O(1) space. It's the optimal solution for the Maximum Subarray Sum problem (Leetcode 53). Key insight: at each position, either extend the previous subarray or start a new one at the current element — whichever is larger. Algorithm: maxSum = arr[0]; currentSum = arr[0]; for i from 1 to n-1: currentSum = max(arr[i], currentSum + arr[i]); maxSum = max(maxSum, currentSum);. This is a DP where dp[i] = maximum subarray sum ending at index i. If dp[i-1] < 0, don't extend (currentSum becomes arr[i]); if ≥ 0, extend. Time: O(n). Space: O(1). Track start/end indices for the actual subarray. Extensions: (1) Circular subarray maximum: max of (Kadane's result, totalSum - minimum subarray); (2) Maximum product subarray: track both max and min products (negatives flip); (3) 2D maximum sum subarray: fix columns, compress rows using prefix sums, apply Kadane's in O(n²×m) total; (4) K maximum non-overlapping subarrays. Applications: financial analysis (max profit period), signal processing, computer vision (maximum density rectangle).

Open this question on its own page
24

What is a balanced parentheses problem and how to solve it?

The balanced parentheses problem checks if a string of brackets is valid — every opening bracket has a corresponding closing bracket in the correct order. Uses a stack. Algorithm: iterate the string; push opening brackets onto the stack; for closing brackets, check if stack is non-empty and top is the matching opening bracket; if not, invalid; at end, if stack is empty, valid. Example: valid: "([])", "()[]{}"; invalid: "(]", "([)]", "{". Time: O(n). Space: O(n). Extension — Minimum insertions to balance: track open and close counts; for each '(': open++; for each ')': if open > 0 open-- else close++; answer = open + close. Generate all valid parentheses (Leetcode 22): backtracking — track open and close counts; add '(' if open < n; add ')' if close < open; O(Catalan(n)). Longest valid parentheses (Leetcode 32): DP or stack — O(n). Score of parentheses: assign scores recursively. Related problems: (1) Remove minimum invalid parentheses; (2) Check if paths in a matrix form valid parentheses; (3) Minimum add to make valid. The stack-based bracket matching pattern extends to: matching HTML tags, checking XML, and parsing arithmetic expressions.

Open this question on its own page
Advanced 15 questions

Deep expertise questions for senior and lead roles.

01

What is amortized analysis?

Amortized analysis gives the average cost per operation over a sequence of operations, even if some individual operations are expensive. Unlike average-case analysis (probabilistic), amortized analysis is a worst-case guarantee for the average cost over any sequence. Methods: (1) Aggregate method: compute the total cost of n operations, divide by n. Example: dynamic array — n pushes cost at most 3n total (each element is moved at most twice across all doubly-capacity resizings). Amortized cost = 3n/n = O(1) per push; (2) Accounting method: assign amortized costs to operations; some operations "pay extra" (credit) that covers future expensive operations. Each operation pays its actual cost + charges credit; (3) Potential method: define a potential function Φ(state); amortized cost of operation = actual cost + ΔΦ. Classic examples: (1) Dynamic array (ArrayList): push O(1) amortized despite occasional O(n) resize; (2) Splay tree: self-adjusting BST — O(log n) amortized; (3) Fibonacci heap: decrease-key O(1) amortized; (4) Binary counter: increment O(1) amortized despite occasional carry propagation; (5) Union-Find: nearly O(1) amortized with path compression + union by rank.

Open this question on its own page
02

What is the A* pathfinding algorithm?

A* (A-star) is an informed search algorithm that finds the shortest path by combining Dijkstra's systematic search with a heuristic to guide exploration toward the goal. Unlike Dijkstra (which explores all directions equally), A* prioritizes nodes that appear closer to the goal. f(n) = g(n) + h(n): g(n) = actual cost from start to node n; h(n) = heuristic estimate of cost from n to goal; f(n) = estimated total cost through n. Priority queue ordered by f(n). Algorithm: same as Dijkstra but use f(n) for priority; expand node with lowest f(n); update neighbors with new g-values; stop when goal is dequeued. Admissibility: heuristic h must never overestimate the true cost — then A* is guaranteed optimal. Consistency: h(n) ≤ cost(n, n') + h(n') for all edges. Common heuristics: Manhattan distance for grid (no diagonals); Euclidean distance for geometric graphs; Chebyshev distance for grids with diagonal moves. Time: O(E) best case, O(b^d) worst where b = branching factor, d = depth. With perfect heuristic: O(d). Applications: (1) GPS navigation; (2) Game AI pathfinding (RTS, RPGs); (3) Robotics motion planning; (4) Puzzle solving (15-puzzle, sliding puzzles). Bidirectional A*: search from both start and goal — faster in practice.

Open this question on its own page
03

What is Tarjan's algorithm for finding Strongly Connected Components?

Tarjan's algorithm finds all Strongly Connected Components (SCCs) in a directed graph in O(V+E). An SCC is a maximal subset of vertices where every vertex is reachable from every other. Key concepts: DFS discovery time (disc[u]): when u is first visited; low[u]: minimum disc value reachable from the subtree rooted at u (including back edges). Algorithm: run DFS; maintain a stack of visited nodes; for each node u: set disc[u] = low[u] = current time; push to stack; for each neighbor v: if unvisited, recurse, low[u] = min(low[u], low[v]); if v is on stack, low[u] = min(low[u], disc[v]); after processing all neighbors: if low[u] == disc[u], u is the root of an SCC — pop the stack until u and output as one SCC. Time: O(V+E). Each node is pushed and popped at most once. Kosaraju's algorithm (alternative): (1) DFS on original graph, record finish order; (2) Transpose the graph (reverse all edges); (3) DFS on transposed graph in reverse finish order. Each DFS on step 3 gives one SCC. Also O(V+E) but requires two DFS passes. Applications: (1) Finding cycles in directed graphs; (2) Condensation DAG (reduce SCCs to single nodes for topological sort); (3) Compiler optimization; (4) Web page groupings.

Open this question on its own page
04

What is the Master Theorem for solving recurrences?

The Master Theorem provides a cookbook solution for recurrences of the form T(n) = aT(n/b) + f(n), where a ≥ 1 (subproblems), b > 1 (division factor), f(n) (work at current level). Three cases based on comparing f(n) with n^(log_b(a)): Let p = log_b(a). Case 1: if f(n) = O(n^(p-ε)) for some ε > 0 (f grows slower than n^p) → T(n) = Θ(n^p). Tree work dominates. Example: T(n) = 8T(n/2) + O(n²); p = log₂8 = 3; f(n) = n² = O(n^(3-ε)) → T(n) = Θ(n³). Case 2: if f(n) = Θ(n^p × log^k(n)) → T(n) = Θ(n^p × log^(k+1)(n)). Equal work. Example: T(n) = 2T(n/2) + O(n); p = 1; f(n) = O(n¹) → T(n) = Θ(n log n) (merge sort). Case 3: if f(n) = Ω(n^(p+ε)) and regularity condition → T(n) = Θ(f(n)). Root work dominates. Example: T(n) = T(n/2) + O(n); p = 0; f(n) = O(n) = Ω(n^(0+ε)) → T(n) = Θ(n). Doesn't apply when: f(n) falls in the gap between cases or the recursion isn't of the exact form. Applications: analyze merge sort O(n log n), Strassen matrix multiplication O(n^2.807), binary search O(log n).

Open this question on its own page
05

What are NP, NP-Complete, and NP-Hard problems?

Complexity classes for decision problems: P (Polynomial time): problems solvable in polynomial time O(n^k) by a deterministic Turing machine. Examples: sorting, shortest path, primality (AKS). NP (Nondeterministic Polynomial): problems where a proposed solution can be verified in polynomial time. Examples: Subset Sum, TSP, 3-SAT, Graph Coloring. P ⊆ NP (solving implies verifying). Whether P = NP is the most important open problem in computer science ($1M Millennium Prize). NP-Hard: every NP problem can be reduced to it in polynomial time — as hard as the hardest NP problems. NP-Hard problems may or may not be in NP. Examples: Halting problem (not in NP), TSP optimization version. NP-Complete: both NP and NP-Hard — the hardest problems in NP. If any NP-Complete problem is solvable in polynomial time, then P = NP. Examples: 3-SAT, Vertex Cover, Independent Set, Clique, Hamiltonian Path, Travelling Salesman (decision), Subset Sum, Graph Coloring (3+ colors), Knapsack. Reductions: Cook's theorem proved 3-SAT is NP-Complete; all others proved NP-Complete by polynomial-time reduction from a known NP-Complete problem. Practical handling: approximation algorithms (near-optimal), heuristics, fixed-parameter tractability, or exponential algorithms for small inputs.

Open this question on its own page
06

What is the Boyer-Moore voting algorithm?

The Boyer-Moore Voting algorithm finds the majority element (appearing more than n/2 times) in O(n) time and O(1) space — better than sorting (O(n log n)) or hash map (O(n) space). Key insight: if we cancel out each occurrence of the majority element with a different element, the majority element still survives. Algorithm: maintain a candidate and a count; initialize both; for each element x: if count == 0, candidate = x, count = 1; else if x == candidate, count++; else count--. After one pass, candidate is the majority element (if one exists). Verification pass (if majority not guaranteed): count occurrences of candidate to confirm it appears > n/2 times. Time: O(n). Space: O(1). Why it works: the majority element appears more than n/2 times. Every cancellation reduces both one majority vote and one non-majority vote — majority votes survive. Variant — N/3 majority (elements appearing more than n/3 times — at most 2 such elements): maintain 2 candidates and 2 counts; use same cancel logic for each; verify both candidates at end. N/k majority: requires k-1 candidates. Applications: (1) Finding majority opinion in voting; (2) Database duplicate detection; (3) Stream processing where memory is limited. One of the most elegant algorithms demonstrating that seemingly hard problems have beautiful O(1)-space solutions.

Open this question on its own page
07

What is the Rabin-Karp string matching algorithm?

Rabin-Karp is a string searching algorithm that uses rolling hashing to find pattern(s) in text efficiently. Naive approach O(n×m) re-compares every character at each position. Rabin-Karp uses a hash of the pattern and a rolling hash of the text window — skip positions where hashes differ (likely mismatches), only verify character-by-character when hashes match. Rolling hash: when the window slides one position, update the hash in O(1): remove leftmost character's contribution, add new rightmost character. Hash formula: hash(s) = (s[0]×p^(m-1) + s[1]×p^(m-2) + ... + s[m-1]) mod q. Rolling: new_hash = (hash - s[left]×p^(m-1)) × p + s[right], mod q. Average time: O(n+m). Worst case (many hash collisions): O(n×m) — choose large prime q and random base p to minimize. Applications: (1) Plagiarism detection — find matching substrings; (2) Multiple pattern search: hash all patterns; O(n×k) best vs O(n×m×k) naive; (3) Longest repeated substring; (4) Longest common substring; (5) 2D pattern matching. KMP (O(n+m)) is better for single pattern exact matching; Rabin-Karp shines for multiple patterns and approximate matching. Aho-Corasick (O(n+m+k)) is optimal for many patterns simultaneously.

Open this question on its own page
08

What is the KMP (Knuth-Morris-Pratt) algorithm?

KMP is a linear-time string matching algorithm that avoids redundant comparisons by preprocessing the pattern to find proper prefix-suffix overlaps. The key insight: when a mismatch occurs, we've already matched some prefix of the pattern in the text — use that knowledge to avoid re-scanning. Failure function (LPS — Longest Proper Prefix Suffix): lps[i] = length of longest proper prefix of pattern[0..i] that is also a suffix. Precompute lps in O(m). Matching: scan text with pointer i, pattern with j; if match, i++ and j++; if j == m, pattern found; if mismatch and j > 0: j = lps[j-1] (don't reset i — skip known characters); if j == 0 and mismatch: i++. Time: O(n+m) — each character is visited at most twice. Space: O(m) for lps array. Example: pattern = "ABABC", text = "ABABABABC". The lps = [0,0,1,2,0]. When mismatch at position 4, jump to lps[3]=2 and continue. Z-algorithm: alternative O(n+m) string matching — Z[i] = length of longest common prefix of the string and the suffix starting at i. Both KMP and Z-algorithm are optimal for single-pattern exact matching. Applications: (1) Substring search; (2) Detecting periodic strings (pattern is its own periodic unit); (3) DNA sequence analysis; (4) Text editors (Find/Replace).

Open this question on its own page
09

What is a B-tree and where is it used?

A B-tree is a self-balancing search tree generalization of BST where each node can have multiple keys and multiple children (between t and 2t children for minimum degree t). Properties: (1) All leaves are at the same depth (balanced); (2) Each node (except root) has at least ⌈m/2⌉-1 keys (at least half full); (3) Each node has at most m-1 keys (m children) — m is the order; (4) Keys within a node are sorted; (5) A node with k keys has k+1 children. Operations: O(log n) for search, insert, delete. Why B-trees for databases/filesystems: designed to minimize disk I/O — nodes correspond to disk pages (typically 4KB-16KB); wide branching factor means very few disk accesses for any lookup (a B-tree with billions of entries has height ~3-4). B+ tree (most databases, including MySQL InnoDB, PostgreSQL): all actual data (values) stored in leaf nodes; internal nodes store only keys for routing; leaf nodes linked as a doubly linked list — enables efficient range scans (walk from first matching key through linked leaf list). B- vs B+: B+ tree requires range scans in O(k+log n) vs O(k log n) for B-tree. Applications: (1) Database indexes (primary key, secondary indexes); (2) File systems (HFS+, NTFS, ext4, Btrfs); (3) Key-value stores; (4) In-memory databases.

Open this question on its own page
10

What is the difference between DFS and BFS in terms of space complexity?

Both BFS and DFS are O(V) space in the worst case, but their space usage patterns differ significantly: DFS space complexity: O(h) where h = maximum depth of the recursion/stack. For a balanced binary tree: O(log n). For a skewed tree (linked list-like): O(n). For a general graph with V vertices: O(V) worst case. DFS only needs to remember the current path from root to current node — efficient for deep, narrow trees. Uses a stack (call stack for recursive, explicit stack for iterative). BFS space complexity: O(w) where w = maximum width of the graph at any level. For a balanced binary tree: O(n/2) = O(n) — the last level has n/2 nodes, all in the queue at once. For a path graph (linked list): O(1) — only 1 node at each level. BFS needs to store all nodes at the current level — memory-intensive for wide trees. Uses a queue. Practical implications: BFS is memory-intensive for wide, shallow graphs; DFS is stack-intensive for deep, narrow graphs. For very wide graphs (e.g., shortest 6-degrees-of-separation in a social network where each person has 500 friends), BFS memory usage grows as 500^level — impractical. Bidirectional BFS addresses this by searching from both ends, reducing queue size from 500^6 to 2×500^3. Iterative deepening DFS (IDDFS) combines BFS's level-by-level exploration with DFS's O(depth) space.

Open this question on its own page
11

What is the travelling salesman problem (TSP)?

The Travelling Salesman Problem (TSP) asks: given n cities with pairwise distances, find the shortest route that visits every city exactly once and returns to the starting city. It's NP-Hard (optimization) and NP-Complete (decision version). Exact algorithms: (1) Brute force: try all (n-1)!/2 permutations. O(n!) — only feasible for n ≤ 12; (2) Held-Karp DP: O(2ⁿ × n²) — bitmask DP where dp[mask][i] = minimum cost to visit all cities in mask ending at city i. Feasible for n ≤ 20-25; (3) Branch and bound: prune search tree; better in practice for n ≤ 50. Approximation algorithms: (1) Greedy nearest neighbor: always go to the nearest unvisited city. O(n²) but solution can be up to O(log n) worse than optimal; (2) Christofides algorithm: guaranteed 3/2-approximation for metric TSP — find MST, find minimum perfect matching on odd-degree vertices, find Eulerian circuit, shortcut repeated vertices. O(n³); (3) Lin-Kernighan heuristic: local search — the best practical heuristic for large instances. Applications: logistics routing, circuit board drilling, DNA sequencing, genome assembly. TSP is the canonical example of a combinatorial optimization problem — studying it reveals techniques applicable to many hard optimization problems.

Open this question on its own page
12

What is the difference between greedy algorithms and dynamic programming?

Both solve optimization problems, but they differ in when and how they make choices: Greedy algorithms: make the locally optimal choice at each step, hoping it leads to a globally optimal solution. Never reconsider previous choices. Only correct when the problem has the greedy choice property — a locally optimal choice is part of some globally optimal solution — AND optimal substructure. Time: usually O(n log n) or O(n). Examples: Dijkstra's (always expand closest node), Prim's/Kruskal's MST, Huffman encoding, Activity Selection, Fractional Knapsack. Greedy fails on: 0/1 Knapsack (fractional greedy doesn't work for integers), Coin Change with arbitrary denominations. Dynamic Programming: considers all possible choices and picks the best, using previously computed subproblems. Correct when the problem has overlapping subproblems + optimal substructure. Time: often O(n²) or O(n × capacity). Examples: 0/1 Knapsack, LCS, LIS, Edit Distance, Matrix Chain Multiplication. How to know which to use: (1) Try greedy — if you can prove it's always optimal (exchange argument), use it; (2) If greedy fails (can construct a counterexample), try DP; (3) Some problems need both (Dijkstra is greedy but relies on DP-like distance update). DP is a superset of greedy in a sense — greedy makes one choice, DP considers all.

Open this question on its own page
13

What is Huffman encoding?

Huffman encoding is a lossless data compression algorithm that assigns shorter binary codes to more frequent characters and longer codes to less frequent ones, achieving optimal prefix-free encoding. Algorithm: (1) Build a frequency table for each character; (2) Create leaf nodes for each character and insert into a min-priority queue ordered by frequency; (3) While queue has more than 1 node: extract two minimum-frequency nodes, create a new internal node with these as children and frequency = sum of their frequencies, insert the new node back; (4) The last remaining node is the root of the Huffman tree; (5) Assign bits: left branch = 0, right branch = 1; each character's code is its path from root to leaf. Properties: prefix-free (no code is prefix of another — enables unambiguous decoding); optimal (no other prefix-free code achieves shorter average length). Average code length = sum(frequency[i] × depth[i]). Time: O(n log n). Applications: (1) ZIP, GZIP, DEFLATE compression; (2) JPEG, PNG (partial use); (3) MP3 audio; (4) Arithmetic coding (superior alternative); (5) Any lossless compression. Greedy proof: at each step, merging two lowest-frequency nodes is optimal (exchange argument). Huffman coding is the canonical example of a greedy algorithm with an elegant proof of optimality.

Open this question on its own page
14

What is the difference between DFS recursive and iterative implementations and their implications?

DFS can be implemented recursively or iteratively, with important differences: Recursive DFS: uses the implicit call stack; code is clean and natural; each recursive call stores local state (current node, iterator position); handles backtracking implicitly. Limitations: (1) Stack overflow for deep graphs (Python default recursion limit ~1000, can be hit for large trees); (2) Hard to pause/resume (generators help). Code: def dfs(node): visited.add(node); for neighbor in graph[node]: if neighbor not in visited: dfs(neighbor). Iterative DFS (explicit stack): push source; while stack: pop, process, push neighbors. Note: this visits in different order than recursive DFS — recursive processes neighbors left-to-right, pushing each then backtracking; iterative pushes all neighbors then pops (LIFO), so rightmost neighbor is visited first. For exact recursive order iteratively: push neighbors in reverse order. Pre-order is naturally iterative; post-order requires marking nodes or using two stacks; in-order is trickier iteratively. Implications: iterative DFS: (1) No stack overflow; (2) Better for very deep graphs; (3) Easier to pause (generators); (4) More explicit control. Recursive DFS: (1) Elegant, concise; (2) Natural for backtracking; (3) The actual call stack IS the DFS stack. For tree problems (depth ≤ few thousand), recursive is fine and cleaner. For graphs that could be deep, iterative or use sys.setrecursionlimit in Python.

Open this question on its own page
15

What is matrix exponentiation and when is it used?

Matrix exponentiation computes Mⁿ (matrix raised to power n) in O(k³ log n) time using repeated squaring (binary exponentiation), where k is the matrix dimension. Binary exponentiation: Mⁿ = M^(n/2) × M^(n/2) if n even; M × M^(n-1) if n odd. Divide n by 2 each step → O(log n) matrix multiplications, each O(k³). Applications: (1) Fibonacci in O(log n): [F(n+1)] = [1 1]^n × [F(1)]. The transformation matrix [[1,1],[1,0]] raised to the n-th power gives Fibonacci numbers. Without this: O(n) DP; with matrix exponentiation: O(log n) — critical for very large n; (2) Linear recurrences in O(log n): any linear recurrence relation (T(n) = aT(n-1) + bT(n-2) + cT(n-3)) can be expressed as matrix multiplication and computed in O(k³ log n); (3) Path counting in graphs: Aⁿ[i][j] = number of paths of length n from vertex i to j; (4) Dynamic programming on graphs with exactly k steps; (5) Number of strings of length n matching a regular expression (via DFA matrix). Matrix exponentiation turns O(n) recurrences into O(log n), which is significant when n can be 10^18 (common in competitive programming) — the difference between an answer in 1 second vs 10^13 seconds.

Open this question on its own page
Back to All Topics 72 questions total