Array Operations: Time and Space Costs
A technical breakdown of array insertions and deletions, why costs vary by operation, and what happens under the hood.
Many of us start programming with high level languages. Arrays feel easy, so we move fast.
In the age of AI, a lot of code is written for us. That makes it easy to skip the basics. This post keeps it simple and technical. I use JavaScript to show what array operations do and why their costs change.
Core idea: indexes and shifting
Arrays store items in order. Each item has an index.
const arr = ["A", "B", "C", "D"];
// indexes: 0 1 2 3
Access by index is usually O(1):
arr[2]; // "C"
But inserts and deletes can be expensive. When you change the start or the middle, items after the edit need new indexes. The engine shifts elements left or right. That is the cost.
Most of these methods mutate the array. They also return different values. Some return the removed item. Some return the new length.
Operations at the end (fast)
Add at the end: push()
JavaScript way:
const arr = ["A", "B", "C"];
const newLength = arr.push("D");
// arr is now ["A", "B", "C", "D"]
// newLength is 4
Deconstruction:
function deconstructPush(arr, value) {
arr[arr.length] = value;
return arr.length;
}
Post mortem:
- Operation. Add one element at the end.
- Under the hood. Append a new slot.
- Time. Usually
O(1). Can spike when the array grows. - Extra space. Usually
O(1). - Returns. New length.
Remove at the end: pop()
JavaScript way:
const arr = ["A", "B", "C"];
const removed = arr.pop();
// arr is now ["A", "B"]
// removed is "C"
Deconstruction:
function deconstructPop(arr) {
if (arr.length === 0) return undefined;
const lastIndex = arr.length - 1;
const value = arr[lastIndex];
arr.length = lastIndex;
return value;
}
Post mortem:
- Operation. Remove one element from the end.
- Under the hood. Drop the last slot.
- Time. Usually
O(1). - Extra space.
O(1). - Returns. The removed element, or
undefined.
Operations at the start (slow)
When you edit index 0, every other item changes index. The array shifts a lot of values.
Add at the start: unshift()
JavaScript way:
const arr = ["B", "C", "D"];
const newLength = arr.unshift("A");
// arr is now ["A", "B", "C", "D"]
Deconstruction:
function deconstructUnshift(arr, value) {
arr.length = arr.length + 1;
for (let i = arr.length - 1; i > 0; i--) {
arr[i] = arr[i - 1];
}
arr[0] = value;
return arr.length;
}
Post mortem:
- Operation. Add one element at index
0. - Under the hood. Shift all existing elements right by one.
- Time.
O(n). - Extra space.
O(1). - Returns. New length.
Remove at the start: shift()
JavaScript way:
const arr = ["A", "B", "C"];
const removed = arr.shift();
// arr is now ["B", "C"]
// removed is "A"
Deconstruction:
function deconstructShift(arr) {
if (arr.length === 0) return undefined;
const value = arr[0];
for (let i = 0; i < arr.length - 1; i++) {
arr[i] = arr[i + 1];
}
arr.length = arr.length - 1;
return value;
}
Post mortem:
- Operation. Remove index
0. - Under the hood. Shift all remaining elements left by one.
- Time.
O(n). - Extra space.
O(1). - Returns. The removed element, or
undefined.
Operations in the middle (slow)
Adding and removing in the middle is slower than the end. Elements must shift to keep indexes correct.
Insert at a position: shift right
Why insert is O(n):
- Start from the end and move backward.
- Shift each element one position to the right.
- Stop at the target index.
- Place the new element.
- Update the array length.
JavaScript way:
const numbers = [10, 20, 30, 40];
numbers.splice(2, 0, 99); // insert at index 2
// [10, 20, 99, 30, 40]
Deconstruction:
function deconstructInsertAt(arr, index, value) {
arr.length = arr.length + 1;
for (let i = arr.length - 1; i > index; i--) {
arr[i] = arr[i - 1];
}
arr[index] = value;
return arr;
}
Post mortem:
- Operation. Insert at a specific index.
- Under the hood. Shift elements after the index to the right.
- Time.
O(n). - Extra space.
O(1). - Returns.
splice()returns an array of removed items. For inserts, that is[].
Delete at a position: shift left
Why delete is O(n):
- Remove the element at the index.
- Shift each element after it one position to the left.
- Update the array length.
JavaScript way:
const numbers = [10, 20, 30, 40, 50];
const removed = numbers.splice(2, 1); // remove index 2
// numbers is [10, 20, 40, 50]
// removed is [30]
Deconstruction:
function deconstructDeleteAt(arr, index) {
for (let i = index; i < arr.length - 1; i++) {
arr[i] = arr[i + 1];
}
arr.length = arr.length - 1;
return arr;
}
Post mortem:
- Operation. Remove a specific index.
- Under the hood. Shift elements after it to the left.
- Time.
O(n). - Extra space.
O(1). - Returns.
splice()returns an array of removed items.
Position matters
The cost depends on where you edit the array:
- Worst case. Insert or delete at the start. You shift all
nelements. - Average case. Insert or delete in the middle. You shift about half.
- Best case. Insert or delete at the end. You shift zero.
Array operations summary
| Operation | Method | Typical time | Typical extra space | Notes |
|---|---|---|---|---|
| Access by index | arr[i] | O(1) | O(1) | direct lookup |
| Update by index | arr[i] = x | O(1) | O(1) | direct update |
| Add at end | push() | O(1) | O(1) | returns new length |
| Remove at end | pop() | O(1) | O(1) | returns removed element |
| Add at start | unshift() | O(n) | O(1) | shifts right |
| Remove at start | shift() | O(n) | O(1) | shifts left |
| Insert at index | splice(i, 0, v) | O(n) | O(1) | returns [] |
| Remove at index | splice(i, 1) | O(n) | O(1) | returns removed items |
| Search unsorted | indexOf() | O(n) | O(1) | scans list |
| Full iteration | for, forEach | O(n) | O(1) | visits all elements |
Fast operations, usually O(1):
- Access by index.
arr[i] - Update by index.
arr[i] = x - Append.
push() - Remove from end.
pop()
Slow operations, usually O(n):
- Search in an unsorted array.
indexOf() - Insert at the middle.
splice() - Delete at the middle.
splice() - Insert at the start.
unshift() - Delete at the start.
shift() - Iteration. Any full pass over the array
When to avoid middle operations
Avoid middle inserts and deletes when:
- You do many updates in a loop.
- The array is large.
- Performance is sensitive.
Alternatives and tradeoffs
Swap and pop
If order does not matter, you can delete in O(1).
JavaScript way:
function swapAndPop(arr, index) {
arr[index] = arr[arr.length - 1];
arr.pop();
return arr;
}
Deconstruction:
function deconstructSwapAndPop(arr, index) {
const lastIndex = arr.length - 1;
arr[index] = arr[lastIndex];
arr.length = lastIndex;
return arr;
}
Post mortem:
- Operation. Remove one element at an index.
- Under the hood. Replace it with the last element, then drop the last slot.
- Time.
O(1). - Cost. Order changes.
Linked lists
If you do frequent inserts and deletes, a linked list can help. Inserts and deletes are O(1) once you have the node. Finding that node is still O(n).
We will discuss linked lists in a JavaScript way in a separate post so the full scenario is clear. That will cover why linked lists are needed and how they help. This article is about arrays, but it still builds the mental model that makes linked lists make sense.