How to use this guide
This is a deep, interview-ready DSA handbook. Each topic: quick definition, intuition, naive example, optimized approach, complexity, pitfalls, interview notes and problems.
- Read intuition first — code second.
- Learn templates — they map directly to interview patterns.
- Practice the problems in Practice Problems section.
Complexity Guide (Big-O quick reference)
Common operation costs — memorize these.
- Array access: O(1)
- Array insert/delete (mid): O(n)
- Linked list insert/delete at head: O(1)
- HashMap lookup/insert (avg): O(1), worst O(n)
- BST balanced operations: O(log n), skewed O(n)
- Heap push/pop: O(log n)
- DFS/BFS: O(V + E)
- Sorting (comparison): O(n log n) average (merge/quick), O(n^2) naive
Arrays — concept & usage
Intuition: contiguous memory, fast indexing, used when size mostly known or random access is needed.
Naive — Find Maximum
int max(int[] arr) {
if (arr == null || arr.length==0) return Integer.MIN_VALUE;
int m = arr[0];
for (int i=1; i m) m = arr[i];
return m;
}
Optimize? — When to optimize
If scanning repeatedly, build prefix/suffix arrays, segment tree, or maintain running aggregates.
In-place rotate (common pattern)
void rotateRight(int[] a, int k) {
k = k % a.length;
reverse(a, 0, a.length-1);
reverse(a, 0, k-1);
reverse(a, k, a.length-1);
}
void reverse(int[] a, int l, int r) {
while (l < r) { int t = a[l]; a[l++] = a[r]; a[r--] = t; }
}
Pitfalls
- Off-by-one on indices
- Null and empty checks
- Beware integer overflow in loops? use l + (r-l)/2 in binary search
Linked Lists
Intuition: nodes with pointers — flexible size, cheap insert/delete at ends.
Naive — Reverse (iterative)
class Node { int v; Node next; Node(int x){v=x;} }
Node reverse(Node head) {
Node prev=null, cur=head;
while(cur!=null){
Node tmp=cur.next; cur.next=prev; prev=cur; cur=tmp;
}
return prev;
}
Find middle (two-pointer)
Node middle(Node head) {
Node slow=head, fast=head;
while(fast!=null && fast.next!=null){ slow=slow.next; fast=fast.next.next; }
return slow;
}
Use cases
- Implement stacks/queues, adjacency lists
Stack (LIFO)
Use for recursion emulation, backtracking, expression parsing.
Naive — Balanced parentheses
boolean isBalanced(String s){
java.util.Stack<Character> st = new java.util.Stack<>();
for(char c : s.toCharArray()){
if(c=='(') st.push(c);
else if(c==')'){ if(st.isEmpty()) return false; st.pop(); }
}
return st.isEmpty();
}
Template: iterative DFS
void dfsIter(int src, List<Integer>[] adj) {
boolean[] vis = new boolean[adj.length];
java.util.Stack<Integer> st = new java.util.Stack<>();
st.push(src);
while(!st.isEmpty()){
int u = st.pop();
if(vis[u]) continue;
vis[u]=true;
// process u
for(int v : adj[u]) if(!vis[v]) st.push(v);
}
}
Queue (FIFO)
Great for BFS, producer-consumer. Use circular buffer or LinkedList.
BFS template
void bfs(int s, List<Integer>[] adj) {
boolean[] vis = new boolean[adj.length];
java.util.Queue<Integer> q = new java.util.LinkedList<>();
q.add(s); vis[s]=true;
while(!q.isEmpty()){
int u=q.remove();
// process u
for(int v: adj[u]) if(!vis[v]) { vis[v]=true; q.add(v); }
}
}
Hashing / HashMap
Average O(1) mapping. Good for frequency, dedup, caching.
Counting frequencies
Map<Integer,Integer> freq(int[] a){
Map<Integer,Integer> m = new java.util.HashMap<>();
for(int x: a) m.put(x, m.getOrDefault(x,0)+1);
return m;
}
Collision & worst-case
Use good hash function; Java's built-in handles most primitives/strings well. For adversarial data, consider tree bins (Java 8+) or alternative maps.
Strings — patterns & common tasks
String problems often reduce to sliding-window, hashing (rolling hash), KMP, or two pointers.
Naive — substring search
int indexOf(String s, String p) {
for(int i=0;i+ p.length()<=s.length(); i++){
if(s.substring(i, i+p.length()).equals(p)) return i;
}
return -1;
}
KMP outline (use for guaranteed O(n))
Precompute lps table, then single pass.
Sorting — naive → efficient
Remember: comparison sorts cannot beat O(n log n) average using comparisons.
Bubble Sort (naive)
void bubbleSort(int[] a){
int n=a.length;
for(int i=0;ia[j+1]){ int t=a[j]; a[j]=a[j+1]; a[j+1]=t; }
}
}
}
Merge Sort (stable, O(n log n))
void mergeSort(int[] a, int l, int r){
if(l>=r) return;
int m=(l+r)/2;
mergeSort(a, l, m);
mergeSort(a, m+1, r);
merge(a, l, m, r);
}
// merge uses temp array
QuickSort (average O(n log n), worst O(n^2))
Randomized pivot avoids worst-case on sorted inputs.
Searching
Binary search template (iterative)
int binarySearch(int[] a, int target){
int l=0, r=a.length-1;
while(l<=r){
int m = l + (r-l)/2;
if(a[m]==target) return m;
if(a[m] < target) l = m+1; else r = m-1;
}
return -1;
}
Trees
Hierarchical structures. Use recursion for traversals.
DFS traversals — recursive templates
void inorder(Node root){
if(root==null) return;
inorder(root.left);
visit(root);
inorder(root.right);
}
Tree problems: lowest common ancestor (LCA)
Use parent+depth lifting (binary lifting) for repeated LCA queries; use recursion for single queries.
Binary Search Tree
Properties: left < node < right. Balanced BSTs (AVL, Red-Black) guarantee O(log n).
Search/insert (naive)
Node insert(Node root, int key){
if(root==null) return new Node(key);
if(key < root.val) root.left = insert(root.left, key);
else root.right = insert(root.right, key);
return root;
}
Heap / Priority Queue
Binary heap supports push/pop in O(log n). Java: PriorityQueue.
Use-case: kth smallest
int kthSmallest(int[] a, int k){
PriorityQueue<Integer> pq = new PriorityQueue<>();
for(int x: a) pq.add(x);
for(int i=1;i
Graphs
Representations: adjacency list (preferred), adjacency matrix (dense). Problems: connectivity, shortest path, cycles, topological order.
BFS & shortest unweighted
int[] distances(List<Integer>[] adj, int s){
int n=adj.length; int[] dist = new int[n]; Arrays.fill(dist, -1);
Queue<Integer> q = new LinkedList<>(); q.add(s); dist[s]=0;
while(!q.isEmpty()){
int u=q.remove();
for(int v: adj[u]) if(dist[v]==-1){ dist[v]=dist[u]+1; q.add(v); }
}
return dist;
}
Dijkstra (weighted, non-negative)
void dijkstra(int src, List<Pair>[] adj) {
int n=adj.length; long[] dist = new long[n]; Arrays.fill(dist, Long.MAX_VALUE);
PriorityQueue<Pair> pq = new PriorityQueue<>((a,b)->Long.compare(a.w,b.w));
dist[src]=0; pq.add(new Pair(src,0));
while(!pq.isEmpty()){
Pair p=pq.poll(); int u=p.v;
if(p.w>dist[u]) continue;
for(Pair e: adj[u]){
if(dist[e.v] > dist[u] + e.w){
dist[e.v] = dist[u] + e.w;
pq.add(new Pair(e.v, dist[e.v]));
}
}
}
}
Topological Sort (DAG)
List<Integer> topo(int n, List<Integer>[] adj){
int[] indeg = new int[n]; for(int u=0;u
Dynamic Programming
Two approaches: memoization (top-down) and tabulation (bottom-up). Identify overlapping subproblems and optimal substructure.
Fibonacci — naive vs memo vs tabulation
int fib(int n){ if(n<=1) return n; return fib(n-1) + fib(n-2); } // exponential
int fibMemo(int n, int[] memo){
if(n<=1) return n;
if(memo[n]!=-1) return memo[n];
return memo[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo);
}
Knapsack (0/1) — DP table outline
// dp[i][w] = max value using first i items with capacity w for(i=1..n) for(w=0..W) dp[i][w] = max(dp[i-1][w], val[i] + dp[i-1][w-weight[i]]);
Backtracking
Explore choices, backtrack on failure. Use for permutations, combinations, N-Queens, Sudoku.
N-Queens outline (place row-by-row)
void solve(int r, int n, boolean[] col, boolean[] diag1, boolean[] diag2){
if(r==n){ /* found */ return; }
for(int c=0;c
Greedy
Take local optimal choice; proof often by exchange argument. Examples: activity selection, Huffman coding, fractional knapsack.
Two Pointers
Use two indices to scan from ends or to maintain window boundaries. Reduce O(n^2) → O(n).
Two-sum (sorted)
int[] twoSumSorted(int[] a, int t){
int l=0, r=a.length-1;
while(l
Sliding Window
Use variable window [l..r] to maintain invariant (sum, counts). Classic: longest substring w/o repeating chars, subarray sum equals K (positives).
Longest substring without repeating
int longestUnique(String s){
int[] last = new int[256]; Arrays.fill(last, -1);
int l=0, best=0;
for(int r=0;r
Bit Manipulation
Useful for parity, subsets, masks. Know: &, |, ^, ~, <<, >>, bit tricks (x & -x gives lowest set bit).
Count bits (Brian Kernighan)
int popcount(int x){
int cnt=0;
while(x!=0){ x &= (x-1); cnt++; }
return cnt;
}
Union-Find (Disjoint Set)
Maintain connected components with union by rank & path compression: near O(1) amortized.
int[] parent, rank;
int find(int x){ return parent[x]==x ? x : (parent[x]=find(parent[x])); }
void union(int a, int b){
a=find(a); b=find(b);
if(a==b) return;
if(rank[a]
Tries (Prefix Trees)
Fast prefix search and word validation. Memory-heavy but fast lookups for strings.
class TrieNode { TrieNode[] next = new TrieNode[26]; boolean end=false; }
void insert(TrieNode root, String s){
TrieNode cur=root;
for(char c: s.toCharArray()){
int i = c-'a'; if(cur.next[i]==null) cur.next[i]=new TrieNode();
cur = cur.next[i];
}
cur.end=true;
}
Segment Tree & Fenwick (Binary Indexed Tree)
Range queries & updates: Fenwick for prefix sums (simple), Segment tree for range queries combining arbitrary assoc ops.
class Fenwick {
int n; long[] bit;
Fenwick(int n){ this.n=n; bit=new long[n+1]; }
void add(int i, long val){ for(; i<=n; i += i&-i) bit[i]+=val; }
long sum(int i){ long s=0; for(; i>0; i -= i&-i) s+=bit[i]; return s; }
}
Problem Patterns
- Sliding window — continuous subarray/string problems
- Two pointers — sorted pair problems, remove duplicates
- Prefix sum — subarray sums, range queries
- Hashing — counting, dedupe, two-sum (unsorted)
- DFS/BFS — traverse graphs/trees
- DP patterns — knapsack, LIS, partition
- Greedy — scheduling, interval selection
Coding Templates (copy & adapt)
Binary Search (lower_bound style)
int lowerBound(int[] a, int target){
int l=0, r=a.length;
while(l
DFS recursive template (graphs)
void dfs(int u, List<Integer>[] adj, boolean[] vis){
vis[u]=true;
// process u
for(int v: adj[u]) if(!vis[v]) dfs(v, adj, vis);
}
Practice Problems (Easy → Hard)
- Easy: Two Sum (array), Reverse Linked List, Merge Two Sorted Lists, Valid Parentheses
- Medium: Longest Substring Without Repeating Characters, Group Anagrams, Lowest Common Ancestor
- Hard: Word Ladder II, N-Queens II, Median of Two Sorted Arrays, Graph longest path (DAG)
Interview strategy
- Explain thought process clearly; pick correct data structure and complexity target.
- Start with naive solution, then optimize.
- Write clean code, mention edge cases & complexity.