Arrays: the One You Already Use
An array is a numbered row of slots. Knowing which operations are cheap and which quietly cost O(n) is most of what separates fast code from slow code.
By the end: Predict the cost of any array operation before you write it.
An array is a row of numbered slots, laid out one after another in memory. That single fact explains every cost in this lesson.
Because the slots are adjacent and all the same size, the computer can find slot i with arithmetic: start address, plus i times the slot size. No searching. That is why reading arr[500000] is exactly as fast as reading arr[0].
const big = Array.from({ length: 1000000 }, (_, i) => i * 2);
console.log('first: ', big[0]);
console.log('middle: ', big[500000]);
console.log('last: ', big[999999]);
console.log('length: ', big.length);
// All four of those are a single arithmetic step. Position is irrelevant.
Output
The cost of every operation
Here is the table you should be able to reproduce from memory. n is the array length.
arr[i]read or write —O(1)arr.push(x)—O(1)amortizedarr.pop()—O(1)arr.unshift(x)—O(n), everything shifts rightarr.shift()—O(n), everything shifts leftarr.splice(i, ...)—O(n)arr.includes / indexOf / find—O(n)arr.slice(),[...arr],concat—O(n)arr.sort()—O(n log n)arr.reverse()—O(n)
function shiftByHand(arr) {
const removed = arr[0];
let moves = 0;
for (let i = 0; i < arr.length - 1; i++) {
arr[i] = arr[i + 1]; // every single element steps left
moves++;
}
arr.length--;
return { removed, moves };
}
const a = [10, 20, 30, 40, 50];
console.log('before:', [...a]);
console.log(shiftByHand(a));
console.log('after: ', a);
// The bigger the array, the more moves. That is the whole reason it is O(n).
for (const n of [10, 100, 1000]) {
const list = Array.from({ length: n }, (_, i) => i);
console.log('n =', String(n).padStart(4), '→ moves:', shiftByHand(list).moves);
}
Output
Building arrays
console.log('literal: ', [1, 2, 3]);
console.log('n zeros: ', new Array(5).fill(0));
console.log('n nulls: ', Array.from({ length: 5 }, () => null));
console.log('0..4: ', Array.from({ length: 5 }, (_, i) => i));
console.log('squares: ', Array.from({ length: 5 }, (_, i) => i * i));
// A 2D grid. This matters: the naive version shares one row.
const rows = 3, cols = 4;
const grid = Array.from({ length: rows }, () => new Array(cols).fill(0));
grid[1][2] = 9;
console.log('correct grid: ', JSON.stringify(grid));
Output
const broken = new Array(3).fill(new Array(4).fill(0));
broken[1][2] = 9;
console.log('broken:', JSON.stringify(broken));
console.log('all three rows are the same object:', broken[0] === broken[1]);
const fixed = Array.from({ length: 3 }, () => new Array(4).fill(0));
fixed[1][2] = 9;
console.log('fixed: ', JSON.stringify(fixed));
console.log('rows are separate objects:', fixed[0] === fixed[1]);
Output
Iterating
const arr = ['a', 'b', 'c'];
for (let i = 0; i < arr.length; i++) console.log('index loop:', i, arr[i]);
for (const v of arr) console.log('for...of gives values:', v);
for (const [i, v] of arr.entries()) console.log('entries gives both:', i, v);
// A classic mistake: for...in gives you KEYS, as strings, and is for objects.
for (const k of Object.keys(arr)) console.log('key is a string:', k, typeof k);
Output
The methods that carry their weight
const nums = [1, 2, 3, 4, 5, 6];
console.log('map: ', nums.map(x => x * x));
console.log('filter: ', nums.filter(x => x % 2 === 0));
console.log('reduce: ', nums.reduce((sum, x) => sum + x, 0));
// reduce is the general one: it can build anything, including an object.
const byParity = nums.reduce((acc, x) => {
const key = x % 2 === 0 ? 'even' : 'odd';
(acc[key] ||= []).push(x);
return acc;
}, {});
console.log('grouped:', byParity);
// Chaining is readable but each link is another full pass.
console.log('chained:', nums.filter(x => x % 2).map(x => x * 10).reduce((a, b) => a + b, 0));
Output
Chaining three methods means three passes, which is O(3n) and therefore still O(n). That is fine. Readability usually wins. But if you are in a hot loop and an interviewer asks you to reduce passes, a single for loop doing all three jobs is the answer.
Copying, and the trap underneath it
const original = [{ id: 1, tags: ['a'] }, { id: 2, tags: ['b'] }];
const shallow = [...original];
shallow[0].id = 99; // reaches through into the original
console.log('after shallow edit, original[0].id =', original[0].id);
const deep = structuredClone(original);
deep[1].id = 77;
console.log('after deep edit, original[1].id =', original[1].id);
// For a flat array of numbers or strings, a shallow copy is genuinely enough.
const flat = [1, 2, 3];
const flatCopy = [...flat];
flatCopy[0] = 99;
console.log('flat original untouched:', flat);
Output
In-place versus returning a new array
Some methods change the array you call them on. Others hand back a new one and leave the original alone. Mixing them up produces bugs that are hard to see.
const list = [3, 1, 2];
const sortedCopy = [...list].sort((a, b) => a - b);
console.log('copy sorted:', sortedCopy, '| original still:', list);
list.sort((a, b) => a - b);
console.log('sort mutates:', list);
console.log('--- mutating: push pop shift unshift splice sort reverse fill');
console.log('--- returning new: map filter slice concat flat join toSorted');
// toSorted / toReversed are the newer non-mutating versions, if available.
if (typeof [].toSorted === 'function') {
const t = [3, 1, 2];
console.log('toSorted:', t.toSorted((a, b) => a - b), '| original:', t);
} else {
console.log('toSorted not available in this browser; use [...arr].sort()');
}
Output
Two array habits that pay off in interviews
Walk backwards when you remove. Removing while iterating forwards makes you skip elements, because everything shifts down into the index you just passed.
const wrong = [1, 2, 2, 3, 2, 4];
for (let i = 0; i < wrong.length; i++) {
if (wrong[i] === 2) wrong.splice(i, 1); // skips the neighbour
}
console.log('forwards, buggy:', wrong);
const right = [1, 2, 2, 3, 2, 4];
for (let i = right.length - 1; i >= 0; i--) {
if (right[i] === 2) right.splice(i, 1); // indices below i are untouched
}
console.log('backwards, correct:', right);
// Usually the best answer is not to remove at all: build a new array.
console.log('filter, clearest:', [1, 2, 2, 3, 2, 4].filter(x => x !== 2));
Output
Write in place with a slow pointer. When a problem says "modify the array in place and return the new length", the technique is one write index trailing one read index. This is the foundation of lesson 05.
function removeAll(arr, value) {
let write = 0;
for (let read = 0; read < arr.length; read++) {
if (arr[read] !== value) {
arr[write] = arr[read];
write++;
}
}
arr.length = write; // everything from write onwards is discarded
return write;
}
const data = [3, 2, 2, 3, 4, 2, 5];
const newLength = removeAll(data, 2);
console.log('new length:', newLength, '| array:', data);
Output
Your turn: rotate an array
Write rotate(arr, k) that moves every element k places to the right, wrapping around. rotate([1,2,3,4,5], 2) should give [4,5,1,2,3].
Do it twice. First the easy way with slice and concat, which costs O(n) extra space. Then in place with O(1) extra space, using the reverse trick: reverse the whole array, then reverse the first k, then reverse the rest. Handle k larger than the length.
Output
Recap
- An array is adjacent numbered slots, which is why indexing is
O(1)at any position. - Cheap at the end (
push,pop), expensive at the front or middle (shift,unshift,splice) because elements shift. - Build 2D grids with
Array.from({length: r}, () => new Array(c).fill(0)), neverfillwith an array, or every row is the same object. - Know which methods mutate and which return a new array;
[...arr].sort()when you must not disturb the original. - To modify in place, use a write index trailing a read index: one pass,
O(1)extra space.