Skip to content
SparkTime, home

Lesson 25 · From Practice to Job-Ready

The Interview Problem-Solving Workflow

A step-by-step routine for coding interviews: clarify, work examples, state a brute force, optimize, agree on a plan, code, test and analyze. Includes a full sample dialogue, advice on getting unstuck, and checklists you can use in mock interviews.

On this page

Key takeaways

  • An interview tests how you think and communicate as much as the final code. Talk through your reasoning, and check in with the interviewer.
  • Follow the same sequence every time: clarify, examples, brute force, optimize, plan, code, test, analyze. Do not skip straight to code.
  • State the brute force and its complexity even if you can see a better idea. It proves you understand the problem and gives you a safe fallback.
  • Test your own code by tracing it on a normal example and on edge cases (empty, single, duplicates, extremes) before the interviewer has to.
  • When stuck, shrink the problem, work an example by hand, and think aloud. Accept hints gracefully. Silence and panic cost more than a wrong idea.

Two candidates are given the same problem. Both finish a correct solution. One of them gets a warm "that was a good conversation", and the other gets a cool "thank you for your time". Why?

The first one asked what the input could look like, tried a simple approach first, noticed it was slow, explained a better idea before writing it, tested the code on tricky cases, and said clearly how fast it was. The second typed silently for twenty minutes and handed over code that happened to work.

Both produced working code. The first showed the interviewer what it would be like to work with them. That is a large part of what a coding interview is for. This article gives you a repeatable routine for being the first candidate.

What you will learn

#
  • What interviewers are usually trying to learn from a coding problem.
  • A step-by-step workflow, with a suggested time split.
  • How to find the optimisation after you state a brute force.
  • A full sample dialogue, with code you can run.
  • What to do when you are stuck, and how to take a hint.
  • Testing and complexity-statement habits, plus checklists for practice.

What interviewers are usually looking for

#

Different companies score differently, but most coding interviews look at some mix of these:

Area

What it looks like in practice

Problem solving

You break the problem down, try ideas, and improve them

Communication

You explain your thinking, ask good questions and respond to hints

Coding

Your code is correct, readable, and uses the language sensibly

Verification

You test your own work and find your own bugs

Fundamentals

You know complexity, and you choose a sensible data structure

Notice that "gets the optimal answer instantly" is not on the list. A candidate who reaches a good solution through clear reasoning, with one hint, often does better than one who recites a memorised solution without being able to explain it. Interviewers often ask a follow-up ("what if the input is a stream?") precisely to see whether you understand your own answer.

The workflow

#

Here is the sequence. The time split is a rough guide for a 45-minute slot, which is a common length but certainly not universal. Adapt it to what you are told.

Step

What you do

About

1. Listen and restate

Read the problem carefully. Say it back in your own words

1 to 2 min

2. Clarify

Ask about inputs, outputs, constraints and edge cases

3 to 5 min

3. Examples

Make one normal and one tricky example, and work out the answers

3 min

4. Brute force

Describe the simplest correct approach, with its complexity

3 to 5 min

5. Optimize

Find the wasted work, and improve. Agree on a plan before coding

5 to 8 min

6. Code

Write clean code, narrating as you go

15 to 20 min

7. Test

Trace your code on the examples and on edge cases. Fix bugs calmly

5 to 7 min

8. Analyze and discuss

State time and space complexity, trade-offs and possible extensions

3 to 5 min

Let us go through each step.

1. Listen and restate

#

Resist the urge to start solving after the first sentence. Read to the end. Then restate the problem in your own words: "So we have two sorted arrays, and we want to merge them into the first one, which has spare room at the end. Is that right?" This catches misunderstandings early and gives you a moment to think.

2. Clarify

#

Questions you should almost always consider:

  • Input. What type is it? Can it be empty? Can it contain duplicates, negatives, null? Is it sorted? How large can it get?
  • Output. What exactly should I return? An index, a value, a list? What if there is no answer? What if there are several?
  • Constraints. What are the limits on size and on values? This tells you which complexity is acceptable, and whether int could overflow.
  • Rules. May I modify the input? Is extra memory allowed?

These are not stalling tactics. They are what a good engineer does before building anything. Write the answers down where you can see them.

3. Examples

#

Make a small normal example. Then add awkward ones: empty, a single element, all equal, a case where the answer is at the end. Work out the expected answer by hand. Later, these become your tests.

4. Brute force

#

Say the simplest correct solution out loud, and its complexity, even if you can already see something better. For example: "The brute force is two nested loops over all pairs, which is O(n^2)." Three benefits: it shows you understand the problem, it gives you a fallback if you get stuck, and the way it wastes work points to the optimisation.

5. Optimize

#

Look at what the brute force repeats or wastes. Then use the toolbox from this course. These questions help:

Question to ask yourself

Possible tool

Am I searching for something I already looked at?

Hash set or hash map

Is the data sorted, or can I sort it cheaply?

Two pointers, binary search

Am I recomputing a sum or count over a range?

Prefix sums, sliding window

Do I repeatedly need the smallest or largest?

Heap

Do I need the most recent or oldest item?

Stack or queue

Are there overlapping subproblems?

Dynamic programming

Is it about connections, ordering or shortest steps?

Graph traversal

Do I need all combinations, with small input?

Backtracking

Do the constraints hint at a complexity?

n up to 10^5 suggests O(n log n) or better

When you have an idea, explain it before you code it, in a sentence or two, with a quick example. Then check: "Does that approach sound reasonable to you?" Agreeing on a plan is the single best way to avoid spending twenty minutes coding the wrong thing.

6. Code

#
  • Write clean, readable code: meaningful names, small helper methods for distinct jobs, consistent formatting.
  • Narrate why, not just what. "I move the pointer on the smaller side, because that is the only side that can improve the answer" is much better than "now I increment left".
  • Do not try to be clever. A plain loop that is obviously correct beats a clever line that needs explaining.
  • Handle the edge cases you identified. If you plan to handle one later, say so ("I will add the empty check after the main loop").
  • If you realise you made a mistake, say so and fix it. Everyone does this.

7. Test

#

Do not hand over code and say "done". Trace it yourself. Take your normal example and walk through the code line by line, tracking variable values, as you did in the dry runs of this course. Then try each edge case. Many bugs, such as off-by-one errors, are found in the first two minutes of tracing. Finding your own bug is a good signal to an interviewer. Having them find it for you is not.

8. Analyze and discuss

#

State the time and space complexity clearly (see below), and compare with your brute force. Be ready for follow-ups: "What if the input does not fit in memory?", "What if it is a stream?", "Can you do it with less space?". If you do not know, say how you would think about it.

Finding the optimisation: a worked sample dialogue

#

Here is a complete example, written as a conversation. The problem is a nice one for showing in-place thinking:

Problem. You are given two sorted integer arrays nums1 and nums2. nums1 has length m + n: its first m positions hold real values in sorted order, and the last n positions are empty padding (zeros). nums2 has n values. Merge nums2 into nums1 so that nums1 becomes one sorted array.

Interviewer: Take your time. Walk me through how you would approach this.

Candidate: Let me restate it first. nums1 has m real elements followed by room for n more, and nums2 has n elements. Both are sorted, and I need the merged, sorted result stored in nums1 itself. Can I assume ascending order?

Interviewer: Yes, ascending.

Candidate: And the padding at the end of nums1, can I overwrite it freely?

Interviewer: Yes. It is just there to give you room.

Candidate: Can m or n be zero?

Interviewer: Yes, either could be.

Candidate: Thanks. Let me make an example. nums1 = [1, 2, 3, 0, 0, 0] with m = 3, and nums2 = [2, 5, 6] with n = 3. The merged result should be [1, 2, 2, 3, 5, 6]. A tricky one is nums1 = [0] with m = 0, nums2 = [1]: the result is [1], and nums1 has no real elements at all.

Candidate: For a brute force, I could copy nums2 into the padding of nums1 and then sort the whole array. That is O((m + n) log (m + n)) time and no extra array. It works, but it ignores that both inputs are already sorted.

Interviewer: Good. Can you do better?

Candidate: The usual merge of two sorted arrays is O(m + n), walking through both from the front and writing the smaller each time. But if I write into nums1 from the front, I will overwrite its real elements before I have read them. I could copy nums1 first, which costs O(m) extra space. Hmm. Is there a way to avoid that?

Candidate: What if I merge from the back? The padding is at the end of nums1, so the largest element of the merged result belongs at the last position, which is empty. I can compare the last real element of nums1 with the last element of nums2, put the larger at the end, and move the corresponding pointer left. Writing from the back never overwrites a value I still need, because the write position is always at or ahead of the read position in nums1.

Interviewer: Convince me of that last claim.

Candidate: Think of the free slots between the unread part of nums1 and the write position. At the start, that is exactly n slots, the padding. Every time I place an element from nums2, one free slot is used up and one element of nums2 is consumed. Every time I place an element from nums1, the read pointer and the write pointer both move left by one, so the number of free slots does not change. So the number of free slots always equals the number of unread elements of nums2. While nums2 still has elements, there is at least one free slot, so the position I write to is never an unread element of nums1.

Interviewer: Good. Go ahead and code it.

Candidate: I will use three pointers: i for the last real element of nums1, j for the last element of nums2, and write for the position to fill. I loop while j >= 0. Once nums2 is exhausted, the remaining elements of nums1 are already in place, so I can stop. But if nums1's real elements run out first, I need to copy the rest of nums2, and the loop condition on j handles that.

(The candidate writes the code below.)

Java
// File: Main.javaimport java.util.Arrays;public class Main {    // Merges nums2 into nums1 in place. nums1 has m real values followed by n spare slots.    static void merge(int[] nums1, int m, int[] nums2, int n) {        int i = m - 1;                 // last real element of nums1        int j = n - 1;                 // last element of nums2        int write = m + n - 1;         // where the next largest value goes        while (j >= 0) {               // once nums2 is used up, nums1's remaining prefix is already in place            if (i >= 0 && nums1[i] > nums2[j]) {                nums1[write] = nums1[i];                i--;            } else {                nums1[write] = nums2[j];                j--;            }            write--;        }    }    public static void main(String[] args) {        int[] a = {1, 2, 3, 0, 0, 0};        merge(a, 3, new int[] {2, 5, 6}, 3);        System.out.println(Arrays.toString(a));        int[] b = {0};        merge(b, 0, new int[] {1}, 1);        System.out.println(Arrays.toString(b));        int[] c = {4, 5, 6, 0, 0, 0};        merge(c, 3, new int[] {1, 2, 3}, 3);        System.out.println(Arrays.toString(c));        int[] d = {1, 2, 3};        merge(d, 3, new int[] {}, 0);        System.out.println(Arrays.toString(d));        int[] e = {2, 0};        merge(e, 1, new int[] {1}, 1);        System.out.println(Arrays.toString(e));    }}
Code
[1, 2, 2, 3, 5, 6][1][1, 2, 3, 4, 5, 6][1, 2, 3][1, 2]

Candidate: Let me test. For my first example, i = 2, j = 2, write = 5. Compare 3 and 6: 6 is larger, so nums1[5] = 6, j = 1. Next, 3 against 5: nums1[4] = 5, j = 0. Next, 3 against 2: 3 is larger, so nums1[3] = 3, i = 1. Next, 2 against 2: they are equal, and my condition is strict, so I take from nums2: nums1[2] = 2, j = -1. The loop ends. nums1 is [1, 2, 2, 3, 5, 6]. Correct. For the case m = 0, i = -1, so the i >= 0 check fails and every element comes from nums2. Good. If n = 0, the loop does not run, and nums1 is untouched, which is correct.

Candidate: Each loop iteration places one element and moves one pointer left, and the loop runs at most m + n times, so the time is O(m + n). The extra space is O(1), just three integers. The brute force was O((m + n) log (m + n)).

Interviewer: What if the arrays were not sorted?

Candidate: Then merging would not apply. I would need to sort first, which is the brute force I mentioned.

Take a moment to see what the candidate did. They asked before coding. They gave a brute force with its complexity. They found the optimisation by looking at a specific obstacle (overwriting). They gave a short justification for why the idea is safe. They traced the code on the example and on two edge cases. They stated the complexity at the end and compared it to the brute force. None of that required genius, only a routine.

Here is the trace of the first example, in a table:

i

j

write

Compare

Action

nums1 after

2

2

5

3 vs 6

take 6 from nums2

[1, 2, 3, 0, 0, 6]

2

1

4

3 vs 5

take 5

[1, 2, 3, 0, 5, 6]

2

0

3

3 vs 2

take 3 from nums1

[1, 2, 3, 3, 5, 6]

1

0

2

2 vs 2 (not greater)

take 2 from nums2

[1, 2, 2, 3, 5, 6]

After that, j = -1, the loop stops, and the leading 1, 2 of nums1 are already in the right place.

Getting unstuck

#

Everyone gets stuck sometimes. What matters is how you behave. Try these in roughly this order:

  1. Say what you know. "I understand the brute force. The part I am stuck on is how to avoid re-checking pairs." Naming the obstacle often reveals it.
  2. Shrink the problem. Try an input of size 2 or 3 by hand. Notice what you do. That pattern may be the algorithm.
  3. Draw it. Boxes and arrows for lists and trees, a grid for DP, a graph for dependencies.
  4. Ask the toolbox questions from step 5: would a hash map, sorting, a heap, a stack, or two pointers help here?
  5. Relax a constraint. Solve an easier version (sorted input, small values, no duplicates), then work back.
  6. Look at the constraints. n up to 20 suggests exponential. n up to 10^5 suggests near-linear.
  7. Ask for a hint, politely. "I am considering X or Y but I am not sure which direction to take. Could you give me a nudge?" Interviewers expect this, and how you use the hint is part of what they see.

Stating complexity clearly

#

Give both time and space, with what the variables mean: "O(m + n) time, where m and n are the sizes of the two arrays, and O(1) extra space." Also:

  • Include the recursion stack in space if you use recursion ("O(h) for the call stack, where h is the height").
  • Mention the hidden costs of library calls: sorting is O(n log n), String += in a loop is quadratic, list.remove(0) is O(n).
  • Say whether you mean worst case, average or amortised when they differ ("average O(1) for a hash map").
  • Compare with the brute force: it shows you know why the improvement matters.
  • Do not claim a complexity you cannot justify. "I think it is O(n) because each pointer moves at most n times" is stronger than a confident but unexplained answer.

Testing: a checklist

#

Category

Examples to try

Empty

Empty array, empty string, m = 0, no edges

Single

One element, one character, one node

Duplicates

All equal, some repeated, repeated at the boundary

Extremes

Largest and smallest values (Integer.MAX_VALUE, Integer.MIN_VALUE), large n

Order

Already sorted, reverse sorted

Boundaries

First and last positions, exactly at a threshold (>= versus >)

Structure

A cycle, a disconnected graph, a skewed tree, a blocked start

When you trace, follow the code as written, not as you meant it. The bug is usually in the gap between the two.

A self-review checklist for practice

#

Use these during mock interviews and when solving problems alone. Copy them into your notes.

Before I code:

  • I restated the problem in my own words.
  • I asked about input size, edge cases and whether I may modify the input.
  • I worked at least one normal and one tricky example by hand.
  • I said the brute force and its complexity out loud.
  • I explained my plan and checked that it sounded reasonable.

While I code:

  • My names are meaningful, and my code is split into small, clear pieces.
  • I narrate why, not only what.
  • I handle the edge cases I listed.

Before I say "done":

  • I traced the code on my normal example.
  • I tried the empty, single-element and extreme cases.
  • I looked for int overflow, off-by-one errors and null dangers.
  • I stated time and space complexity, and compared with the brute force.
  • I am ready for a follow-up ("what if it were a stream?").

Java hygiene for interviews

#
  • Know your core library by heart: Arrays.sort, Arrays.fill, Arrays.copyOf, Collections.sort, Map.getOrDefault, Map.merge, Deque (push, pop, offer, poll), PriorityQueue with a comparator, and StringBuilder.
  • Use Integer.compare(a, b) in comparators, never a subtraction.
  • Use long when sums or products can be large.
  • Do not modify the input unless the problem allows it. Copy first if you must.
  • Prefer the simplest syntax. Fancy features that you cannot explain quickly are a risk.
  • Ask whether you may use library utilities such as Arrays.sort or Collections.reverse, or whether you should implement them. Usually the answer is yes, as long as you state their complexity.

Common failure patterns and mistakes

#

What goes wrong

What to do instead

Jumping to code after reading the problem

Restate, clarify and make examples first

Coding in silence

Narrate your reasoning, and check in at milestones

Never saying the brute force

Say it, with its complexity, then improve

Skipping the test step

Trace the code yourself before declaring it finished

Memorising solutions without understanding

Be able to explain why each step is correct, and handle variations

Over-engineering

Solve the stated problem first, then discuss extensions

Ignoring the constraints

Let the input size guide the complexity you aim for

Defensiveness about hints or bugs

Treat them as information, and respond with curiosity

Making up a confident but unjustified complexity

Say what the loop does, and count

Giving up when the optimal answer does not come

Offer the brute force, and keep improving it

Exercises

#

Exercise 1

Trace

#

Trace the merge-from-the-back code on nums1 = [4, 5, 6, 0, 0, 0], m = 3, nums2 = [1, 2, 3], n = 3. Write i, j, write and the array after each step. What do you notice about the number of loop iterations?

Exercise 2: Edge cases

#

You are asked to write merge(intervals), which merges overlapping intervals (the problem from the greedy article). Before coding, write a list of at least eight inputs you would test, including at least two that test the rules about touching intervals.

Exercise 3: Apply the workflow

#

Problem: given a string of ( and ) characters, return the minimum number of parentheses you must add (anywhere) to make the string valid. "())" needs 1, "(((" needs 3, "()" needs 0. Write your workflow steps (clarify, example, brute force, optimise, test) briefly, then give the code.

Exercise 4: Debug

#

This merge-from-the-back forgets one case. Find an input that breaks it, and fix it.

Java
static void mergeBuggy(int[] nums1, int m, int[] nums2, int n) {    int i = m - 1;    int j = n - 1;    int write = m + n - 1;    while (i >= 0) {        if (nums1[i] > nums2[j]) {            nums1[write] = nums1[i];            i--;        } else {            nums1[write] = nums2[j];            j--;        }        write--;    }}

Exercise 5: Critique a complexity claim

#

A candidate says this method is O(n) because it has "only one loop". Is the claim right? What is the true complexity, and how would you improve it?

Java
static List<Integer> uniqueValues(int[] numbers) {    List<Integer> result = new ArrayList<>();    for (int x : numbers) {        if (!result.contains(x)) {            result.add(x);        }    }    return result;}
Show solutionHide solution

i = 2, j = 2, write = 5.

i

j

write

Compare

Action

nums1 after

2

2

5

6 vs 3

6 is larger: take from nums1

[4, 5, 6, 0, 0, 6]

1

2

4

5 vs 3

take 5

[4, 5, 6, 0, 5, 6]

0

2

3

4 vs 3

take 4

[4, 5, 6, 4, 5, 6]

-1

2

2

i < 0

take from nums2: 3

[4, 5, 3, 4, 5, 6]

-1

1

1

i < 0

take 2

[4, 2, 3, 4, 5, 6]

-1

0

0

i < 0

take 1

[1, 2, 3, 4, 5, 6]

Six loop iterations: one per element placed, here m + n = 6. This case is interesting because nums1 runs out first, and the loop (which runs while j >= 0) correctly copies all of nums2. In the earlier example, nums2 ran out first and the loop stopped early, leaving the front of nums1 untouched. Both directions work because the loop condition depends on j, and the i >= 0 check guards the nums1 reads.

Java
// File: Main.javaimport java.util.Arrays;public class Main {    public static void main(String[] args) {        int[] nums1 = {4, 5, 6, 0, 0, 0};        int[] nums2 = {1, 2, 3};        int i = 2;        int j = 2;        int write = 5;        int steps = 0;        while (j >= 0) {            if (i >= 0 && nums1[i] > nums2[j]) {                nums1[write--] = nums1[i--];            } else {                nums1[write--] = nums2[j--];            }            steps++;            System.out.println("step " + steps + ": " + Arrays.toString(nums1));        }    }}
Code
step 1: [4, 5, 6, 0, 0, 6]step 2: [4, 5, 6, 0, 5, 6]step 3: [4, 5, 6, 4, 5, 6]step 4: [4, 5, 3, 4, 5, 6]step 5: [4, 2, 3, 4, 5, 6]step 6: [1, 2, 3, 4, 5, 6]

Exercise 2

Show solutionHide solution

Here are some good tests for interval merging:

#

Input

Why

1

[]

Empty input

2

[ [1, 5] ] (one interval)

A single interval

3

[[1, 3], [2, 6], [8, 10], [15, 18]]

A normal case, some overlap and some gaps

4

[[1, 4], [4, 5]]

Touching intervals: do they merge? The problem decides, so ask

5

[[1, 10], [2, 3]]

A nested interval: the end must not shrink

6

[[8, 10], [1, 3], [2, 6]]

Unsorted input, so we must sort first

7

[[1, 4], [0, 4]]

Same end, different starts

8

[[1, 4], [1, 4]]

Identical (duplicate) intervals

9

[[1, 2], [3, 4]]

Disjoint, with a gap, even though they are consecutive numbers

10

[[Integer.MIN_VALUE, 0], [1, 2]]

Extreme values, where a subtraction comparator can break

11

[ [5, 5] ] (one interval)

A zero-length interval

12

A chain where each interval overlaps only the next one

Merges into one big interval

Tests 4 and 9 are the touching-interval cases: [1, 4] and [4, 5] share the point 4, while [1, 2] and [3, 4] do not touch at all. Asking the interviewer about the rule for touching intervals before you code is exactly the kind of clarifying question the workflow is about.

Exercise 3

Show solutionHide solution

Clarify. The string contains only ( and )? Is the empty string valid (yes, and it needs 0)? We may insert parentheses at any position.

Examples. "())" needs 1 (add ( at the front). "(((" needs 3 (three )). "()" needs 0. A tricky one: ")(" needs 2, because the closer comes before any opener and the opener at the end is never closed.

Brute force. Try all ways to insert parentheses, and check validity. Exponential, and far too slow.

Optimise. Scan once, keeping open, the number of unmatched ( seen so far. For (, increase open. For ): if open > 0, it matches an earlier (, so decrease open. Otherwise this ) has nothing to match, and we must add a ( for it: count it in added. At the end, every unmatched ( still in open needs a ). The answer is added + open. This is O(n) time and O(1) space.

Why it is right. Each unmatched ) can only be fixed by inserting a ( before it, and each unmatched ( only by inserting a ) after it. They are independent, so the total is the sum.

Java
// File: Main.javapublic class Main {    static int minAddToMakeValid(String s) {        int open = 0;        // unmatched '(' so far        int added = 0;       // '(' we must insert for unmatched ')'        for (char c : s.toCharArray()) {            if (c == '(') {                open++;            } else if (open > 0) {                open--;      // this ')' matches an earlier '('            } else {                added++;     // nothing to match: we need an extra '('            }        }        return added + open;    }    public static void main(String[] args) {        String[] tests = {"())", "(((", "()", ")(", "", "()))((", "(()"};        for (String test : tests) {            System.out.println("[" + test + "] -> " + minAddToMakeValid(test));        }    }}
Code
[())] -> 1[(((] -> 3[()] -> 0[)(] -> 2[] -> 0[()))((] -> 4[(()] -> 1

Test. ")(": the first ) has open = 0, so added = 1. The ( makes open = 1. Total 2. ✓. "()))((": () matches. Then ) and ) each add one (added = 2), then (( gives open = 2. Total 4. ✓.

Exercise 4

Show solutionHide solution

The loop runs while (i >= 0), so it stops as soon as nums1's real elements are used up, without copying the remaining elements of nums2. If nums2 contains values smaller than everything in nums1, they are never placed. For nums1 = [4, 5, 6, 0, 0, 0] and nums2 = [1, 2, 3], the loop places 6, 5, 4, then stops, leaving the front of the array as [4, 5, 6, ...], with 1, 2, 3 never copied.

There is a second problem. When nums2 is used up first (j becomes -1) while nums1 still has unread elements, the loop continues and reads nums2[-1], which throws an index error. The article's own first example does this. The fix from the article makes the loop run while j >= 0, and guards the read of nums1[i] with i >= 0.

Java
// File: Main.javaimport java.util.Arrays;public class Main {    static void mergeBuggy(int[] nums1, int m, int[] nums2, int n) {        int i = m - 1;        int j = n - 1;        int write = m + n - 1;        while (i >= 0) {            if (nums1[i] > nums2[j]) {                nums1[write] = nums1[i];                i--;            } else {                nums1[write] = nums2[j];                j--;            }            write--;        }    }    static void mergeFixed(int[] nums1, int m, int[] nums2, int n) {        int i = m - 1;        int j = n - 1;        int write = m + n - 1;        while (j >= 0) {            if (i >= 0 && nums1[i] > nums2[j]) {                nums1[write--] = nums1[i--];            } else {                nums1[write--] = nums2[j--];            }        }    }    public static void main(String[] args) {        int[] a = {4, 5, 6, 0, 0, 0};        mergeBuggy(a, 3, new int[] {1, 2, 3}, 3);        System.out.println("buggy:  " + Arrays.toString(a));        int[] b = {4, 5, 6, 0, 0, 0};        mergeFixed(b, 3, new int[] {1, 2, 3}, 3);        System.out.println("fixed:  " + Arrays.toString(b));        int[] c = {1, 2, 3, 0, 0, 0};        try {            mergeBuggy(c, 3, new int[] {2, 5, 6}, 3);            System.out.println("buggy on the first example: " + Arrays.toString(c));        } catch (ArrayIndexOutOfBoundsException e) {            System.out.println("buggy on the first example: crashes with an index error");        }    }}
Code
buggy:  [4, 5, 6, 4, 5, 6]fixed:  [1, 2, 3, 4, 5, 6]buggy on the first example: crashes with an index error

Notice that the buggy version crashes on the article's own first example, because j goes negative while i is still non-negative. A test on only the "happy" example would not have caught the first bug, but it does expose this one. This is why the checklist insists on running several kinds of input.

Exercise 5

Show solutionHide solution

The claim is wrong. There is one loop in the source, but result.contains(x) is itself a loop: it scans the list from the start until it finds x, which costs up to O(size of result). In the worst case (all distinct values), the result grows to n, so the total work is about 1 + 2 + ... + n, which is O(n^2). The right way to count is to look for hidden loops inside library calls.

The fix: keep a HashSet of values seen so far. Checking and adding to it is O(1) on average, so the whole method becomes O(n) time and O(n) space. We still use a List to keep the order in which values first appeared.

Java
// File: Main.javaimport java.util.ArrayList;import java.util.HashSet;import java.util.List;import java.util.Set;public class Main {    static List<Integer> uniqueValuesSlow(int[] numbers) {        List<Integer> result = new ArrayList<>();        for (int x : numbers) {            if (!result.contains(x)) {          // contains scans the list: O(size)                result.add(x);            }        }        return result;    }    static List<Integer> uniqueValuesFast(int[] numbers) {        Set<Integer> seen = new HashSet<>();        List<Integer> result = new ArrayList<>();        for (int x : numbers) {            if (seen.add(x)) {                  // add returns true only for a new value                result.add(x);            }        }        return result;    }    public static void main(String[] args) {        int[] numbers = {4, 8, 4, 15, 8, 16, 23, 42, 15};        System.out.println(uniqueValuesSlow(numbers) + " " + uniqueValuesFast(numbers));        System.out.println(uniqueValuesSlow(new int[] {}) + " " + uniqueValuesFast(new int[] {}));    }}
Code
[4, 8, 15, 16, 23, 42] [4, 8, 15, 16, 23, 42][] []

When you make a complexity claim in an interview, say what you counted: "one pass over n elements, and each contains on a list is linear, so this is quadratic. A hash set brings the check to constant time on average."

Where to go next

#
  • Guide · Technology

    How to Learn Data Structures and Algorithms Without Burning Out

  • Guide · Career

    OA Strategy: Constraints, Time, Brute Force and Partial Scoring

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages