Part 2 · Lesson 12 · 24 min read

Big-O: Counting the Steps

How many times does the loop run? Reading time and space complexity from code, and from the constraints of a problem.

On this page

  1. Why we count steps, not seconds
  2. Counting the steps of a loop
  3. Big-O: keep only what grows
  4. The growth rates you will meet
  5. O(log n): the halving loop
  6. Traced: halving 16 down to 1
  7. Reading Big-O from code
  8. Hidden loops in built-in methods
  9. Space complexity
  10. Best, average and worst case
  11. From constraints to approach
  12. Practice questions (7)
  13. Make it stick
  14. What's next

Why we count steps, not seconds

In Lesson 11 we often said that one solution was “faster” than another. How can we know this before we run the code? A stopwatch does not help much. The same code runs at different speeds on a laptop and on a phone. And with a small input, almost every solution looks instant.

So we count steps instead. A step is one simple operation, such as a comparison, an addition or reading arr[i]. Then we ask one question: when the input gets bigger, how fast does the number of steps grow? We write the answer in Big-O notation. Big-O is a short way to describe how the work grows when the input size grows. Every interviewer uses it to talk about efficiency. The letter n always means the size of the input, for example the length of an array.

Counting the steps of a loop

Take one of the simplest loops, adding up an array, and count its steps:

JavaScript
function sum(nums) {  let total = 0;                              // 1 step  for (let i = 0; i < nums.length; i++) {     // n + 1 checks, n updates    total += nums[i];                         // n steps  }  return total;                               // 1 step}

If you add up the numbers in the comments, you get 1 + (n + 1) + n + n + 1 = 3n + 3 steps for an array of length n. (Different people count slightly different steps. For example, we did not count let i = 0. That is fine, as you will see.) Check the formula for a few sizes:

Dry run · steps taken by sum(nums)
n (length)3n + 3Growth
1033
100303×10 input → about ×10 steps
1,0003,003×10 input → about ×10 steps
1,000,0003,000,003×1000 input → about ×1000 steps

Double the input and the work doubles. That straight-line growth is what O(n) means.

Big-O: keep only what grows

Exact counts like 3n + 3 depend on small details. For example, does i++ count as one step or as two? Big-O ignores such details. It uses two rules:

  1. Drop constant factors. A constant factor is a fixed number that you multiply by. 3n and n grow in the same way: if you double n, the work doubles. So 3n becomes n.
  2. Keep only the fastest-growing term. Look at n² + n + 3. When n is 1,000, the n² part is 1,000,000 and the rest is only about 1,000. The small terms stop mattering. So n² + n + 3 becomes n².
Dry run · simplifying to Big-O
Exact countBig-OSaid as
3n + 3O(n)linear
5O(1)constant
n²/2 + n/2O(n²)quadratic
2n + 100O(n)linear
n² + 1000nO(n²)quadratic

So the sum function is O(n). Say it “order n” or “linear time”. This measure of how the number of steps grows with the input is called time complexity.

The growth rates you will meet

Almost every solution in this series is one of these growth rates. They are in order from fastest to slowest:

Big-ONameTypical codeExample
O(1)constantNo loop over the inputarr[i], Map get, a maths formula
O(log n)logarithmicHalve the problem each stepBinary search (Lesson 28)
O(n)linearOne loopSum, max, linear search
O(n log n)n log nGood sortingarr.sort(), merge sort (Lesson 17)
O(n²)quadraticA loop inside a loopChecking every pair
O(2ⁿ)exponentialTry every subset (every possible group of items)Subsets (Lesson 33)

The difference between these rates is very big. Here are the step counts as n grows:

Dry run · steps for each growth rate
nlog nnn log nn²2ⁿ
10≈ 310≈ 331001,024
100≈ 7100≈ 66010,000≈ 10³⁰
1,000≈ 101,000≈ 10,0001,000,000far too many
1,000,000≈ 201,000,000≈ 20,000,00010¹²far too many

As a rough rule, a computer does about 10⁸ (100 million) simple steps per second. For n = 1,000,000: O(n) takes about 0.01 seconds, O(n log n) takes about 0.2 seconds, and O(n²) takes about 3 hours.

The idea to remember

For large inputs, the growth rate matters much more than the speed of the computer or small changes to the code. Often, the main improvement that an interviewer looks for is to change an O(n²) solution into an O(n) solution.

O(log n): the halving loop

One growth rate needs more explanation. A logarithm (log for short) answers this question: how many times can I halve n before I reach 1? The small 2 in log₂ means “halve”.

  • 16 → 8 → 4 → 2 → 1: four halvings, so log₂ 16 = 4.
  • 1,024 → … → 1: ten halvings, so log₂ 1,024 = 10.
  • 1,000,000 → … → 1: about twenty halvings.

So any loop that throws away half of what is left on each step is O(log n). It grows very slowly. Even for a billion items, it runs only about 30 times.

In Big-O, the base of the logarithm does not matter. (Dividing by 10 instead of 2 changes the work only by a constant factor.) So we just write log n. You have already met one such loop. The digit loop from Lesson 5 divides by 10 each time, so it is O(log n) in the size of the number.

Traced: halving 16 down to 1

step 1 / 16
Set up
n = 16

n is the size of the problem.

▶let n = 16;2let steps = 0;3while (n > 1) {4  n = Math.floor(n / 2);5  steps++;6}7console.log(steps);
Variables
n16
Console
(nothing printed yet)
The loop body runs once per halving. Make n twice as big (32) and the loop runs only one more time. · interactive

Reading Big-O from code

You rarely need to count exactly. Look at the shape of the loops instead. Here are four rules:

  • One after another → add. Two separate loops over n are n + n = 2n, which is O(n).
  • One inside another → multiply. A loop of n inside a loop of n is n × n = O(n²).
  • Halving or doubling → log. A loop variable that is multiplied or divided by 2 each time gives O(log n).
  • Two inputs → two letters. If there are two arrays of lengths n and m, write O(n + m) or O(n × m). Do not merge them into one n.
JavaScript
// O(1) — the same work for any nconst first = nums[0];// O(n) — one loop over the inputfor (let i = 0; i < n; i++) { /* ... */ }// O(n) — two loops one after the other: n + n = 2n → O(n)for (let i = 0; i < n; i++) { /* ... */ }for (let j = 0; j < n; j++) { /* ... */ }// O(n²) — a loop inside a loop: n × nfor (let i = 0; i < n; i++) {  for (let j = 0; j < n; j++) { /* ... */ }}// O(n²) — the inner loop starts at i + 1: about n²/2, still O(n²)for (let i = 0; i < n; i++) {  for (let j = i + 1; j < n; j++) { /* ... */ }}// O(log n) — the loop variable doubles (or n halves) each timefor (let i = 1; i < n; i *= 2) { /* ... */ }// O(n + m) — two different inputs, one after the otherfor (const a of arrA) { /* ... */ }for (const b of arrB) { /* ... */ }// O(n × m) — two different inputs, one inside the otherfor (const a of arrA) {  for (const b of arrB) { /* ... */ }}

Hidden loops in built-in methods

Some JavaScript methods look like one step, but they run a loop inside. If you put one of these inside your own loop, an O(n) solution becomes O(n²). You will not see a nested loop in your code, but it is there.

OperationCostWhy
arr[i], arr.length, push, popO(1)Works at one known position. (push and pop are O(1) on average)
Map / Set: get, set, has, add, deleteO(1) on averageJumps straight to the key (Lesson 15)
includes, indexOf, findO(n)Linear search through the array
shift, unshift, spliceO(n)Every later item may have to move one position (some engines can make shift faster, but you should not rely on it)
slice, [...arr], concat, joinO(n)Copies every item
new Set(arr), map, filter, reduceO(n)Visits every item
sortO(n log n)Sorting (Lessons 16–18)
JavaScript
// Looks like one loop, but it is really two.function commonValues(a, b) {  const result = [];  for (const x of a) {           // runs n times    if (b.includes(x)) {         // includes scans b: up to m steps      result.push(x);    }  }  return result;                 // O(n × m) in total}// Same result, but only one pass over each arrayfunction commonValuesFast(a, b) {  const inB = new Set(b);        // m steps, once  const result = [];  for (const x of a) {           // n times    if (inB.has(x)) {            // 1 step      result.push(x);    }  }  return result;                 // O(n + m)}

Space complexity

Time is not the only cost. Space complexity measures how much extra memory a solution needs as the input grows. We do not count the input itself. We use the same Big-O notation for it.

CodeExtra space
A few number variables (total, i, max)O(1)
A copy of the array, or a new array of resultsO(n)
A Set or Map holding every valueO(n)
An n × n gridO(n²)

Many improvements trade space for time. This means you use more memory to save time. In the hidden-loop example above, commonValuesFast uses an extra Set (O(m) space). In return, the time drops from O(n × m) to O(n + m). This is usually a good trade. Be ready to say it out loud, for example: “this is O(n) time and O(n) extra space”. Interviewers expect both numbers.

Best, average and worst case

The same code can do very different amounts of work for different inputs. Take linear search:

JavaScript
function indexOf(nums, target) {  for (let i = 0; i < nums.length; i++) {    if (nums[i] === target) return i;  }  return -1;}
Dry run · linear search on n items
CaseWhenSteps
Besttarget is the first item1 → O(1)
Averagetarget is somewhere in the middleabout n/2 → O(n)
Worsttarget is last, or not there at alln → O(n)

Unless someone says otherwise, we quote the worst case. It is the only one you can promise, because the best case depends on luck. The common exception is Map and Set. For them we quote the average case, O(1), because the worst case almost never happens in practice.

From constraints to approach

This is where Big-O helps you most in interviews. Take the constraints (Lesson 11) and the rule of thumb of about 108 simple steps per second. Then the size of n tells you which growth rate you need before you design the solution:

If n is at most…This is fast enoughTypical technique
about 10O(n!)Try every ordering (n! means n × (n−1) × … × 1, and a “permutation” is one ordering)
about 20O(2ⁿ)Try every subset (backtracking: try a choice, and go back if it fails)
about 500O(n³)Three nested loops
about 5,000O(n²)Check every pair
105 to 106O(n log n) or O(n)Sorting, one pass with a Map or Set, two pointers
109 or moreO(log n) or O(1)Binary search, a maths formula
Try it on Lesson 11

secondLargest allowed up to 105 values. O(n²) would be 1010 steps, which is far too slow. So we needed O(n log n) (sort) or O(n) (one pass). Both of our solutions are fast enough. In “Maximum product of two elements”, n was at most 500, so even the O(n²) pair check was fine.

Practice questions

For every solution below, say its time complexity and space complexity before you read the explanation. Saying both numbers should become automatic for you.

Q1Name the Big-OEasy

Give the time complexity of each snippet. Assume work() is O(1).

JavaScript
// Afor (let i = 0; i < n; i++)  for (let j = 0; j < n; j++) work();// Bfor (let i = 0; i < n; i++) work();for (let j = 0; j < n; j++) work();// Cfor (let i = 1; i < n; i *= 2) work();// Dfor (let i = 0; i < n; i++)  for (let j = 0; j < i; j++) work();// Efor (let i = 0; i < n; i++)  for (let j = 0; j < 5; j++) work();// Ffor (const x of nums) if (other.includes(x)) work();   // both arrays have length n
Example 1
Input: A
Output: O(n²)
Explanation: A loop of n inside a loop of n: n × n.
Example 2
Input: B
Output: O(n)
Explanation: Two loops one after the other: n + n = 2n, and we drop the constant 2.
Example 3
Input: C
Output: O(log n)
Explanation: i doubles each time: 1, 2, 4, 8 … It reaches n after about log₂ n steps.
Example 4
Input: D
Output: O(n²)
Explanation: The inner loop runs 0, 1, 2, … n − 1 times. That is about n²/2 in total, which is still quadratic.
Example 5
Input: E
Output: O(n)
Explanation: The inner loop always runs 5 times (a constant). So the total is 5n, which is O(n).
Example 6
Input: F
Output: O(n²)
Explanation: includes is a hidden loop over other. So the work is n × n.
Stuck? Open one hint at a time
Hint 1
Loops one after another: add. One loop inside another: multiply.
Hint 2
An inner loop with a fixed limit (like 5) is a constant. It is not another n.
Hint 3
If the loop variable is multiplied by 2 each time, how many steps does it take to pass n?
Show all 2 solutions with explanations
Approach 1Measure it
The idea

Count how many times work() runs for bigger and bigger n. Then see how the count changes when n is multiplied by 10.

JavaScript
function count(n) {  const c = { A: 0, B: 0, C: 0, D: 0, E: 0 };  for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) c.A++;  for (let i = 0; i < n; i++) c.B++;  for (let j = 0; j < n; j++) c.B++;  for (let i = 1; i < n; i *= 2) c.C++;  for (let i = 0; i < n; i++) for (let j = 0; j < i; j++) c.D++;  for (let i = 0; i < n; i++) for (let j = 0; j < 5; j++) c.E++;  return c;}console.log(count(10));   // { A: 100, B: 20, C: 4, D: 45, E: 50 }console.log(count(100));  // { A: 10000, B: 200, C: 7, D: 4950, E: 500 }console.log(count(1000)); // { A: 1000000, B: 2000, C: 10, D: 499500, E: 5000 }
How it works

Each time n becomes 10 times bigger, A and D become about 100 times bigger (quadratic). B and E become 10 times bigger (linear). C grows by only about 3 (logarithmic). Measuring like this is a good way to check your thinking while you learn.

Approach 2Reason from the shape
The idea

Look at the limit of each loop and how its variable changes. Then use the rules: add for loops one after another, multiply for loops inside loops.

JavaScript
// A  n × n                 → O(n²)// B  n + n = 2n             → O(n)// C  1, 2, 4 … < n          → O(log n)// D  0 + 1 + … + (n-1)      → n(n-1)/2 → O(n²)// E  n × 5                  → O(n)// F  n × (includes: n)      → O(n²)
How it works

This is what you do in an interview. You do not run the code. You only read its shape. All six snippets use only a few variables, so every one needs O(1) extra space.

Which approach should you use?

Most people get D and F wrong. D looks like “half as much work” as A, but halving is only a constant factor. F has only one loop that you can see, but includes is a second, hidden loop.

Q2Sum from 1 to nEasy

Return 1 + 2 + … + n. Constraints: 1 ≤ n ≤ 108.

Example 1
Input: 5
Output: 15
Explanation: 1 + 2 + 3 + 4 + 5 = 15.
Example 2
Input: 100000000
Output: 5000000050000000
Explanation: n = 10⁸. A loop needs 100 million steps. The formula needs one.
Stuck? Open one hint at a time
Hint 1
The loop is O(n). Is there a way that does not depend on n at all?
Hint 2
Pair the first and last numbers: 1 + n, 2 + (n − 1), and so on. Each pair adds up to n + 1. How many pairs are there?
Show all 2 solutions with explanations
Approach 1Loop
The idea

Add every number from 1 to n to a running total (an accumulator).

JavaScript
function sumTo(n) {  let total = 0;  for (let i = 1; i <= n; i++) total += i;  return total;}console.log(sumTo(5)); // 15
How it works

Time O(n), space O(1). For n = 108 this is 100 million additions. That takes roughly between a tenth of a second and a second in JavaScript.

Approach 2Formula
The idea

There are n/2 pairs, and each pair adds up to n + 1. So the total is n × (n + 1) / 2.

JavaScript
function sumTo(n) {  return (n * (n + 1)) / 2;}console.log(sumTo(5));         // 15console.log(sumTo(100000000)); // 5000000050000000
How it works

Time O(1), space O(1). It is one multiplication and one division, for any n. You used this formula in “Missing number” in Lesson 11.

Which approach should you use?

The constraints decide. If n ≤ 1,000, either way is fine. If n can be 109, only the O(1) formula is acceptable. This is the “109 or more” row of the table.

Q3Check if a number and its double existEasy

Return true if there are two different positions i and j with arr[i] === 2 * arr[j].

Example 1
Input: [10, 2, 5, 3]
Output: true
Explanation: 10 is double 5 (at positions 0 and 2).
Example 2
Input: [3, 1, 7, 11]
Output: false
Explanation: No value is double of another value.
Example 3
Input: [0, 0]
Output: true
Explanation: Edge case: 0 is double 0, and the two 0s are at different positions.
Example 4
Input: [0]
Output: false
Explanation: Edge case: a single 0 cannot pair with itself.
Stuck? Open one hint at a time
Hint 1
Brute force: check every pair. What is its Big-O?
Hint 2
For each value x, which earlier values would make a match? Both 2 * x and x / 2 would.
Hint 3
Keep the earlier values in a Set, so that each check is O(1).
Show all 2 solutions with explanations
Approach 1Check every pair
The idea

For every pair of different positions, check whether one value is double the other value.

JavaScript
function checkIfExist(arr) {  for (let i = 0; i < arr.length; i++) {    for (let j = 0; j < arr.length; j++) {      if (i !== j && arr[i] === 2 * arr[j]) return true;    }  }  return false;}console.log(checkIfExist([10, 2, 5, 3])); // trueconsole.log(checkIfExist([3, 1, 7, 11])); // falseconsole.log(checkIfExist([0, 0]));        // trueconsole.log(checkIfExist([0]));           // false
How it works

Time O(n²), space O(1). The check i !== j is what makes [0] return false.

Approach 2Set of values seen so far
The idea

For each value, ask the Set whether its double or its half was seen earlier. Then add the value to the Set.

JavaScript
function checkIfExist(arr) {  const seen = new Set();  for (const x of arr) {    if (seen.has(2 * x) || seen.has(x / 2)) return true;    seen.add(x);  }  return false;}console.log(checkIfExist([10, 2, 5, 3])); // trueconsole.log(checkIfExist([3, 1, 7, 11])); // falseconsole.log(checkIfExist([0, 0]));        // trueconsole.log(checkIfExist([0]));           // false
How it works

Time O(n), space O(n). We check before we add, so a value is never matched with itself. That is why [0] gives false and [0, 0] gives true. (For an odd x, x / 2 is a fraction, and a fraction is never in the Set.)

Which approach should you use?

With up to 500 values (the LeetCode limit), both pass. Still, give the Set version and say the trade: you use O(n) extra space to go from O(n²) to O(n) time. (LeetCode 1346.)

Q4Intersection of two arraysEasy

Return an array of the values that appear in both nums1 and nums2. Each value must appear only once in the result, in any order.

Example 1
Input: [1, 2, 2, 1], [2, 2]
Output: [2]
Explanation: 2 is in both arrays. Each value appears once in the result.
Example 2
Input: [4, 9, 5], [9, 4, 9, 8, 4]
Output: [9, 4]
Explanation: 4 and 9 are in both arrays. The statement says that any order is accepted.
Stuck? Open one hint at a time
Hint 1
There are two inputs, with lengths n and m. Use both letters in your Big-O.
Hint 2
Look for hidden loops. includes inside a loop is O(n × m).
Hint 3
Put one array into a Set. Then walk through the other array.
Show all 3 solutions with explanations
Approach 1includes inside a loop
The idea

For each value of the first array, check the second array with includes. Do not add a value twice.

JavaScript
function intersection(nums1, nums2) {  const result = [];  for (const x of nums1) {    if (nums2.includes(x) && !result.includes(x)) result.push(x);  }  return result;}console.log(intersection([1, 2, 2, 1], [2, 2]));       // [ 2 ]console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])); // [ 4, 9 ]
How it works

Time O(n × m), because of the hidden loop. There is also another hidden loop in result.includes. Space is O(1) beyond the result. The code is short, but it is slow for large inputs.

Approach 2Two Sets
The idea

Turn the first array into a Set. Walk through the second array. Collect the matches in another Set, so that duplicates disappear.

JavaScript
function intersection(nums1, nums2) {  const inFirst = new Set(nums1);  const result = new Set();  for (const x of nums2) {    if (inFirst.has(x)) result.add(x);  }  return [...result];}console.log(intersection([1, 2, 2, 1], [2, 2]));       // [ 2 ]console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])); // [ 9, 4 ]
How it works

Time O(n + m): one pass to build the Set and one pass to check. Space O(n) for the Set.

Approach 3Sort both, walk together
The idea

Sort both arrays. Then use one position (a pointer) in each array. Always move the pointer that points to the smaller value.

JavaScript
function intersection(nums1, nums2) {  const a = [...nums1].sort((x, y) => x - y);  const b = [...nums2].sort((x, y) => x - y);  const result = [];  let i = 0, j = 0;  while (i < a.length && j < b.length) {    if (a[i] < b[j]) i++;    else if (a[i] > b[j]) j++;    else {      if (result[result.length - 1] !== a[i]) result.push(a[i]);      i++;      j++;    }  }  return result;}console.log(intersection([4, 9, 5], [9, 4, 9, 8, 4])); // [ 4, 9 ]
How it works

Time O(n log n + m log m) for the sorting. The walk itself is only O(n + m). This is a first look at the two-pointer pattern (Lesson 21).

Which approach should you use?

Two Sets is the standard answer. It is the fastest and it is easy to explain. If the interviewer adds “both arrays are already sorted”, Approach 3 needs only O(n + m) time and no Set. (LeetCode 349.)

Q5Count the digitsEasy

Given a whole number n (0 ≤ n ≤ 109), return how many digits it has.

Example 1
Input: 7
Output: 1
Explanation: One digit.
Example 2
Input: 12345
Output: 5
Explanation: Five digits.
Example 3
Input: 0
Output: 1
Explanation: Edge case: 0 still has one digit, even though the loop would not run for 0.
Stuck? Open one hint at a time
Hint 1
Use the digit loop from Lesson 5: divide by 10 until nothing is left.
Hint 2
How many times can you divide n by 10? The answer is a logarithm.
Show all 3 solutions with explanations
Approach 1Divide by 10
The idea

Remove the last digit with Math.floor(n / 10) (it divides by 10 and rounds down). Repeat until n is 0, and count each removal.

JavaScript
function countDigits(n) {  if (n === 0) return 1;  let count = 0;  while (n > 0) {    n = Math.floor(n / 10);    count++;  }  return count;}console.log(countDigits(7));     // 1console.log(countDigits(12345)); // 5console.log(countDigits(0));     // 1
How it works
Dry run · countDigits(12345)
ncount
123450
12341
1232
123
14
05

Time O(log n): the loop runs once per digit, and a number n has about log₁₀ n digits. Space O(1).

Approach 2Convert to a string
The idea

Turn the number into a string with String(n). The length of the string is the number of digits.

JavaScript
function countDigits(n) {  return String(n).length;}console.log(countDigits(12345)); // 5console.log(countDigits(0));     // 1
How it works

This is also O(log n), because building the string still visits every digit. The work is just hidden inside String. Space is O(log n) for the string.

Approach 3Logarithm formula
The idea

A number with d digits is at least 10d−1 and less than 10d. So d = floor(log₁₀ n) + 1. (floor rounds down.)

JavaScript
function countDigits(n) {  if (n === 0) return 1;  return Math.floor(Math.log10(n)) + 1;}console.log(countDigits(7));     // 1console.log(countDigits(12345)); // 5console.log(countDigits(1000));  // 4
How it works

This shows exactly where the “log” comes from: the digit count is based on the logarithm. Math.log10(0) is -Infinity, so 0 still needs its own check. (Computers store decimals with tiny rounding errors. For n up to 109 this formula is fine, but Approach 1 never has this risk.)

Which approach should you use?

All three are correct. In an interview, give Approach 1 and say “O(log n) time, because each step removes one digit”. That sentence is the point of the question. (Related: LeetCode 1295.)

Q6Reverse an array — time and spaceEasy

Reverse the order of the items in nums. Give the time and the extra space of each solution.

Example 1
Input: [1, 2, 3, 4, 5]
Output: [5, 4, 3, 2, 1]
Explanation: The order is reversed.
Example 2
Input: []
Output: []
Explanation: Edge case: nothing to reverse.
Stuck? Open one hint at a time
Hint 1
Both obvious solutions take O(n) time. Compare how much extra memory they use.
Hint 2
Swap the first and last items. Then swap the second and the second from last, and move inwards.
Show all 2 solutions with explanations
Approach 1Build a new array
The idea

Walk from the end to the start. Push each item into a new array.

JavaScript
function reversed(nums) {  const out = [];  for (let i = nums.length - 1; i >= 0; i--) out.push(nums[i]);  return out;}console.log(reversed([1, 2, 3, 4, 5])); // [ 5, 4, 3, 2, 1 ]console.log(reversed([]));              // []
How it works

Time O(n), space O(n) for the new array. The original array is unchanged. Sometimes that is exactly what you want.

Approach 2Swap in place
The idea

Use two positions, one at each end. Swap their items. Move both positions inwards until they meet.

JavaScript
function reverseInPlace(nums) {  let left = 0;  let right = nums.length - 1;  while (left < right) {    const tmp = nums[left];    nums[left] = nums[right];    nums[right] = tmp;    left++;    right--;  }  return nums;}console.log(reverseInPlace([1, 2, 3, 4, 5])); // [ 5, 4, 3, 2, 1 ]console.log(reverseInPlace([]));              // []
How it works

Time O(n), because there are about n/2 swaps. Space O(1), because it uses only left, right and tmp, however big the array is.

Which approach should you use?

The time is the same, but the space is different. If the statement says “in place” or “O(1) extra space”, only Approach 2 is accepted. If it says “do not modify the input”, only Approach 1 is accepted. This is a clarifying question that is worth asking (Lesson 11).

Q7Find all missing numbersMedium

nums has n values, each between 1 and n. Some values appear twice and some do not appear at all. Return every number from 1 to n that does not appear.

Example 1
Input: [4, 3, 2, 7, 8, 2, 3, 1]
Output: [5, 6]
Explanation: n = 8, so the values should be 1 to 8. The numbers 5 and 6 never appear (2 and 3 appear twice).
Example 2
Input: [1, 1]
Output: [2]
Explanation: n = 2: 2 is missing.
Stuck? Open one hint at a time
Hint 1
Brute force: for each k from 1 to n, check nums.includes(k). What is the Big-O?
Hint 2
A Set of the values that are present gives O(n) time with O(n) space.
Hint 3
For O(1) extra space: every value v points to position v − 1. Mark that position by making its number negative. A position that is still positive at the end was never pointed to.
Show all 3 solutions with explanations
Approach 1includes for each candidate
The idea

For every number from 1 to n, check with includes whether it is in the array.

JavaScript
function findDisappearedNumbers(nums) {  const result = [];  for (let k = 1; k <= nums.length; k++) {    if (!nums.includes(k)) result.push(k);  }  return result;}console.log(findDisappearedNumbers([4, 3, 2, 7, 8, 2, 3, 1])); // [ 5, 6 ]
How it works

Time O(n²), because there are n candidates and each one has a hidden O(n) search. Space O(1). With n up to 105, that is about 1010 steps, which is too slow.

Approach 2Set of present values
The idea

Put all values in a Set. Then check each number from 1 to n.

JavaScript
function findDisappearedNumbers(nums) {  const present = new Set(nums);  const result = [];  for (let k = 1; k <= nums.length; k++) {    if (!present.has(k)) result.push(k);  }  return result;}console.log(findDisappearedNumbers([4, 3, 2, 7, 8, 2, 3, 1])); // [ 5, 6 ]console.log(findDisappearedNumbers([1, 1]));                   // [ 2 ]
How it works

Time O(n), space O(n). Most interviewers expect this answer first.

Approach 3Mark positions in place
The idea
  1. For each value v, make the number at position v − 1 negative (if it is not already negative).
  2. Use Math.abs (it removes the minus sign) when you read a value, because earlier steps may have made it negative.
  3. If position i is still positive at the end, the value i + 1 never appeared.
JavaScript
function findDisappearedNumbers(nums) {  for (const x of nums) {    const i = Math.abs(x) - 1;    if (nums[i] > 0) nums[i] = -nums[i];  }  const result = [];  for (let i = 0; i < nums.length; i++) {    if (nums[i] > 0) result.push(i + 1);  }  return result;}console.log(findDisappearedNumbers([4, 3, 2, 7, 8, 2, 3, 1])); // [ 5, 6 ]console.log(findDisappearedNumbers([1, 1]));                   // [ 2 ]
How it works

Time O(n), extra space O(1). The result array is the output, so we do not count it. The cost is that the input array is changed. So ask first whether that is allowed.

Which approach should you use?

This is a good example of trading space for time, and then trading back. The three solutions are O(n²) time and O(1) space, then O(n) time and O(n) space, then O(n) time and O(1) space. Give the Set version first. Mention the in-place version if the interviewer asks for O(1) extra space. (LeetCode 448.)

Make it stick

Make it stick — close this page, then
  1. Write the six growth rates in order, from fastest to slowest, with one example of each.
  2. Explain why a loop that halves n is O(log n), using 16 as the example.
  3. Name three array methods that hide an O(n) loop, and say why putting them inside a loop is a problem.
  4. From memory, fill in the “if n is at most… then this is fast enough” table.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

What's next

You can now read a problem carefully and judge how fast a solution must be. Lesson 13 covers basic maths that appears in many problems: divisors, prime numbers, the greatest common divisor and fast powers. Several of these ideas turn an O(n) loop into O(√n) or O(log n). You now have the words to say exactly why that matters.

Practise on LeetCode

Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series