An array is the simplest data structure there is, and understanding exactly what makes it fast explains most of the others by contrast.
One multiplication
The elements sit in memory back to back, all the same size. So the computer never searches for element 5. It calculates where it is:
address = start + 5 × bytesPerElementOne multiply and one add, regardless of whether the array has ten elements or ten million. That is what indexing means, and it is the only reason arrays are worth anything.
What contiguity costs
Everything is back to back, so inserting in the middle means physically shifting everything after it:
values.splice(2, 0, 99); // everything from index 2 rightwards moves one slot. Same for deleting from the middle. Adding at the end is cheap, because nothing is in the way.
Which gives the table people memorise, now with a reason attached:
| Cost | Because | |
|---|---|---|
Read a[i] | arithmetic | |
Overwrite a[i] | arithmetic | |
| Push / pop at the end | amortised | nothing is in the way |
| Insert / delete in the middle | everything after it shifts | |
| Search unsorted | no structure to exploit | |
| Search sorted | binary search |
“Amortised” is doing real work in that table
A fixed block of memory cannot grow. So when a dynamic array — Array in
JavaScript, vector in C++, list in Python — runs out of room, it allocates a
bigger block and copies everything across.
If it grew by one slot each time, every single push would copy the whole array and pushes would cost . Instead it doubles. Watch where the copies land:
1 items in a slot array of 1
- copies this push
- 0
- copies so far
- 0
- average per push
- 0.00
There was a free slot, so this push is a single write — the cheap case, and it is the case almost every time. The running average stays under 2 no matter how far you go.
The distinction matters in exactly one place: hard real-time code, where a single slow operation is unacceptable even if the average is fine. Everywhere else, amortised is the number you should reason with.
Strings are arrays with two complications
They are usually immutable. In JavaScript, Java, Python and C#, a string
cannot be changed in place. Every += builds a whole new string:
let out = '';for (const ch of input) out += ch; // O(n²) — a fresh copy every iterationFor a 100,000-character input that is five billion character copies. Collect into an array and join once instead:
const parts: string[] = [];for (const ch of input) parts.push(ch);const out = parts.join(''); // O(n)The one precomputation worth knowing
If you will be asked “what is the sum of the range i to j?” many times,
build a prefix sum array once:
const prefix = [0];for (const v of values) prefix.push(prefix[prefix.length - 1]! + v);
// Sum of values[i..j] inclusive, in one subtraction:const rangeSum = (i: number, j: number) => prefix[j + 1]! - prefix[i]!;prefix[k] is the sum of the first k values. Everything before i is in both
totals, so subtracting cancels it exactly and leaves the middle.
to build, per query. It converts a nested loop into two passes often enough that it is worth recognising the shape: many range questions over data that does not change.
In JavaScript specifically
Array is not really an array. It is an object with integer-ish keys, and the
engine keeps it as a genuine contiguous block only while you behave:
const a = [1, 2, 3];a[1000] = 4; // now sparse — the engine switches to a dictionarydelete a[1]; // creates a hole; use splice if you mean "remove"Once it converts, indexing is a hash lookup and the cache benefit is gone. Keep
arrays dense, keep the element types uniform, and reach for Int32Array or
Float64Array when you want a guarantee rather than a hope.