Hello, and welcome to the first lesson in your interview-preparation course.
This first module rebuilds the coding-interview foundation needed for Staff and Principal-level backend interviews: not just producing a correct solution, but explaining why it scales, what it stores, and where its guarantees have limits. In this lesson, you will practice giving a precise complexity analysis of Java code, including the distinction between worst-case, expected, and amortized cost.
For interview purposes, a complexity statement is part of the solution, not an afterthought. A strong answer names the input variables, identifies the dominant work, states the relevant guarantee, and accounts for memory honestly.
Complexity is a growth-rate contract
Let be the running time of a method on an input of size . Asymptotic notation describes how grows as inputs become large.
- is an asymptotic upper bound.
- is an asymptotic lower bound.
- is a tight bound: both upper and lower.
In a coding interview, saying “time complexity is ” usually means “the worst-case running time is linear in the number of input elements,” unless you explicitly state a different qualifier. Do not treat as automatically synonymous with worst case; it is an upper-bound notation. The qualifier matters particularly for hashing and dynamically resized collections.
A useful mental model is that Big-O intentionally ignores:
- Constant multipliers, such as .
- Lower-order terms, such as , which is .
- Machine-specific execution details.
It does not mean constants never matter. An allocation-heavy Java implementation can be much slower than a cache-friendly implementation. At interview scale, establish asymptotic correctness first; discuss practical overhead when it affects a design choice.
Big-O Notation - Everything you Need for Coding Interviews
Watch “Big-O Notation - Everything you Need for Coding Interviews” from NeetCode for a concise visual reset on growth rates and a useful warning about nested loops.
Watch growth-rate intuition to refresh why constants and lower-order terms are ignored. Then watch common loop patterns, focusing on why two nested loops may still be linear when their total pointer movement is bounded, and why separate input dimensions should remain separate.
Start by defining the input size
Before analyzing code, declare what each variable represents.
- One array or string of length : use .
- Two independent arrays of lengths and : use both and .
- A matrix with rows and columns: use and , giving for a full scan.
- Later, for graphs, the natural variables will be vertices and edges .
Collapsing everything into can hide the actual cost. For example, comparing every element of one list with every element of another costs , not necessarily .
A reliable way to count work
When reading a Java solution, apply this sequence:
- Identify the dominant operations. Examples include comparisons, hash lookups, heap updates, array copies, or calls to a helper method.
- Count how often each operation can occur.
- Combine sequential phases by addition. Keep the dominant term asymptotically.
- Multiply only for truly independent nested work.
- State the guarantee: worst-case, expected, or amortized.
- Analyze memory separately.
For sequential phases, costs add:
For a branch, the worst-case cost is the larger branch, not the sum of both branches:
A loop that halves a search range each iteration is logarithmic:
A loop that processes all elements at each of levels, such as merge sort, is:
Nested loops: inspect total movement, not indentation
One of the most common analysis mistakes is declaring every nested loop to be . That conclusion is correct only when the inner loop performs a full traversal for each outer-loop iteration.
Consider this window-maintenance pattern:
static int longestWithinLimit(int[] values, int limit) {
int left = 0;
int sum = 0;
int best = 0;
for (int right = 0; right < values.length; right++) {
sum += values[right];
while (sum > limit) {
sum -= values[left];
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
At first glance, the while loop is nested inside the for loop. But left never moves backward. Across the entire method:
rightadvances at most times.leftadvances at most times.- Each array element is added once and removed at most once.
The total pointer movement is bounded by , so the time complexity is:
The method uses only a fixed number of primitive local variables:
auxiliary space.
This accounting style will be essential in the next sliding-window lesson. The rule is not “nested loops are linear”; it is: prove a global bound on the inner loop’s total work.
By contrast, this loop does enumerate roughly every pair:
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
process(i, j);
}
}
Its operation count is:
which is:
Expected time: what hashing actually guarantees
Hash-based lookup is one of the most important interview tools, but its complexity must be described accurately.
static boolean containsDuplicate(int[] values) {
Set<Integer> seen = new HashSet<>();
for (int value : values) {
if (!seen.add(value)) {
return true;
}
}
return false;
}
Let be values.length.
Under the usual assumption that hashes distribute keys well:
HashSet.addis expected .- The loop performs at most additions.
- Total expected time is .
- The set can store up to values, so auxiliary space is .
A high-quality interview explanation would be:
“Let be the number of array elements. I scan the array once and maintain a
HashSetof values already seen. Each membership-and-insert operation is expected constant time under the normal hashing assumption, giving expected time and auxiliary space. The method can return earlier if it finds a duplicate, but the worst case scans the whole array.”
There are three different ideas here:
| Guarantee | What varies? | Example |
|---|---|---|
| Worst-case | The input or structure state is unfavorable | Linear scan finds the target last |
| Expected | Cost is analyzed under a probability assumption | Hash lookup with well-distributed hashes |
| Amortized | Cost is spread across a sequence of operations | Repeated ArrayList.add operations |
Expected time does not mean “it is fast most of the time” in an informal sense. It rests on an explicit probabilistic model, such as uniform hash distribution. Amortized time does not depend on randomness at all.
A related Java caveat: HashMap and HashSet may resize internally, and pathological collisions can affect observed behavior. In ordinary coding interviews, state the expected hashing assumption. In a production design discussion, also consider attacker-controlled keys, hashCode() quality, load factor, allocation pressure, and whether an ordered structure would provide more predictable behavior.
Space complexity: count memory deliberately
Space complexity needs its own statement. In interviews, the usual convention is to report auxiliary space: memory allocated beyond the input itself.
For containsDuplicate:
- Input array: , but normally excluded from auxiliary space.
HashSet<Integer>: up to entries.- Local references and counters: .
Therefore:
For the window example, there is no extra collection and no recursion:
Use these distinctions consistently:
| Memory source | Usually counted as auxiliary space? | Notes |
|---|---|---|
| Local primitives and references | Yes | Often |
| New arrays, maps, sets, queues | Yes | Count their maximum size |
| Recursion call stack | Yes | Depth matters |
| Input array or input string | No | State this convention if relevant |
| Returned output | Depends | State whether output space is included |
| In-place mutation of input | No extra structure | May still have stack space |
For recursive Java methods, do not report only the heap allocations. If recursion reaches depth , its call stack is , even if the method creates no collection.
Autoboxing changes practical memory usage: HashSet<Integer> stores boxed Integer objects rather than raw int values. That does not change the asymptotic result of , but it can matter in high-throughput services and memory-sensitive code.
Amortized analysis: expensive operations that happen rarely
A dynamic array has an apparent contradiction:
- Adding an element to an array with spare capacity is .
- Adding an element when it is full may allocate a larger array and copy all existing elements, which is .
Java’s ArrayList is backed by a dynamically resized array. Its exact capacity-growth policy is an implementation detail, but the important property is geometric growth: capacity increases by a constant factor rather than by a fixed number of slots.

Watch “Amortized Analysis” from 0612 TV w/ NERDfirst for a visual explanation of dynamic-array resizing and the aggregate proof.
Watch dynamic-array costs to distinguish a normal append from an append that triggers a resize. Then watch aggregate analysis, focusing on the geometric series of copy costs and why its total remains linear over a sequence of insertions.
Suppose capacity begins at and doubles whenever it becomes full. Across append operations:
- The new elements must each be written once, costing .
- Resize copies occur at capacities .
The total copying work is bounded by a geometric series:
So the work for all appends is bounded by:
Therefore:
Dividing the total by the number of appends gives constant amortized cost per append:
The word amortized is crucial. It does not say every individual append is constant time. A specific append that triggers a resize remains . It says any long sequence of appends has a total cost linear in the number of appends.
Lecture 20: Amortized Analysis
Read Cornell’s “Lecture 20: Amortized Analysis” for the formal motivation behind geometric growth and its connection to hash-table resizing.
In the opening discussion, before the “Amortized Analysis” subsection, read from the resizing explanation. Then read the geometric-growth warning. Focus on why infrequent expensive operations are acceptable only when their frequency decreases geometrically.
Why fixed-size growth fails
Suppose an array increases capacity by only elements each time it fills.
To hold elements, it resizes roughly times. The copy costs resemble:
This sum is quadratic:
The average cost per append is then , not constant. The difference is structural:
- Geometric growth: larger copies occur much less often, giving amortized append.
- Fixed-increment growth: increasingly large copies occur too frequently, giving non-constant amortized append.
The accounting intuition
One way to explain amortization informally is the accounting method. Charge every append three abstract units:
- One unit pays for placing the new element.
- The remaining units are saved as credit.
- When a resize occurs, saved credit pays for copies.
The stored credit must never become negative. This establishes that a constant charge per append covers all actual work over the sequence.
For an interview, the aggregate proof is typically enough. In a more algorithmically rigorous setting, the potential method gives a state-based proof. If the array has stored elements and capacity , one valid potential function for doubling is:
The amortized cost of an operation is:
where is actual cost and is the structure after operation . Regular appends accumulate potential; resizing spends it. Both kinds of appends have constant amortized cost.
You do not need to derive potential functions during a typical product-company coding interview. You should, however, be able to state precisely:
“
ArrayList.addat the end is amortized , because occasional resize-and-copy operations have total linear cost across appends. A resize-triggering append itself is .”
Also distinguish it from insertion at an arbitrary position:
arrayList.add(value)at the end: amortized .arrayList.add(index, value)in the middle: , because elements afterindexmust shift.arrayList.get(index): .
An interview-ready complexity report
After writing a solution, use this compact reporting structure:
-
Define input variables.
“Let be the number of values.” -
Describe the traversal and dominant operations.
“The loop visits each value once, and each lookup is expected constant time.” -
State the correct time qualifier.
“Expected time because the implementation uses hashing.”
Or: “Amortized per append because the backing array grows geometrically.” -
State auxiliary space and name the allocation.
“The hash set stores at most values, so auxiliary space is .” -
Mention relevant caveats only when useful.
“A single resize can be linear, but the sequence cost is amortized constant.”
“Hashing relies on normal distribution assumptions.”
For example, a concise answer for the duplicate-detection method is:
“Let be the array length. I make one pass through the input. The
HashSetstores each distinct value once, so it uses auxiliary space. Under the standard uniform-hashing assumption,addis expected , making total expected runtime .”
This is much stronger than simply saying “one loop, so .” It identifies the hidden data-structure operation, the guarantee being claimed, and the memory cost.
Key takeaways
- Complexity analysis begins by defining the real input variables, such as , , , and .
- Sum sequential phases, multiply independent nested traversals, and analyze total pointer movement before calling nested loops quadratic.
- Report expected time for hash-based operations when relying on normal hash distribution.
- Report auxiliary space separately from time, including collections and recursion stack.
- An
ArrayListappend can be on a resize, yet repeated appends are amortized because geometric resizing makes total copy work . - In an interview, state assumptions and qualifiers rather than presenting a bare Big-O label.
Next, you will apply this discipline to a core coding pattern: using a hash-based lookup strategy to solve an array or string problem efficiently.
Can't find a good explanation? Sign up and we'll make it for you
Sign up