Arrays
The most fundamental way to store an ordered list of items in memory.
What is it?
Imagine a row of numbered storage slots sitting right next to each other, so that knowing a slot's number lets you jump straight to it without checking any of the others first. That's the idea behind an array: a way to store a group of values right next to each other in memory, in order, so you can find any item instantly if you know its position (its index). It's one of the most basic building blocks for almost every other data structure.
Because array items sit next to each other in memory, reading any item by its index is extremely fast — but inserting or removing an item in the middle can be slow, since everything after it may need to shift.
Explain like I'm 10
An array is like a row of parking spaces, each numbered. If you know the space number, you can walk straight to that car. But if you need to add a car in the middle of a full row, every car after it has to shift over one space.
Examples
Reading vs inserting
const arr = [10, 20, 30, 40];
console.log(arr[2]); // 30 — instant, O(1)
arr.splice(1, 0, 15); // insert 15 at index 1
// arr is now [10, 15, 20, 30, 40] — everything after index 1 had to shiftHow it works
Because array elements are stored in one continuous block of memory, accessing arr[i] is a direct calculation ("jump to this exact spot") — constant time, or O(1). Inserting or deleting somewhere other than the end requires shifting every following element over by one, which takes time proportional to the array's size, or O(n).
Why does it exist?
Fast, predictable access by position is essential for countless problems — searching, sorting, storing sequences of steps or events. Arrays are the default choice whenever order matters and you mostly need to read items rather than insert them in the middle.
When to use it
Reach for an array when you need fast, predictable access to items by their position, and you mostly read data rather than insert into the middle — a leaderboard, a list of recent events, a lookup table by index.
When not to use it
If your program frequently inserts or removes items from the front or middle of a large collection, an array's O(n) shifting cost adds up — a linked list (or a different structure entirely) may be a better fit.
Common mistakes
Repeatedly inserting or removing items from the front of a large array, which is slower than it looks.
Assuming array search by value is instant — finding a value (not an index) still requires checking items one by one, O(n).
Forgetting that in JavaScript, arrays can hold mixed types, which is convenient but easy to misuse.
Practice exercises
- Easy:
Write a function that returns the largest number in an array.
- Medium:
Write a function that reverses an array without using the built-in
.reverse()method. - Hard:
Write a function that removes duplicate values from an array while preserving the original order.
Interview questions
Why is accessing an array by index O(1)?
Because array elements sit in contiguous memory, so the position of any index can be calculated directly, without searching.
Why is inserting into the middle of an array O(n)?
Because every element after the insertion point has to shift over by one position to make room.
When would you choose an array over a linked list?
When you need fast, random access to elements by index and don't need to frequently insert or remove items from the middle.
Why is `array.push()` described as *amortized* O(1) rather than plain O(1)?
Most calls to push just place the new element in the next available slot — O(1). But a JavaScript array's underlying storage has a fixed capacity at any moment; when it fills up, the engine allocates a new, larger block of memory and copies every existing element into it, which costs O(n) for that single push. Because capacity typically doubles on each resize, these expensive copies happen exponentially less often as the array grows, so the average cost per push, spread over many pushes, still comes out to O(1).
Why is `array.pop()` O(1) but `array.shift()` (removing the first element) is O(n)?
pop() only touches the last element — it reads it and decrements the length, and nothing else needs to move. shift() removes the first element, which leaves a gap at index 0, so every remaining element must shift one position to the left to close that gap and keep indices contiguous — that shifting is proportional to the array's size.
Why is `array.unshift()` O(n)?
Inserting a new element at index 0 requires first shifting every existing element one position to the right to make room for it at the front, before the new element can be written in — that shift touches every element currently in the array.
What does `[10, 20, 30, 40, 50].slice(1, 3)` return, and what's the time complexity of `slice`?
It returns [20, 30] — elements starting at index 1 up to, but not including, index 3. Its time complexity is O(k), where k is the number of elements copied into the new array (up to O(n) if slicing the whole array), since each included element must be copied.
What's the key difference between `.slice()` and `.splice()`?
.slice() returns a new array containing a shallow copy of a portion of the original, without modifying it. .splice() mutates the original array directly — removing and/or inserting elements at a given position — and returns any removed elements.
Why is copying an array with `[...arr]` called a *shallow* copy?
It creates a new top-level array and copies each element's value into it — but if an element is itself an object or array, only the reference to that nested object is copied, not a separate duplicate of it. So the original and the copy still both point to the same nested object, and mutating that nested object through either one affects both.
Why can't you reliably use `===` to check if two arrays contain the same values?
=== on arrays checks reference equality — whether both sides point to the exact same object in memory — not whether their contents match. Two separately-created arrays with identical elements are still different objects, so [1,2] === [1,2] is false; you'd need to compare their contents element by element (or use a helper) instead.
What is the time complexity of `Array.prototype.sort()` in modern JavaScript engines?
O(n log n) in the average and worst case — modern engines (e.g. V8) use hybrid algorithms like Timsort, which is comparison-based sorting, and any comparison-based sort requires at least O(n log n) comparisons to guarantee a fully ordered result in the general case.
Why is binary search O(log n) while a plain search through an unsorted array is O(n) — and what does binary search require that the other doesn't?
Linear search has no information about where a value might be, so in the worst case it checks every element. Binary search compares the target to the middle element and, based on that, discards half of the remaining elements each step — but this only works because the array is sorted, which is what guarantees the target must be entirely in one particular half.
If you only need to search an array once, is it worth sorting it first so you can binary search?
No — sorting costs O(n log n) up front, which is already more expensive than a single O(n) linear scan. Sorting-then-binary-search only pays off when you'll search the same array many times, since the one-time sorting cost gets amortized across many fast O(log n) searches afterward.
What's the time and space complexity of reversing an array in place with two pointers (one at each end, swapping and moving inward)?
O(n) time — each element is visited once as the pointers move toward the middle — and O(1) extra space, since the swaps happen directly within the existing array instead of allocating a new one.
What's wrong with this loop meant to print every element of an array — `for (let i = 0; i <= arr.length; i++) { console.log(arr[i]); }`?
It's an off-by-one bug: using <= lets i reach arr.length, which is one index past the last valid element (valid indices only go up to arr.length - 1), so the final iteration logs undefined. The condition should be i < arr.length.
Why does `delete arr[2]` behave differently from `arr.splice(2, 1)`?
delete removes the value stored at that index but leaves a hole there (the slot becomes empty/undefined) without shifting later elements or updating .length. splice actually removes the element, shifts every subsequent element left by one, and correctly shrinks .length by one.
What's the time complexity of merging two arrays with `.concat()` or the spread operator?
O(n + m), where n and m are the lengths of the two arrays, since a brand-new array is created and every element from both source arrays must be copied into it.
What's the time complexity of finding the maximum value in an unsorted array versus in an array already sorted ascending?
Unsorted: O(n) — every element must be checked, since any one of them could be the maximum. Sorted ascending: O(1) — the maximum is guaranteed to be the last element, so no search is needed at all.
If you need to repeatedly check whether values exist in a collection, why might converting the array to a `Set` first be worth it?
array.includes() is O(n) per check, since it may scan the whole array. Converting to a Set costs O(n) once, but afterward each lookup is O(1) on average, since a Set uses hashing to jump directly to where a value would be. If you're going to do many lookups, the one-time conversion cost is easily paid back.
For the classic "two sum" problem (find two numbers in an array that add up to a target), what's the time and space complexity of a brute-force approach versus a hash-map approach?
Brute force checks every pair with nested loops: O(n²) time, O(1) extra space. The hash-map approach makes a single pass, and for each number checks whether target - number was already seen (stored in the map): O(n) time, O(n) space — trading memory for a large speed-up.
For a 2D array (an array of arrays) representing a grid, what's the time complexity of accessing `grid[i][j]`?
O(1) — grid[i] jumps directly to the i-th row in constant time, and then [j] jumps directly to that row's j-th element in constant time; two O(1) operations combined are still O(1), independent of the grid's size.
What's the space complexity of a 2D array with n rows and m columns?
O(n × m) — one storage slot is needed for every combination of row and column, so total space scales with the product of the two dimensions, not just their sum.
A `for` loop and `.forEach()` are both O(n) to iterate an array — so why might the plain loop run measurably faster?
Big O only measures how work scales with input size, not the actual constant-factor cost per operation. .forEach() invokes a callback function on every single iteration, which carries extra overhead compared to a plain loop body — both are O(n), but with different constant factors, which Big O deliberately ignores.
Are `Array.isArray()` and reading `.length` ever more expensive than O(1)?
No — both are O(1) in JavaScript. .length is a property maintained automatically as elements are added or removed, not recalculated by counting; Array.isArray() just checks the object's internal type tag, not its contents.
Both arrays and linked lists take O(n) to fully traverse — so why do arrays tend to iterate faster in practice?
Array elements sit in one contiguous block of memory, so iterating them benefits from CPU cache locality — the processor can pre-fetch nearby memory it's likely to need next. A linked list's nodes can be scattered anywhere in memory, so each step may require a slower, uncached memory access — same Big O, different real-world constant factor.
What's the time complexity of rotating an array left by one position by removing the first element and pushing it to the end?
O(n) overall — shift() to remove the first element costs O(n), since every remaining element shifts left by one, and push() to add it at the end costs amortized O(1); the shift dominates, so the whole operation is O(n).
Finding duplicate values in an unsorted array with nested loops is O(n²) — how does using a hash set reduce that to O(n)?
The nested-loop approach compares every element to every other element, doing roughly n² comparisons. With a hash set, you make one pass through the array, checking whether each element is already in the set (O(1) average) before adding it — reducing the total work to O(n) time, at the cost of O(n) extra space for the set.
What's the worst-case time complexity of `array.indexOf()`, and when does that worst case happen?
O(n) — the worst case is when the target value is the very last element or isn't in the array at all, forcing every element to be checked before the search can conclude.
Pushing n elements one at a time versus preallocating an array of size n and assigning by index — do they have different time complexity?
No, both are O(n) overall: pushing is amortized O(1) per call, so n pushes total O(n); assigning to a preallocated slot by index is also O(1) per assignment, so n assignments total O(n). Preallocating can avoid some of the occasional O(n) resize-and-copy operations that array growth triggers, which may lower the constant factor, but it doesn't change the asymptotic complexity.