Arrays & strings
An array keeps its items side by side, so any index is one step away. The same layout makes inserting in the middle expensive, and it explains why building a string one piece at a time can be slow.
An array is one block of memory, so every index is one step away
An array stores its items next to each other in one block of memory, and every item takes the same number of bytes. An item's position is its index, counted from 0: the first item is a[0], the fourth is a[3].
Because the items are packed and equal in size, the computer never has to walk the array to find one. It works out where item i lives:
address of a[i] = start of the block + i × size of one item
That is one multiplication and one addition, whether the array holds four items or four million. Reading or writing a[i] costs O(1).
Index | Address | Value |
|---|---|---|
0 | 1000 | 3 |
1 | 1004 | 8 |
2 | 1008 | 1 |
3 | 1012 | 5 |
The table is an array of four whole numbers, 4 bytes each, starting at address 1000. Item 3 is at 1000 + 3 × 4 = 1012. This is also why indexes start at 0: the index is how many items you skip from the start.
The price of this layout is that the block is one piece. There is no room between two neighbors, and the block has a fixed size, chosen when it is created. The next three sections are about what that costs.
Inserting in the middle moves every item after it
Say the array holds 3, 8, 1, 5 and has one free cell at the end. You want 9 at index 1, between 3 and 8. There is no gap there, so you have to make one: every item from index 1 onward moves one cell to the right, and then 9 goes into the cell that opened up.
The moves run from the end backward. If you went left to right, copying 8 into index 2 would overwrite the 1 before it had moved anywhere.
Inserting at index i into n items moves n − i of them:
- at the front (
i= 0), allnitems move; - in the middle, about half of them;
- at the end (
i=n), none do, as long as there is a free cell.
The rule
Reading or writing any index takes one step. Inserting or deleting anywhere but the end moves every item after that spot, so it costs O(n).
Deleting closes the gap the same way, in the other direction
Deleting the item at index i leaves a hole, and an array can't have holes: its items must stay packed for the address rule to work. So every item after i moves one cell to the left. This time the moves run from the hole forward, so each item is copied before its own cell is overwritten.
The last cell ends up holding a copy of the item before it, so the code clears it and the array's length drops by one. Like insertion, deleting at the front moves everything, and deleting at the end moves nothing.
When the order of the items doesn't matter (a bag of tasks, a set of players), there is a faster way: move the last item into the hole and shorten the array by one. That is one copy instead of n − i, so it costs O(1).
When the block is full, the whole array moves
The block can't simply get longer, because the memory right after it may already belong to something else. A dynamic array (Python's list, Java's ArrayList, C++'s std::vector) handles this by keeping some spare cells. Its capacity is how many cells the block has; its length is how many are in use.
Appending while there is spare capacity is one write: O(1). When the block is full, the next append has to do more work:
- Get a bigger block
Allocate a new block, a multiple of the old capacity (1.5 or 2 times is common).
- Copy every item
All
nitems are copied across, in order: O(n). - Let the old block go
The old block is freed for other uses.
- Write the new item
The append that triggered the move finally lands.
A copy of every item sounds expensive, and that one append is. But because the capacity grows by a multiple, the copies get rarer as the array grows, and spread over all the appends they add only a constant amount to each. Appending is O(1) on average, or amortized. Space & amortized analysis works through why.
A string is an array of characters that you usually can't change
A string is a sequence of characters stored like an array, so s[i] is a constant-time lookup and length of s is known without counting.
One caveat about what a "character" is. Java and JavaScript store strings as UTF-16, and their length counts 16-bit code units. Most characters take one unit, but an emoji or a rarer script takes two, so "😄".length is 2 and an index can land on half of one. Python indexes whole characters (code points).
In Java, Python and JavaScript, strings are immutable: once created, a string never changes. Code that seems to change one, like s ← s + c, builds a new string and copies both parts into it. In a loop, that copying adds up.
Building a string of n characters one at a time copies 1 + 2 + … + n = n(n + 1) ÷ 2 characters: about 50 million for a 10,000-character string. That is O(n²) for a job that should be O(n).
The fix is to collect the pieces in something you can change and turn it into a string once at the end:
Language | Collect the pieces in | Then |
|---|---|---|
Java | a |
|
Python | a list |
|
JavaScript | an array |
|
Some runtimes speed up repeated + in special cases, but the language doesn't promise it, so don't count on it.
Arrays are fast to read and slow to reshape
Operation | Cost | Why |
|---|---|---|
Read or write | O(1) | The address is computed |
Find a value (unsorted) | O(n) | Check items one by one |
Append at the end | O(1) on average | An occasional full copy |
Insert or delete at index | O(n − i) | Items after |
Insert or delete at the front | O(n) | Every item shifts |
The table undersells one thing: arrays are fast in practice even when you visit every item. The processor fetches memory in small blocks (cache lines), so reading one item brings its neighbors along, and walking an array front to back is about the friendliest thing you can ask of the hardware.
Arrays are the right default for most lists. When a job needs many inserts and deletes in the middle, Linked lists trade the shifting for slower lookups by index.
4 questions
Remember this
- An array's items sit side by side, so the address of any index is computed in one step: reading or writing
a[i]is O(1). - Inserting or deleting at index
ishifts every item after it, which is O(n) at the front and free at the end. - A dynamic array grows by copying itself into a bigger block; because the block grows by a multiple, appends cost O(1) on average.
- Strings in Java, Python and JavaScript are immutable, so building one with
+in a loop is O(n²); collect the pieces and join once.
Now solve some yourself
Only this topic 0 of 13 solved
- EasyRemove every copy of a value in place
- EasyFewest raises to make a list never decrease
- EasyShorten long words to first letter, count, last letter
- EasyAdd one to a number stored digit by digit
- EasyLongest run of one letter in a DNA string
- EasyBuild rows of Pascal's triangle
- EasyBest single buy and sell
- EasyLength of the last word
- EasyLongest start shared by every word
- MediumRotate a list by k places
- MediumRead a grid in a spiral
- MediumTurn a square grid a quarter turn in place
- MediumOne step of the Game of Life, in place
Mixed with topics you have practiced 0 unlocked
1 more once you have practiced Hash tables & maps. Play one of its examples to the end and solve one of its problems to unlock them.
Problems open on their own sites (LeetCode, Codeforces, CodeChef, AtCoder, CSES) in a new tab. Tick Solved when you get it accepted there; your ticks decide which mixed problems open next.
Next