Loading content...
We've seen arrays at their best:
But arrays have a dark side. When you need to insert an element in the middle or delete one from anywhere but the end, arrays reveal their fundamental weakness.
The cost: O(n) — linear time.
Inserting a single element into a 1-million-element array may require moving 1 million elements. This is the price we pay for contiguous memory and instant access.
By the end of this page, you will understand exactly why insertion and deletion in arrays cost O(n), visualize the element shifting that occurs, and learn when this cost is acceptable versus when you should consider alternative data structures.
To understand why insertion and deletion are expensive, we must revisit why arrays are fast in the first place.
The O(1) access formula:
address(arr[i]) = base + (i × element_size)
This formula works ONLY because elements are contiguous — stored back-to-back with no gaps. If we allowed gaps:
Arrays trade modification speed for access speed. Contiguous storage enables O(1) access but forces O(n) modifications. You cannot have both instant access AND instant insertion in the same structure — this is a fundamental computer science constraint, not a language limitation.
The hotel room analogy:
Imagine a hotel where rooms are numbered 0, 1, 2, 3, ... and you can instantly tell a guest: "You're in room 57. Walk to the door marked 57."
Now imagine someone wants to 'insert' themselves after room 25. You can't just create room 25.5 — the numbering system doesn't allow it. Instead, every guest from room 26 onwards must pack up and move to the next room (26→27, 27→28, etc.) to make space.
This is exactly what happens in arrays.
Let's trace exactly what happens when you insert an element in the middle of an array.
Example: Insert 15 at index 2 in [10, 20, 30, 40, 50]
Before: [10, 20, 30, 40, 50, _ ]
0 1 2 3 4 5 (need space for new element)
Step 1: Shift arr[4] to arr[5]
[10, 20, 30, 40, 50, 50]
←──┘
Step 2: Shift arr[3] to arr[4]
[10, 20, 30, 40, 40, 50]
←──┘
Step 3: Shift arr[2] to arr[3]
[10, 20, 30, 30, 40, 50]
←──┘
Step 4: Write 15 to arr[2]
[10, 20, 15, 30, 40, 50]
↑
Inserted!
After: [10, 20, 15, 30, 40, 50]
0 1 2 3 4 5
We had to shift 3 elements (indices 2, 3, 4) just to insert one value.
1234567891011121314151617181920212223242526
def insert_at(arr, index, value): """ Insert value at the given index, shifting elements right. Assumes arr has spare capacity (or we extend it). Time: O(n) — must shift all elements after index Space: O(1) — in-place (assuming capacity exists) """ # Extend array to make room arr.append(None) # Using list.append which is O(1) amortized # Shift elements from right to left (important: start from end!) for i in range(len(arr) - 1, index, -1): arr[i] = arr[i - 1] # Each shift is O(1), but we do (n - index) shifts # Insert the new value arr[index] = value # Examplenumbers = [10, 20, 30, 40, 50]insert_at(numbers, 2, 15)print(numbers) # [10, 20, 15, 30, 40, 50] # Python's built-in does this for you:numbers = [10, 20, 30, 40, 50]numbers.insert(2, 15) # Still O(n) underneath!print(numbers) # [10, 20, 15, 30, 40, 50]We shift from the end backward to avoid overwriting data. If we shifted left-to-right (arr[2] → arr[3] first), we'd overwrite arr[3] before saving it. Working backward preserves each element before we overwrite its source.
Deletion is the mirror of insertion: instead of making a gap and filling it, we close a gap by shifting elements left.
Example: Delete element at index 2 from [10, 20, 30, 40, 50]
Before: [10, 20, 30, 40, 50]
0 1 2 3 4
↑
Delete this
Step 1: Shift arr[3] to arr[2]
[10, 20, 40, 40, 50]
└──→
Step 2: Shift arr[4] to arr[3]
[10, 20, 40, 50, 50]
└──→
Step 3: Reduce logical size (optional: clear last position)
[10, 20, 40, 50]
0 1 2 3
After: [10, 20, 40, 50]
We shifted 2 elements to remove one value.
123456789101112131415161718192021222324
def delete_at(arr, index): """ Delete element at index, shifting elements left to fill gap. Time: O(n) — must shift all elements after index Space: O(1) — in-place """ # Shift elements left (start from left!) for i in range(index, len(arr) - 1): arr[i] = arr[i + 1] # Remove the last element (now duplicated) arr.pop() # O(1) # Examplenumbers = [10, 20, 30, 40, 50]delete_at(numbers, 2) # Delete element at index 2print(numbers) # [10, 20, 40, 50] # Python's built-in methods:numbers = [10, 20, 30, 40, 50]del numbers[2] # O(n) — shifts elements# ornumbers.pop(2) # O(n) for middle elements, O(1) for lastprint(numbers)Deletion shifts left-to-right (opposite of insertion). We copy arr[i+1] into arr[i], starting from the deletion point. This ensures we always read a value before we overwrite its destination.
Not all insertions and deletions are equally expensive. The position determines the cost.
| Position | Elements to Shift | Time Complexity | Case Type |
|---|---|---|---|
| Beginning (index 0) | n | O(n) | Worst case |
| Middle (index n/2) | n/2 | O(n) | Average case |
| End (index n) | 0 | O(1) | Best case |
Key insight: Inserting/deleting at the end is O(1) because no shifting is required. The existing elements stay put; we simply add or remove the last one.
This is why:
list.append(x) in Python is O(1) amortizedarr.push(x) in JavaScript is O(1) amortizedArrayList.add(x) in Java is O(1) amortizedBut:
list.insert(0, x) is O(n)arr.unshift(x) is O(n)ArrayList.add(0, x) is O(n)The first/beginning position is the most expensive to modify.
Many developers don't realize that operations like unshift() or insert(0, x) are O(n), not O(1). In tight loops, this creates accidental O(n²) complexity. Always check the documentation for your language's array modification methods!
Let's make the O(n) cost concrete. Assume each element move takes 1 nanosecond (10⁻⁹ seconds).
| Array Size | Elements Shifted | Time | Practical Impact |
|---|---|---|---|
| 100 | 100 | 100 ns | Imperceptible |
| 10,000 | 10,000 | 10 μs | Still fast |
| 1,000,000 | 1,000,000 | 1 ms | Noticeable in loops |
| 100,000,000 | 100,000,000 | 100 ms | User-visible lag |
| 1,000,000,000 | 1,000,000,000 | 1 second | Major performance issue |
The cumulative effect in loops:
Insert 1,000 elements at the beginning of an initially empty array:
Insert #1: Shift 0 elements
Insert #2: Shift 1 element
Insert #3: Shift 2 elements
...
Insert #1000: Shift 999 elements
Total shifts: 0 + 1 + 2 + ... + 999 = 499,500 shifts
This is O(n²) for n insertions at the front! What looks like O(n) work becomes O(n²) when repeated.
Real-world example:
A developer wrote code that built a list by inserting at the front (to maintain reverse order). With 50,000 records, the operation took 15 minutes. Switching to append-then-reverse reduced it to 0.1 seconds — a 9,000× speedup.
Inserting n elements at index 0 is O(n²), not O(n). Each insertion is O(n), done n times = O(n × n). This is one of the most common performance bugs in production code. Always append and reverse if you need items in reverse order.
When O(n) insertions/deletions become problematic, several strategies can help:
12345678910111213141516
def delete_unordered(arr, index): """ Delete element at index in O(1) by swapping with last. NOTE: This does NOT preserve element order! Time: O(1), Space: O(1) """ arr[index] = arr[-1] # Overwrite with last element arr.pop() # Remove last element (O(1)) # Examplenumbers = [10, 20, 30, 40, 50]delete_unordered(numbers, 1) # Delete 20print(numbers) # [10, 50, 30, 40] — 50 moved to index 1 # Order changed, but deletion was O(1)!# Use when order doesn't matter (e.g., unordered sets)Arrays aren't always the right choice. Here's how they compare with alternatives for common operations:
| Operation | Array | Linked List | Dynamic Array | Balanced BST |
|---|---|---|---|---|
| Access by index | O(1) ✓ | O(n) | O(1) ✓ | O(log n) |
| Search (unsorted) | O(n) | O(n) | O(n) | O(log n) ✓ |
| Insert at beginning | O(n) | O(1) ✓ | O(n) | O(log n) |
| Insert at end | O(1) ✓ | O(1) ✓ | O(1)* ✓ | O(log n) |
| Insert in middle | O(n) | O(1)** | O(n) | O(log n) |
| Delete from middle | O(n) | O(1)** | O(n) | O(log n) |
Notes:
When to choose arrays:
When to consider alternatives:
Every data structure excels at some operations and suffers at others. There is no 'best' structure—only the right one for your specific access pattern. Understanding these trade-offs is the heart of data structure selection.
Let's see how insertion/deletion costs manifest in real systems:
Case Study 1: Text Editors
A naive text editor storing characters in an array would face O(n) for every keystroke inserted in the middle of a document. For a 1-million-character document, that's catastrophically slow. Real text editors use:
These reduce typical insertions to O(log n) or O(1).
Case Study 2: Database Indexes
Database B-trees maintain sorted order while allowing O(log n) insertions. A naive sorted array index would require O(n) per insert—impossible for high-write workloads. The tree structure trades some memory and complexity for dramatically better insertion performance.
Case Study 3: Real-Time Systems
In real-time systems (games, audio processing), worst-case latency matters. An unexpected O(n) operation in the audio pipeline causes audible glitches. Such systems often use ring buffers or pre-allocated pools to avoid any operation worse than O(1).
In production systems, data structure choice isn't just about average case—it's about worst case. An O(1) average with O(n) worst case can cause latency spikes that violate SLAs. Understanding these costs helps you make robust architecture decisions.
We've thoroughly explored why arrays struggle with modifications. Let's consolidate:
What's next:
We've covered access, search, and interior modifications. There's one more critical operation: appending to the end. While we noted it's O(1), there's more to the story—when arrays run out of space, they need to resize.
Next, we explore append with O(1) amortized time and understand the elegant doubling strategy that makes dynamic arrays practical.
You now understand the fundamental trade-off at the heart of arrays: contiguous storage gives O(1) access but costs O(n) for modifications. This knowledge is essential for choosing the right data structure and avoiding accidental quadratic complexity.