Start learning free

JAVASCRIPT · BIG O NOTATION

Time Complexity Practice 1 in JavaScript, explained

Why built-ins have a cost

A line like arr.includes(x) looks like one step, but it is a loop that someone else wrote. When you estimate the Big O time of your code, every built-in call counts for the work it does inside. The classic trap is a built-in called inside a loop: a loop that runs n times and calls a method costing O(n) each time takes O(n²), even though the code shows only one loop.

function hasDuplicate(a) {
  for (let i = 0; i < a.length; i++) {
    if (a.slice(i + 1).includes(a[i])) return true;
  }
  return false;
}

Both slice (which copies part of the array) and includes (which scans it) cost O(n). They run once per pass of a loop that runs n times, so the function is O(n²). Using a Set fixes it:

function hasDuplicate(a) {
  const seen = new Set();
  for (const v of a) {
    if (seen.has(v)) return true;
    seen.add(v);
  }
  return false;
}

Now each pass does a lookup and an insert, both constant time on average, so the total is O(n). Both versions give true for [1, 2, 3, 1] and false for [1, 2, 3].

Arrays

Below, n is the number of elements in the array. The rule of thumb: an operation that touches only the end of an array is cheap, and one that has to shift or visit many elements is not.

OperationTimeWhy
arr[i], arr.lengthO(1)Direct access by index
push, popO(1)Work at the end only (push is O(1) amortized)
shift, unshiftO(n)Every other element moves by one position
spliceO(n)Elements after the change move
indexOf, includes, findO(n)Scans until it finds a match
slice, concat, [...arr]O(n)Copies elements
map, filter, forEach, reduce, join, reverseO(n)Visit every element once
sortO(n log n)Comparison sort

Two of these are easy to underestimate. First, shift() removes the first element and moves everything else down, so using an array as a queue with shift in a loop is O(n²) in theory. (Engines can optimize this for some arrays, but you should not rely on that when you reason about complexity.) Second, sort is O(n log n) in comparisons, and calling it inside a loop multiplies that cost.

Also note what sort compares. With no comparator it sorts as strings, so [10, 9, 1].sort() gives [1, 10, 9]. For numbers, pass a comparator: [10, 9, 1].sort((x, y) => x - y) gives [1, 9, 10].

Strings

Strings are immutable, so any operation that changes or builds a string produces a new one and costs time proportional to its length:

  • s.length and s[i] are O(1).
  • indexOf, includes and split scan the string, so they are O(n). Searching for a text of length m in a string of length n can take up to O(n * m) in the worst case for a simple search.
  • slice, substring, toUpperCase, trim, replace and concat produce a new string, so think O(n) for each.
  • Building a result with += in a loop can look like O(n²), since each step conceptually copies the string so far. Engines often optimize this, but if you need a guarantee, collect the pieces in an array and call join("") once at the end, which is O(n).

Maps, sets and objects

Map and Set are built on hash tables. get, set, has, add and delete are O(1) on average. Reading map.size is O(1), and iterating over all entries is O(n). The word "average" matters: in the worst case a hash table can degrade, but for the problems in this course you can treat these operations as constant time.

Plain objects used as dictionaries behave similarly: obj[key], assigning to it, key in obj and delete obj[key] are constant time on average. Object.keys(obj), Object.values(obj) and Object.entries(obj) build an array of all the keys, values or entries, so they cost O(n) and should not be called inside a loop that runs for every key.

Hidden loops

These calls look like single operations and are not:

  • Math.max(...arr) spreads the array into arguments and then looks at each one: O(n). Very large arrays can also exceed the engine's limit on the number of arguments.
  • [...arr] and Array.from(arr) copy the array: O(n).
  • JSON.stringify(obj) and JSON.parse(text) are proportional to the size of the data.

How to use this when you write code

  1. Count the loops. Look at every loop, and at every built-in call inside it.
  2. Multiply a call's cost by how often it runs. An O(n) call inside an n-pass loop is O(n²).
  3. Replace repeated scans with lookups. If you keep asking "have I seen this value?", store values in a Set or a Map instead of searching an array.
  4. Keep the dominant term. Work that is O(n) plus O(n log n) for a sort is O(n log n) overall.

Key takeaways

  • Built-in methods hide loops: includes, indexOf, slice, concat, shift and splice are all O(n).
  • push, pop and index access are O(1).
  • sort is O(n log n), and without a comparator it sorts as strings.
  • Map, Set and object lookups are O(1) on average, which makes them the standard tool for removing a nested scan.
  • When a built-in sits inside a loop, multiply the two costs.
Keep going with JavaScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
Start learning free