Java DSA Interview Prep Master Guide
Java DSA Interview Prep Master Guide
Core Java Theory & Deep-Dive Q&A + Data Structures & Algorithms
Covering the technical / coding-round style asked across:
Tier-1: Google · Microsoft · Amazon · Meta-style product companies
Tier-2 / Tier-3: IBM · TCS · Infosys · Wipro · Cognizant · Capgemini and similar
service companies
PART B: DSA
1. Complexity Analysis & Big-O Foundations
2. Arrays & Strings
3. Linked Lists
4. Stacks & Queues
5. Trees — Binary Trees, BSTs, Traversals & Balancing
6. Graphs — Representation, BFS/DFS, Shortest Paths
7. Sorting & Searching Algorithms
8. Recursion & Backtracking
9. Dynamic Programming
10. Greedy Algorithms & Hashing
Primitive types: byte(1B), short(2B), int(4B), long(8B), float(4B), double(8B), char(2B, unsigned, holds Unicode),
boolean(JVM-dependent, conceptually 1 bit). Everything else (String, arrays, custom classes) is a reference
type stored on the heap, with the reference itself living on the stack.
Default values matter for interviews: instance/static fields get defaults (0, 0.0, false, null) automatically; local
variables do NOT — the compiler forces you to initialize them before use ('variable might not have been
initialized').
Widening (implicit) conversion goes byte→short→int→long→float→double (char is a side branch that widens to
int). Narrowing conversion is explicit and can lose data or overflow silently — this is a favorite trick-question
area across all companies, from Amazon to TCS.
Q2. What happens when you print a local variable that was never assigned?
Compile-time error: 'variable X might not have been initialized.' Unlike instance/static fields, local variables get
no default value.
Q5. Why does 0.1 + 0.2 != 0.3 in Java (or most languages)?
float/double use IEEE-754 binary floating point, which cannot exactly represent decimal fractions like 0.1. The
stored values are approximations, so arithmetic on them yields tiny rounding errors. Use BigDecimal for exact
decimal math.
Q6. What is the size and range of char in Java, and why is it unsigned?
char is 2 bytes (0 to 65535), used to represent a single UTF-16 code unit. It's unsigned because it's meant to
hold a Unicode code point, not a signed number — this differs from C/C++ where char is signed and 1 byte.
Q9. What's the difference between float f = 1.5; and float f = 1.5f;?
1.5 is a double literal by default, so assigning it to a float without the 'f' suffix (or an explicit cast) is a compile
error — you're narrowing double to float implicitly. 1.5f is a float literal, which compiles fine.
The trap most candidates fall into: assuming x = x++ increments x. It does NOT change x's final value, because
the old value of x is saved before the increment happens, and that saved (unincremented) value is what gets
assigned back.
Short-circuit operators (&&, ||) evaluate the right operand only if necessary; the non-short-circuit versions (&, |)
always evaluate both sides. This matters when the right side has side effects (e.g., a function call, or an
increment).
Operator precedence surprises: bitwise operators have LOWER precedence than relational operators, and the
ternary operator is right-associative. Combined with ++/--, these produce classic 'predict the output' interview
Q4. Why does x = x++ not increment x, but x++; (as a standalone statement) does?
In x = x++, the assignment overwrites x with the saved pre-increment value, masking the increment. As a
standalone statement x++;, there's no assignment competing with it, so the incremented value simply becomes
x's new value with nothing overwriting it.
Q6. Difference between & and && in a condition like (a != null & [Link]())?
& always evaluates both operands, so if a is null this throws NullPointerException. && short-circuits — if a != null
is false, isValid() is never called, avoiding the NPE. Always prefer && / || in guard conditions.
Q7. What does (5 & 3) == 1 evaluate as, given == binds tighter than &?
This is false-if-misjudged: relational (==) has HIGHER precedence than bitwise (&) in Java, so this parses as 5 &
(3==1) → 5 & false, which is a compile error (can't mix int and boolean with &). You must write (5 & 3) == 1
explicitly with parentheses.
Q9. Is the ternary operator ?: left- or right-associative? Give an example that matters.
Right-associative. a ? b : c ? d : e parses as a ? b : (c ? d : e), which matters when chaining nested ternaries — a
common source of subtle bugs in condensed one-liners.
Q12. What does the instanceof operator do, and can it be used with null?
Q13. Compound assignment: byte b = 10; b += 5; — does this compile, and why does byte b = b + 5;
not?
b += 5; compiles because compound assignment operators implicitly cast the result back to the target type. byte
b = b + 5; does NOT compile because b+5 promotes to int, and assigning an int to byte needs an explicit cast —
this asymmetry is a classic interview question.
try-with-resources (Java 7+) auto-closes any resource implementing AutoCloseable, replacing verbose finally
blocks and guaranteeing close() runs even on an exception — a frequently asked 'why is this better' question.
finally always runs except for [Link]() or JVM crash — even if try or catch has a return statement. If finally
itself has a return, it silently overrides any return/exception from try/catch, which is considered a code smell.
Q3. If both try and finally have return statements, which one wins?
The return in finally wins — it silently discards whatever try or catch was about to return (or even an exception
being propagated), which is why returning from finally is considered bad practice.
Q4. Does finally run if the try block has a return statement?
Yes. finally always executes before the method actually returns, unless the JVM exits ([Link]()) or crashes.
Q6. Checked vs unchecked exceptions — give one example of each and explain the compiler
difference.
Checked (e.g., IOException) extends Exception but not RuntimeException, and the compiler forces you to either
catch it or declare it with throws. Unchecked (e.g., NullPointerException, ArithmeticException) extends
RuntimeException and the compiler doesn't enforce handling.
Q7. Can you catch multiple exception types in one catch block?
Yes, using the pipe syntax: catch (IOException | SQLException e) { ... }. The exceptions in that list must not be
related by subclassing (no catching a parent and child together).
Q9. What happens if you don't catch a checked exception and don't declare throws?
Compile-time error: 'unreported exception must be caught or declared to be thrown.'
Overloading = same method name, different parameter list, resolved at COMPILE time (static/early binding).
Overriding = subclass redefines a superclass method with the identical signature, resolved at RUNTIME based
on the actual object type (dynamic/late binding) — this is the mechanism behind polymorphism.
Java doesn't support multiple inheritance of classes (to avoid the diamond problem) but does support it through
interfaces, since Java 8 interfaces can have default methods — if two interfaces provide conflicting default
methods, the implementing class MUST override the method to resolve the ambiguity, or it's a compile error.
Q3. Can you overload a method by changing only the return type?
No. Return type alone is not part of the method signature for overload resolution — you'll get a compile error
'method already defined' if only the return type differs.
Q5. What is the diamond problem, and how does Java avoid/handle it?
Q6. What is the order of constructor calls in a class hierarchy when you instantiate a subclass?
The superclass constructor always runs first (implicitly via super() if not written explicitly), then the subclass
constructor body — this happens all the way up the hierarchy to Object, then unwinds back down.
Q9. Abstract class vs interface — when would you choose one over the other (post Java 8)?
Abstract class: can hold state (instance fields), constructors, and a mix of implemented/abstract methods — use
it when subclasses share common state/behavior ('is-a' with shared implementation). Interface: purely a contract
of capability (can now include default/static methods but no instance state) — use it for unrelated classes to
share a capability ('can-do'), and because a class can implement multiple interfaces but extend only one class.
Q10. What is polymorphism, and what are its two forms in Java?
The ability of an object to take many forms. Compile-time (static) polymorphism = method overloading, resolved
by the compiler. Runtime (dynamic) polymorphism = method overriding, resolved by the JVM at runtime using
the actual object's type (virtual method dispatch).
Q11. If a subclass object is referenced by a superclass variable, and both define a field with the same
name, which field is accessed?
Field access is resolved at compile time based on the REFERENCE type, not the object's runtime type (fields
are not polymorphic like methods) — so the superclass's field is accessed. This differs from method calls, which
use the runtime type.
Q12. Can you call an overridden method from a constructor? Why is this dangerous?
Yes, syntactically, but it's dangerous: if the subclass overrides that method and relies on fields initialized in the
subclass constructor, the overridden version runs BEFORE those subclass fields are initialized (since superclass
constructor runs first), leading to subtle bugs (e.g., NPEs on fields that look 'always initialized').
Q13. What is the 'super' keyword used for? Give three uses.
(1) super() calls the immediate superclass's constructor, must be the first statement if used explicitly. (2)
[Link]() explicitly invokes the superclass's version of an overridden method. (3) [Link] accesses a
superclass field that's shadowed by a subclass field of the same name.
StringBuilder is mutable and NOT thread-safe (fast, use in single-threaded code, e.g. loops building large
strings). StringBuffer is mutable and thread-safe (methods are synchronized, slightly slower). Both avoid the
overhead of creating a new String object on every append, unlike naive String concatenation in a loop.
Q5. String s = "a" + "b"; — is this pooled, and why does it matter that both are literals?
Yes. The compiler performs constant folding on literal concatenation at COMPILE time, producing "ab" as a
single pooled literal — equivalent to writing "ab" directly. This differs from concatenating with a variable (String s
= a + "b";), which happens at RUNTIME via StringBuilder internally and does NOT get pooled.
Q6. Why is doing String result = ""; for(...) { result += item; } bad in a loop with many iterations?
Each += creates a brand-new String object (since String is immutable) and copies the old content plus the new
piece — for N iterations this is roughly O(N^2) total character copying. Use [Link]() inside the
loop instead, which mutates an internal char array in amortized O(1) per append.
A common design question: 'when would you create a custom exception?' — when you need to convey
domain-specific failure information (e.g., InsufficientFundsException) that generic exceptions can't express, or
to let calling code catch/handle your specific failure type distinctly from unrelated errors.
Q5. Can a finally block suppress an exception thrown from try? How?
Yes, if finally itself throws an exception (or has a return), it replaces/suppresses whatever exception was
propagating from try/catch — the original exception is lost unless explicitly captured, which is one reason to
avoid throwing or returning from finally.
7. Collections Framework
THEORY NOTES
Core interface hierarchy: Collection → List (ordered, duplicates allowed: ArrayList, LinkedList, Vector), Set (no
duplicates: HashSet, LinkedHashSet, TreeSet), Queue/Deque (PriorityQueue, ArrayDeque). Map is separate
(not a Collection): HashMap, LinkedHashMap, TreeMap, Hashtable.
HashMap internals (very frequently asked at Amazon/Google/Microsoft level): backed by an array of 'buckets';
each key's hashCode() is used to compute a bucket index; collisions within a bucket are handled via a linked list
(converted to a red-black tree if a bucket gets 8+ entries, since Java 8, for O(log n) worst case instead of O(n)).
Default capacity 16, load factor 0.75, resizes (doubles) when size exceeds capacity*loadFactor.
Q2. When would you actually choose LinkedList over ArrayList in practice?
Rarely in modern Java — ArrayDeque usually beats LinkedList for queue/stack use cases due to better cache
locality. LinkedList is justified when you frequently insert/delete at both ends or in the middle AND you already
hold iterator references to the insertion point, avoiding traversal cost.
Q3. How does HashMap handle collisions, and what changed in Java 8?
Before Java 8: colliding entries in the same bucket formed a singly linked list, so worst-case lookup was O(n) if
all keys collided. Since Java 8: if a bucket's list grows to 8+ entries (and the table is large enough), it's converted
into a red-black tree, making worst-case lookup O(log n).
Q5. Why must a class override both equals() and hashCode() together for correct HashMap/HashSet
behavior?
HashMap uses hashCode() to locate the bucket, then equals() to find the exact matching key within that bucket
(handling collisions). If you override equals() but not hashCode(), two 'equal' objects can produce different hash
codes and land in different buckets — the map won't recognize them as duplicates, breaking the
equals/hashCode contract.
Q10. Can you modify a List while iterating over it with a for-each loop? What happens?
No — this throws ConcurrentModificationException, because the enhanced for-loop uses an Iterator internally,
and structurally modifying the list (add/remove) outside the iterator's own remove() method invalidates it via a
'fail-fast' modCount check. Use [Link](), or a ListIterator, or collect items to remove separately.
Q11. What is the difference between fail-fast and fail-safe iterators? Give an example of each.
Fail-fast iterators (ArrayList, HashMap's default) detect concurrent structural modification and throw
ConcurrentModificationException immediately. Fail-safe iterators (CopyOnWriteArrayList, ConcurrentHashMap)
operate on a snapshot or tolerate concurrent changes without throwing, though they may not reflect the very
latest modifications during iteration.
Q12. PriorityQueue — what ordering does it use by default, and how do you customize it?
By default, min-heap ordering based on natural ordering (Comparable) — the smallest element is always at the
head. Pass a custom Comparator to the constructor to get max-heap behavior or any other ordering.
A Thread can be created by extending Thread (overriding run()) or implementing Runnable (passed to a
Thread) — implementing Runnable is generally preferred since Java doesn't support multiple inheritance, so it
leaves your class free to extend something else, and it better separates 'the task' from 'the thread executing it'.
synchronized provides both mutual exclusion (only one thread in the critical section at a time) AND a memory
visibility guarantee (changes made inside a synchronized block by one thread become visible to other threads
that later synchronize on the same lock). volatile provides ONLY the visibility guarantee, not atomicity/mutual
exclusion — this distinction is a very common trick question.
Q3. What does synchronized guarantee, precisely — two things, not one.
(1) Mutual exclusion: only one thread can hold the lock on a given object/class at a time, so only one thread
executes the synchronized block/method concurrently. (2) Visibility: it establishes a happens-before relationship,
so writes made by a thread before releasing the lock are guaranteed visible to the next thread that acquires the
same lock.
Q8. What is the Executor framework, and why is it preferred over manually creating Thread objects?
A higher-level abstraction (ExecutorService, thread pools via [Link]() etc.) that
manages a reusable pool of worker threads, queues submitted tasks, and separates task submission from
thread lifecycle management — avoiding the overhead/unpredictability of creating a new OS thread per task.
Q11. What does ThreadLocal do, and when would you use it?
Gives each thread its own independent copy of a variable, isolated from other threads (no sharing, no
synchronization needed). Common use: per-thread state like a database connection, a SimpleDateFormat
instance (not thread-safe by itself), or a per-request user context in a web server handling concurrent requests.
Garbage Collection reclaims heap memory occupied by objects with no reachable references. Java uses a
generational hypothesis: most objects die young, so GC focuses effort on the (small, fast-to-scan) Young
Generation via frequent 'Minor GC', promoting long-surviving objects to Old Gen, which is collected less often
via slower 'Major/Full GC'.
StackOverflowError happens when the call stack exceeds its size limit — classically from unbounded/infinite
recursion (missing or wrong base case). OutOfMemoryError happens when the heap can't allocate more
memory and the GC can't free enough — from memory leaks (unintentionally retained references) or genuinely
needing more heap than allocated.
Q2. Why is the stack divided per-thread but the heap shared?
Each thread executes its own sequence of method calls, so it needs its own independent call frames/local
variables (stack) to avoid interference. Objects on the heap, however, can be shared and passed between
threads, so a single shared heap allows that sharing — this is also exactly why heap access needs
synchronization but stack-local variables generally don't.
Q4. What causes an OutOfMemoryError, and name two common real-world causes.
The heap is exhausted and GC cannot reclaim enough space. Common causes: (1) memory leaks — objects
unintentionally kept reachable (e.g., growing static collections that are never cleared, unclosed resources,
listener registrations never removed), (2) genuinely processing data larger than the configured heap allows.
Q9. What is Metaspace, and how does it differ from the old PermGen?
Metaspace (Java 8+) stores class metadata (like PermGen did), but unlike PermGen, it's allocated from native
(off-heap) memory and grows dynamically by default rather than having a small fixed size — this largely
eliminated the once-common 'PermGen space' OutOfMemoryError from too many loaded classes.
Streams provide a declarative, pipeline style for processing collections: a source, zero or more intermediate
operations (map, filter, sorted — these are LAZY, they don't execute until a terminal operation is invoked), and
exactly one terminal operation (collect, forEach, reduce, count) which triggers actual execution.
Optional<T> is a container object that may or may not hold a non-null value, designed to make the possibility of
'no value' explicit in a method's return type, reducing accidental NullPointerExceptions and forcing callers to
consciously handle the empty case.
Q3. Are Stream intermediate operations like map() and filter() lazy or eager? Why does it matter?
Lazy — they just build up a pipeline description and don't process any elements until a terminal operation (like
collect() or forEach()) is called. This matters because it allows short-circuiting (e.g., findFirst() can stop early
without processing the whole source) and avoids unnecessary intermediate collection allocation.
Q6. What is [Link]() vs [Link]() — and why does the difference matter for
performance?
orElse(defaultValue) always evaluates/constructs the default value argument eagerly, even if the Optional has a
value present and it will be discarded. orElseGet(supplier) only invokes the supplier lazily if the Optional is
actually empty — preferred when constructing the default is expensive (e.g., a DB call), since orElse would
waste that work unconditionally.
Q7. Why shouldn't you call [Link]() without checking isPresent() first?
If the Optional is empty, get() throws NoSuchElementException — calling it blindly reintroduces exactly the kind
of unchecked failure Optional was designed to prevent. Prefer orElse/orElseGet/orElseThrow/ifPresent/map
instead of a raw isPresent()+get() pattern.
Q9. What's the difference between Predicate, Function, Supplier, and Consumer?
Predicate<T>: takes T, returns boolean (a test/condition). Function<T,R>: takes T, returns R (a transformation).
Supplier<T>: takes nothing, returns T (a factory/source). Consumer<T>: takes T, returns void (performs a
side-effecting action).
Common complexity classes from best to worst: O(1) constant, O(log n) logarithmic, O(n) linear, O(n log n)
linearithmic, O(n^2) quadratic, O(2^n) exponential, O(n!) factorial. Know at least one canonical algorithm
example for each.
Amortized analysis matters for structures like ArrayList/dynamic arrays: a single append is occasionally O(n)
(when resizing), but AVERAGED over a sequence of n appends, each is O(1) amortized — interviewers ask
this specifically to check you understand the difference between worst-case-per-operation and amortized cost.
Q2. Give one canonical algorithm for each: O(log n), O(n log n), O(n^2), O(2^n).
O(log n): binary search. O(n log n): merge sort / heap sort / efficient comparison-based sorting in general.
O(n^2): bubble sort / naive nested-loop pair checking. O(2^n): naive recursive Fibonacci without memoization, or
generating all subsets of a set.
Q3. Why is [Link]() considered O(1) amortized, even though resizing is O(n)?
Resizing (doubling capacity) happens rarely — roughly every time the array fills up — and each resize costs
O(n) to copy elements, but you 'pay' for that cost gradually across all the O(1) appends that happened since the
last resize. Summed over n appends, the total work is O(n), so the AVERAGE cost per append is O(1), even
though occasional individual appends are more expensive.
Q4. What's the difference between time complexity and space complexity?
Time complexity measures how the number of operations (roughly, running time) grows with input size. Space
complexity measures how much extra memory the algorithm uses (beyond the input itself) as input size grows —
including auxiliary data structures and recursion call-stack depth.
Q6. What is the time complexity of recursive Fibonacci without memoization, and why?
O(2^n) — each call to fib(n) spawns two more calls (fib(n-1) and fib(n-2)), creating a binary recursion tree of
roughly 2^n nodes, with massive redundant recomputation of the same subproblems.
Q7. How does memoization change recursive Fibonacci's complexity, and why?
Q8. What is the space complexity of an algorithm that uses O(n) recursion depth but no other data
structures?
O(n) — even without any explicit arrays/maps, each recursive call adds a frame to the call stack, and with depth
n, that's O(n) stack space, which counts toward space complexity.
Strings in most languages (Java included) are effectively char arrays under the hood for algorithmic purposes
even though the Java String object itself is immutable — most string algorithm questions (reverse, palindrome
check, anagram check, substring search) reduce to array/two-pointer techniques.
Know these patterns cold: two-pointer (opposite ends closing in, e.g., reverse array, two-sum on sorted array,
container-with-most-water), sliding window (variable or fixed size, e.g., longest substring without repeating
characters, max sum subarray of size k), and prefix sums (precompute cumulative sums for O(1) range-sum
queries).
Q2. How do you check if a string is a palindrome, and what's the complexity?
Two pointers from both ends moving inward, comparing characters at each step; mismatch means not a
palindrome. O(n) time, O(1) extra space (ignoring the string itself).
Q3. How do you detect if two strings are anagrams of each other?
Either (a) sort both strings and compare (O(n log n)), or (b) count character frequencies using a fixed-size array
(26 for lowercase English) or a HashMap, then compare counts — O(n) time, O(1) space for a bounded
alphabet, O(k) for a HashMap of distinct chars.
Q4. What is the sliding window technique, and when do you use it?
A technique for problems involving a contiguous subarray/substring, where you maintain a 'window' (defined by
two pointers) that expands and contracts based on a condition, avoiding recomputation from scratch for every
possible window — turning an O(n^2) or O(n^3) brute force into O(n). Used for problems like 'longest substring
without repeating characters', 'maximum sum subarray of size k', 'smallest subarray with sum >= target'.
Q5. How would you find the maximum sum subarray of size k in an array of size n?
Sliding window: compute the sum of the first k elements, then slide the window one step at a time by subtracting
the element leaving the window and adding the element entering it, tracking the max sum seen. O(n) time
instead of the brute force O(n*k).
Q8. How do you rotate an array by k positions in-place, in O(n) time and O(1) space?
Reverse the whole array, then reverse the first k elements, then reverse the remaining n-k elements (for a right
rotation) — three reversals achieve the rotation without extra space, each reversal being O(n), total still O(n).
Q9. What is a prefix sum array, and what problem class does it solve efficiently?
An array where prefix[i] = sum of all elements from index 0 to i-1 of the original array. It lets you answer any 'sum
of range [l, r]' query in O(1) time (prefix[r+1] - prefix[l]) after an O(n) one-time preprocessing step, instead of O(n)
per query with a naive approach — ideal when you have many range-sum queries on a static array.
Q10. How would you find all pairs in an array that sum to a target value?
With a sorted array: two pointers from both ends, moving inward based on whether the current sum is too high or
too low — O(n log n) for the sort plus O(n) scan. Without sorting: a HashSet, iterating once and for each element
checking if (target - element) has already been seen — O(n) time, O(n) space.
Q11. Given a string, how do you find the longest substring without repeating characters?
Sliding window with a HashMap/HashSet tracking characters currently in the window; expand the right pointer,
and whenever a repeated character is found, move the left pointer forward past its previous occurrence. O(n)
time, O(min(n, alphabet size)) space.
3. Linked Lists
THEORY NOTES
A singly linked list node holds data plus a reference to the next node; traversal is O(n) but insertion/deletion at a
known position is O(1) once you have the node reference (no shifting like arrays). Doubly linked lists add a
'prev' reference, enabling O(1) backward traversal and easier deletion.
Fast/slow pointer (Floyd's Tortoise and Hare) is THE canonical linked-list technique: move one pointer one step
and another two steps at a time — used for cycle detection, finding the middle node, and finding the start of a
cycle.
Reversing a linked list (iteratively, in O(1) space) is one of the single most commonly asked coding questions
across literally every company tier from TCS to Google, precisely because it tests pointer manipulation
fundamentals cleanly.
Q3. How do you detect a cycle in a linked list without extra space?
Floyd's cycle detection (tortoise and hare): a slow pointer moves 1 step, a fast pointer moves 2 steps; if there's a
cycle, they will eventually meet inside it; if fast reaches null, there's no cycle. O(n) time, O(1) space.
Q4. Once a cycle is detected with Floyd's algorithm, how do you find where the cycle STARTS?
After slow and fast meet inside the cycle, reset one pointer to the head and keep the other at the meeting point;
move both one step at a time — they will meet exactly at the cycle's starting node. This works due to the
mathematical relationship between the distances traveled before and after the first meeting.
Q5. How do you find the middle node of a linked list in one pass?
Slow/fast pointers: slow moves 1 step, fast moves 2 steps; when fast reaches the end (or null), slow is at the
middle. O(n) time, O(1) space, single pass.
Q6. How do you merge two sorted linked lists into one sorted list?
Use a dummy head node and a tail pointer; repeatedly compare the current nodes of both lists, attach the
smaller one to tail, and advance that list's pointer; after one list is exhausted, attach the remainder of the other
list directly. O(n+m) time, O(1) extra space (just rewiring pointers).
Q7. How do you detect if two linked lists intersect, and find the intersection node, in O(n+m) time and
O(1) space?
Compute lengths of both lists (or use a two-pointer trick): advance the longer list's pointer by the length
difference first, then move both pointers together one step at a time — they will meet at the intersection node
(compared by reference, not value), or both reach null if there's no intersection.
Q8. How do you remove the Nth node from the end of a linked list in one pass?
Two pointers, both starting at a dummy head: advance the 'fast' pointer n steps ahead first, then move both
pointers together until fast reaches the last node; at that point, 'slow' is right before the node to remove, so
[Link] = [Link] removes it. O(n) time, one pass, O(1) space.
Queue: FIFO (first-in-first-out) — enqueue/dequeue O(1) with a proper implementation (like a circular buffer or
linked list; a naive array-based queue that shifts elements on dequeue is O(n), a common beginner mistake).
Classic uses: BFS, task scheduling, buffering.
Monotonic stack (elements kept in increasing or decreasing order as you push/pop) is a very high-value pattern
for 'next greater element', 'largest rectangle in histogram', and stock-span type problems — worth memorizing
the template.
Q3. What is a monotonic stack, and what problem does it solve efficiently?
A stack maintained so its elements are always in strictly increasing or decreasing order; when a new element
would violate that order, you pop elements off before pushing. It efficiently solves 'next greater/smaller element'
type problems in O(n) total (each element is pushed and popped at most once), versus O(n^2) brute force with
nested loops.
Q4. How do you find the 'next greater element' for every element in an array?
Iterate right to left (or left to right with a different technique) maintaining a monotonic decreasing stack of
candidate values; for each new element, pop all stack elements smaller than it (they've found their next greater
element = current element), then push the current element. O(n) time overall despite the nested-looking loop,
since each element is pushed/popped once.
Q5. Why is a naive array-based queue (shifting elements on dequeue) inefficient, and what's the fix?
Removing from the front and shifting every remaining element left is O(n) per dequeue. Fixes: a circular buffer
(track head/tail indices, wrap around with modulo, no shifting), or a linked-list-based queue (O(1) removal from
the front by just updating the head pointer).
Q6. How would you implement a stack that also supports getMin() in O(1) time?
Maintain a second 'min stack' alongside the main stack: whenever you push a value <= the current min stack's
top (or the min stack is empty), also push it onto the min stack; when you pop from the main stack, if the popped
value equals the min stack's top, pop the min stack too. getMin() just peeks the min stack. O(1) for push, pop,
and getMin, O(n) extra space worst case.
BST (Binary Search Tree) property: for every node, all values in the left subtree are smaller, all values in the
right subtree are larger. This gives O(log n) average search/insert/delete for a BALANCED BST, but degrades
to O(n) for a skewed/unbalanced one (e.g., inserting sorted data into a naive BST produces a linked-list-like
structure) — this exact degradation scenario is a very common follow-up question.
Q2. Why does an inorder traversal of a BST always produce sorted output?
By the BST property, at every node, everything in the left subtree is smaller and everything in the right subtree is
larger than the node itself. Inorder visits left-subtree-entirely, then the node, then right-subtree-entirely —
recursively this guarantees strictly increasing order across the whole traversal.
Q4. What is the time complexity of search/insert/delete in a BST, best case vs worst case?
Best/average case (balanced tree): O(log n), because each comparison eliminates roughly half the remaining
nodes. Worst case (degenerate/skewed tree, e.g. built by inserting already-sorted data): O(n), because the tree
degrades into essentially a linked list.
Q5. Why does inserting sorted data into a plain BST produce a bad (O(n) operations) tree?
Each new value is either always greater or always smaller than everything already inserted, so each insertion
just extends a single chain to one side (all right children, or all left children) instead of branching — producing a
tree of height n with no left/right balance, i.e., effectively a linked list.
Q6. How does an AVL tree keep operations at O(log n) worst case?
It maintains a 'balance factor' (height difference between left and right subtrees) of at most 1 for every node; after
any insert/delete, it performs rotations (single or double, i.e., left/right/left-right/right-left) to restore this balance,
which bounds the tree's height to O(log n) at all times.
Q7. What is the height/depth of a balanced binary tree with n nodes, in terms of n?
O(log n) — specifically roughly log2(n) for a fully balanced tree, since each level can hold up to double the nodes
of the previous level.
Q8. How do you find the Lowest Common Ancestor (LCA) of two nodes in a BST?
Start at the root; if both target values are less than the current node, go left; if both are greater, go right; the first
node where the values 'split' (one is <= current, other is >= current, or one equals current) is the LCA. O(h) time
where h is tree height, O(1) space (iterative).
Q9. How do you find the LCA in a general binary tree (not necessarily a BST)?
Recursively search both subtrees for the two target nodes; if a node itself is one of the targets, return it up; if both
left and right recursive calls return non-null (meaning one target was found on each side), the current node is the
LCA; otherwise propagate up whichever side found something. O(n) time, O(h) space for recursion.
Q10. What's the difference between a complete binary tree and a full (proper) binary tree?
Q11. Why do Java's TreeMap/TreeSet use red-black trees rather than plain BSTs?
A plain BST offers no balance guarantee and can degrade to O(n) operations on adversarial or sorted input.
Red-black trees self-balance via rotations and color-based rules, guaranteeing O(log n) worst-case for
search/insert/delete, which is essential for a general-purpose library data structure that must perform predictably
regardless of insertion order.
BFS (queue-based, explores level by level) finds the SHORTEST PATH in an UNWEIGHTED graph, O(V+E)
time. DFS (stack-based or recursive, explores as deep as possible before backtracking) is used for cycle
detection, topological sort, connected components, and path existence — also O(V+E) time.
Dijkstra's algorithm finds shortest paths from a source in a weighted graph with NON-NEGATIVE edge weights,
O((V+E) log V) with a min-heap/priority queue. For graphs with negative weights, Bellman-Ford is needed
instead (O(V*E), also detects negative-weight cycles).
Q2. Why does BFS (not DFS) find the shortest path in an unweighted graph?
BFS explores the graph in increasing distance 'layers' from the source — it fully explores all nodes at distance 1,
then all at distance 2, and so on — so the first time it reaches a target node is guaranteed to be via the shortest
(fewest-edges) path. DFS explores as deep as possible along one path first and offers no such distance-ordering
guarantee.
Q5. What is topological sort, and what precondition must a graph satisfy for it to exist?
Q6. Name two algorithms for topological sort and briefly describe one.
Kahn's algorithm (BFS-based: repeatedly remove nodes with in-degree 0, decrementing the in-degree of their
neighbors) and DFS-based (do a DFS, and prepend each node to the result once ALL its descendants have
been fully explored — i.e., use the reverse of the DFS postorder finishing times).
Q7. Why doesn't Dijkstra's algorithm work correctly with negative edge weights?
Dijkstra greedily finalizes a node's shortest distance as soon as it's popped from the priority queue, assuming no
future path could possibly improve on it — but a negative edge encountered later could still reduce the distance
to an already-finalized node, violating that greedy assumption and producing an incorrect result.
Q8. What algorithm handles negative edge weights, and what extra capability does it provide over
Dijkstra?
Bellman-Ford, O(V*E) time. It correctly computes shortest paths with negative edges (but not negative cycles
reachable from the source), and additionally can DETECT negative-weight cycles (if you can still relax an edge
after V-1 iterations, a negative cycle exists) — something Dijkstra cannot do at all.
Q9. How would you find the number of connected components in an undirected graph?
Run BFS or DFS from any unvisited node, marking all nodes reachable from it as visited (that's one component);
repeat from the next unvisited node; count how many times you start a fresh traversal. O(V+E) time overall.
Quicksort: average O(n log n), worst-case O(n^2) (when the pivot is consistently the smallest/largest element,
e.g., already-sorted input with a naive first-element pivot) — this is exactly why production implementations use
randomized or median-of-three pivot selection to make worst-case behavior astronomically unlikely.
Merge sort: guaranteed O(n log n) in ALL cases (no bad-input worst case like quicksort), but requires O(n) extra
space for merging, whereas quicksort is in-place (O(log n) space for recursion only) — this space/guarantee
tradeoff is a frequently asked comparison question.
Q3. What causes quicksort's worst-case O(n^2), and how do real implementations avoid it?
Consistently picking a 'bad' pivot (the smallest or largest remaining element every time), which happens with a
naive first-element pivot on already-sorted or reverse-sorted input, causing highly unbalanced partitions (one
side has n-1 elements) at every level, giving O(n^2) instead of the balanced O(n log n). Fix: randomized pivot
selection or median-of-three, making the worst case extremely unlikely in practice.
Q4. How does binary search work, and what precondition does it require?
Repeatedly compare the target to the middle element of the current search range; if equal, found; if target is
smaller, recurse/iterate on the left half; if larger, the right half — halving the search space each step. Requires
the array to be SORTED beforehand; O(log n) time, O(1) space iteratively (O(log n) space if implemented
recursively, due to call stack).
Q5. What's a common off-by-one bug in binary search, and how do you avoid it?
Computing mid = (low + high) / 2 can integer-overflow for very large low+high in languages with fixed-size ints
(less of an issue in Java's int range for typical interview-sized inputs, but still good practice); safer: mid = low +
(high - low) / 2. Another common bug: using high = mid instead of high = mid - 1 (or low = mid instead of low =
mid + 1) can cause infinite loops — always verify the loop invariant shrinks the range on every iteration.
Q6. How would you find the first and last occurrence of a target value in a sorted array with duplicates,
in O(log n)?
Two separate binary searches: one biased to keep searching LEFT even after finding a match (to find the first
occurrence), one biased to keep searching RIGHT after finding a match (to find the last occurrence). Both are
still O(log n), so O(log n) total, much better than an O(n) linear scan.
Q8. What is the difference between a stable and an unstable sort, and why would it matter in a real
scenario?
A stable sort preserves the relative order of elements that compare as equal; an unstable sort makes no such
guarantee. It matters e.g. when sorting a list of orders first by customer name and then (stably) by order date —
a stable sort on date preserves the prior name-based grouping/order for orders with the same date, while an
unstable sort could scramble it.
Q9. Explain how you'd search in a rotated sorted array in O(log n).
Modified binary search: at each step, determine which half (left of mid, or right of mid) is properly sorted by
comparing arr[low] and arr[mid]; then check if the target lies within that sorted half's range — if yes,
recurse/iterate into that half, otherwise recurse into the other half. Still O(log n) since you always eliminate half
the search space each step.
Backtracking = recursion + explicit undo: try a choice, recurse deeper, and if that path fails (or you've explored it
fully), UNDO the choice ('backtrack') and try the next option. It's the standard technique for generating all
subsets/permutations/combinations, and for constraint-satisfaction problems (N-Queens, Sudoku solver, word
search in a grid).
The backtracking template: define the choices available at each step, recurse into each choice, and explicitly
revert any shared state (like removing the last-added element from a running path, or un-marking a visited cell)
before trying the next choice — forgetting the 'undo' step is the single most common bug.
Q2. What happens if a recursive function has no base case, or the base case is unreachable?
Infinite recursion — each call adds a new frame to the call stack, and since it never stops, the stack eventually
exceeds its size limit, throwing a StackOverflowError.
Q3. How would you generate all subsets of a set using backtracking?
At each element, recursively branch into two choices: include it in the current subset, or don't; when you've made
a decision for every element, add the current subset to the results. This explores 2^n leaf outcomes for n
elements, O(2^n) time, matching the number of possible subsets.
Q4. How would you generate all permutations of an array using backtracking?
Maintain a 'used' marker per element (or swap-based approach); at each recursive step, try placing each
not-yet-used element next in the current permutation, recurse, then UNDO (mark it unused again / swap back)
before trying the next candidate. O(n!) time, matching the number of permutations.
Q5. What is the key difference between backtracking and plain brute-force recursion?
Backtracking actively PRUNES: as soon as a partial solution is known to violate a constraint, it abandons that
branch immediately rather than continuing to build it out fully — this can dramatically reduce the actual explored
search space below the theoretical worst case, even though the worst-case complexity bound is often the same.
9. Dynamic Programming
THEORY NOTES
DP applies when a problem has (1) OVERLAPPING SUBPROBLEMS (the same smaller subproblem is solved
repeatedly in a naive recursive approach) and (2) OPTIMAL SUBSTRUCTURE (the optimal solution to the full
problem can be built from optimal solutions to its subproblems). Recognizing these two properties — usually by
first writing the naive recursive solution and noticing repeated calls — is the actual interview skill being tested,
more than memorizing specific problems.
Two implementation styles: top-down (memoization — write the natural recursion, add a cache) and bottom-up
(tabulation — build up a table iteratively from the smallest subproblems to the full problem, usually avoiding
recursion/call-stack overhead entirely). Both achieve the same time complexity; tabulation is often preferred in
practice for avoiding stack depth issues and sometimes allows further space optimization (e.g., only keeping
the last 1-2 rows of a 2D table).
Classic DP problem families worth having memorized end-to-end: 0/1 Knapsack, Longest Common
Subsequence, Longest Increasing Subsequence, Coin Change (min coins / number of ways), Edit Distance,
and House Robber-style 'take or skip adjacent' problems.
Q2. What's the difference between top-down (memoization) and bottom-up (tabulation) DP?
Top-down: write the natural recursive solution, then add a cache (array/map) to store and reuse results of
subproblems already solved — closely mirrors the recursive structure, computes only subproblems actually
needed. Bottom-up: build an iterative table starting from the smallest base-case subproblems, progressively
combining them up to the final answer — usually avoids recursion/stack overhead, and computes all
subproblems in the table's range whether needed or not.
Q3. How would you solve 0/1 Knapsack with DP? State the recurrence and complexity.
dp[i][w] = max value using the first i items with capacity w. Recurrence: dp[i][w] = dp[i-1][w] (skip item i) if item i's
weight > w, else max(dp[i-1][w], dp[i-1][w - weight[i]] + value[i]) (best of skip vs take). O(n*W) time and space,
where n is item count and W is capacity; space can be reduced to O(W) using a 1D rolling array.
Q4. How would you find the Longest Common Subsequence (LCS) of two strings with DP?
dp[i][j] = length of LCS of the first i characters of string A and first j characters of string B. If A[i-1] == B[j-1], dp[i][j]
= dp[i-1][j-1] + 1; otherwise dp[i][j] = max(dp[i-1][j], dp[i][j-1]). O(n*m) time and space for strings of length n and m.
Q5. How would you compute the minimum number of coins to make a target amount (Coin Change)?
dp[amount] = minimum coins to make that amount. Base case dp[0] = 0. For each amount from 1 upward,
dp[amount] = min over all coin denominations c <= amount of (dp[amount - c] + 1), if reachable; otherwise
Q6. What is the House Robber problem, and what is its DP recurrence?
Given houses in a row with values, find the max sum you can rob without robbing two ADJACENT houses. dp[i]
= max(dp[i-1] (skip house i), dp[i-2] + value[i] (rob house i, must skip i-1)). O(n) time, and space can be reduced
to O(1) since you only ever need the last two dp values.
Q7. What is the recurrence for Longest Increasing Subsequence (LIS), and its time complexity in the
basic DP formulation?
dp[i] = length of the LIS ending exactly at index i = 1 + max(dp[j]) over all j < i where arr[j] < arr[i] (or just 1 if no
such j exists). O(n^2) time in this basic form (can be optimized to O(n log n) using binary search with a
patience-sorting-style approach, a common 'can you do better' follow-up).
Q8. Why can House Robber's DP be optimized from O(n) space to O(1) space, but LCS generally cannot
go below O(n) or O(min(n,m)) easily?
House Robber's recurrence for dp[i] only ever depends on the two immediately preceding values (dp[i-1],
dp[i-2]), so you only need to keep those two rolling variables. LCS's dp[i][j] depends on an entire previous ROW
(dp[i-1][*]) to compute the current row, so you need at least one full row's worth of space (O(min(n,m)) with a
rolling-row optimization), not just a couple of scalars.
Q9. How would you compute Edit Distance (minimum operations to convert string A to string B)?
dp[i][j] = edit distance between first i chars of A and first j chars of B. If A[i-1]==B[j-1], dp[i][j] = dp[i-1][j-1] (no
operation needed); otherwise dp[i][j] = 1 + min(dp[i-1][j] (delete), dp[i][j-1] (insert), dp[i-1][j-1] (replace)). O(n*m)
time and space.
Classic correct-greedy problems: Activity Selection (sort by finish time, pick the earliest-finishing compatible
activity each time), Fractional Knapsack (sort by value/weight ratio), Huffman Coding, Dijkstra's shortest path
(greedy + priority queue). Classic problems where naive greedy FAILS: 0/1 Knapsack (greedy by value/weight
ratio does not guarantee optimal — needs DP instead), which is a favorite 'why doesn't greedy work here' trap
question.
Hashing underlies HashMap/HashSet and is separately tested as a standalone technique: using a hash
set/map to achieve O(1) average lookups turns many O(n^2) brute-force pair/subarray problems into O(n) —
e.g., Two Sum, detecting duplicates, subarray sum equals K.
Q3. Why does a naive greedy-by-value/weight-ratio approach FAIL for 0/1 Knapsack, when it works for
Fractional Knapsack?
In Fractional Knapsack, you can take a PARTIAL item, so always grabbing the best ratio first and filling
remaining capacity fractionally is optimal. In 0/1 Knapsack, items are all-or-nothing — greedily taking the
best-ratio item first can lock in a choice that leaves awkward leftover capacity that can't be used efficiently,
missing a better combination that a full DP search over all inclusion/exclusion choices would find. This is exactly
why 0/1 Knapsack requires DP, not greedy.
Q4. How does hashing turn the 'Two Sum' problem from O(n^2) to O(n)?
Brute force checks every pair (O(n^2)). With a HashMap, iterate once, and for each element check whether
(target - element) has already been seen and stored in the map; if yes, you found the pair; if no, add the current
element to the map and continue. Single pass, O(n) time, O(n) space.
Q5. How would you find if an array contains any duplicate elements efficiently?
Iterate once, inserting each element into a HashSet; if an insertion attempt finds the element already present
(add() returns false, or contains() check first), a duplicate exists. O(n) time, O(n) space — versus O(n log n) via
sorting first, or O(n^2) via brute-force nested comparison.
Q6. How would you find the count of subarrays whose sum equals a target K, in O(n)?
Use a prefix sum running total plus a HashMap that tracks how many times each prefix-sum VALUE has
occurred so far. At each index, check if (currentPrefixSum - K) exists in the map — its count tells you how many
subarrays ending here sum to K — then increment the map's count for currentPrefixSum. O(n) time, O(n) space.
Q7. What is a hash collision, and name two common resolution strategies.
When two different keys map to the same hash bucket/index. Two strategies: (1) Chaining — each bucket holds
a list (or tree, as in Java 8+ HashMap) of all entries that hash there. (2) Open addressing — on collision, probe
for the next available slot using a defined sequence (linear probing, quadratic probing, or double hashing).
Q8. Give an example of a problem where greedy gives a WRONG answer and explain why, to show you
understand greedy's limitations.
Coin Change (minimum coins) with an arbitrary coin system, e.g. coins = {1, 3, 4}, target = 6: greedy (always
take the largest coin <= remaining) picks 4, then 1, then 1 → 3 coins (4+1+1), but the optimal answer is 3+3 → 2
coins. Greedy fails here because taking the largest coin first isn't always part of the truly optimal combination —
this specific coin system lacks the greedy-choice property, which is exactly why general Coin Change is solved
with DP, not greedy.