Array Insertion Explained Through Varastra's E-Commerce Product Catalog — Learn How Inserting an Element at a Specific Index Works, Why Existing Elements Must Shift, and When an Array Insertion Costs O(n).
The interview insight: Your code might insert one product, but the computer may need to move thousands of existing products to make room for it.
Why Inserting Into an Array Is Slow (The Hidden Cost of Shifting)
Reading from an array takes one step. Adding an item in the middle can take thousands. The reason is a hidden move called shifting, and once you see it, you cannot unsee it.
The problem at Varastra
Varastra is an online store for Indian ethnic wear. Its home page has a "New arrivals" list. When a new saree is added, it goes to the top of the list.
Dev, a developer, writes the code in one line. It works on his laptop with 20 items. Then the sale starts, and a bulk upload adds thousands of products in one go. The page that shows the list is slow for minutes. (This story is made up.)
Tara, a new intern, looks at the code. Nothing is wrong. Each line is correct. So why is it slow?
She takes a pen and draws boxes on paper.
What an array really is
An array is a row of boxes. Each box holds one item. The boxes are numbered from 0, and that number is the index. In Python, a list works like an array.
Reading any box by index is fast. You jump straight to it. That costs O(1), which means one step, no matter how long the list is.
But the boxes sit side by side. There is no gap between them. So to put a new item in the middle, you must first move the others to make room. This moving is called shifting.
Insert, step by step
The diagram shows [10, 20, 30, 40] with 99 inserted at index 1. These are the steps:
- Add an empty box at the end.
- Shift 40 one place to the right.
- Shift 30 one place to the right.
- Shift 20 one place to the right.
- Write 99 at index 1.
Here is the same idea as code.
def insert_at(a, index, value):
a.append(None) # 1. add an empty box at the end
for i in range(len(a) - 1, index, -1):
a[i] = a[i - 1] # 2-4. shift items right, from the back
a[index] = value # 5. write the new item
a = [10, 20, 30, 40]
insert_at(a, 1, 99)
print(a) # [10, 99, 20, 30, 40]
Why start from the back? If you started from the front, you would overwrite items before you copied them. Try it on paper. Copy 10 into the box of 20, and the 20 is gone. Starting at the back keeps every item safe.
Three items moved in this example. The position matters:
- Insert at the back: 0 items move.
- Insert in the middle: about half of the items move.
- Insert at the front: every item moves.
So inserting at the front costs O(n), where n is the number of items. Deleting works the same way in reverse. Remove an item at the front, and every other item shifts one place left to close the gap.
Measure it
Tara does not guess. She times it. She inserts n items one by one at the front, with her own code and with Python's built-in insert.
import time
def time_inserts(n, use_builtin):
a = []
start = time.perf_counter()
for i in range(n):
if use_builtin:
a.insert(0, i)
else:
insert_at(a, 0, i)
return time.perf_counter() - start
for n in (2000, 4000, 8000):
print(n, round(time_inserts(n, False), 4), round(time_inserts(n, True), 4))
Pause and predict
Before you look at the table: 2,000 inserts at the front took 0.064 seconds. How long will 4,000 inserts take?
Answer: About four times longer, so around 0.25 seconds. Twice the items means twice the inserts, and each insert shifts twice as many items.
Here are her results.
| n | Her insert_at |
Python list.insert |
|---|---|---|
| 2,000 | 0.064 s | 0.0004 s |
| 4,000 | 0.253 s | 0.0013 s |
| 8,000 | 1.12 s | 0.0057 s |
Python's built-in insert is much faster, because it does the shifting in quick built-in code. But look at how both columns grow. When n doubles, the time goes up about four times.
That is the O(n²) shape. Each insert costs O(n), and you do n inserts. The shifting is still there. Python only hides it inside a faster machine.
These numbers were measured on one machine. They will differ on yours. The shape will not.
Try it
In time_inserts, change the insert position from 0 to len(a). That inserts at the back. How many items shift now?
None. In our run, 8,000 inserts at the back took about 0.001 seconds. The same 8,000 at the front took over a second.
What Tara does about it
She has three options.
- Add at the back, and read the list in reverse. Appending costs O(1). If the page can read the list from the end, no shifting is needed.
- Collect first, then build once. Gather all the new items in a small list. Then join it with the old list in one step. That is one pass, not thousands of shifts.
- Use a different container. Python has
collections.deque, which is built for adding at both ends. Choose it when you add to the front all the time.
from collections import deque
new_arrivals = deque()
for product in ["saree A", "saree B", "saree C"]:
new_arrivals.appendleft(product) # O(1), no shifting
print(list(new_arrivals)) # ['saree C', 'saree B', 'saree A']
The right choice depends on what else the code does. If you read by index a lot, a list is better. A deque is slow at reading the middle. Every container buys one speed with another.
How to spot a hidden shift
Watch for these clues in any code you read or write.
insert(0, x)orpop(0). Both shift every item.del a[i]ora.remove(x)inside a loop. Each call can shift the rest of the list.- A slice like
a[1:]inside a loop. A slice builds a new copy, so it costs O(n) each time.
When you see one of these inside a loop, the total cost is O(n²). Draw the boxes first. Count who moves. Then decide if it matters for your input size.
In an interview, say it out loud:
"Inserting at the front shifts every item, so each insert costs O(n)."
Takeaway: Reading an array is one step. Inserting or deleting in the middle shifts items, so count the shifts before you write the loop.
