Emmanuel Akinfulubi

Lesson 02 of 2414 min read

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].

Indexing is O(1), whatever the position

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.

Why shift is O(n): watch the elements move

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

Five ways to make an array, and when to use each

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


      

See the shared-row bug happen

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

The loops worth knowing, and what each gives you

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

map, filter, reduce: one pass each

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

Shallow copies share nested objects

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.

Which methods mutate

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.

Removing while looping: forwards is wrong, backwards is right

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.

In-place removal in one pass, O(1) extra space

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.

Editable. Fill it in, then run.

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)), never fill with 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.

Saved in this browser only.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.