The Story Behind Arrays
When you want to find your friend in seat 7, you don't ask the usher to search the whole auditorium — you walk straight to position 7. You know exactly where it is because seats are evenly spaced from the entrance. That instant lookup is the superpower of an array.
But here's the catch: if someone arrives late and wants to sit between seats 3 and 4, every person from seat 4 onward must shuffle one seat to the right to make room. And if someone leaves mid-film, the remaining audience shuffles left to close the gap. That shuffling cost is the price of keeping seats contiguous.
An array is a fixed-size, contiguous block of memory that stores elements of the same type. Each element sits at a predictable offset from the start, so reading any element by its index costs exactly the same time regardless of array size — a property called random access in O(1).
The index is not a count of seats — it is an offset from the first seat.
The first element is 0 seats away from the start, so its index is 0.
This makes address arithmetic trivial: address = base + index × element_size.
Every major language except a handful of legacy ones follows this convention.
Interactive Diagram — See Arrays in Motion
Use the controls below to step through four core operations: indexing, insertion, deletion, and linear search. Each frame shows what happens to the cells in memory.
Arrays give you blazing-fast reads (O(1)) in exchange for slow writes in the middle (O(n)). If your workload is read-heavy and you know the index, arrays are the best possible choice. If you insert and delete frequently in the middle, consider a linked list or a dynamic array with amortised O(1) appends.
Time Complexity at a Glance
| Operation | Time (Best) | Time (Worst) | In-place? | Notes |
|---|---|---|---|---|
| Read by index | O(1) | O(1) | Yes | Direct address arithmetic. Always constant time. |
| Write by index | O(1) | O(1) | Yes | Same as read — one memory write at a computed offset. |
| Append (end) | O(1) | O(n)* | Varies | Amortised O(1) for dynamic arrays (Python list, Java ArrayList). O(n) only on resize. |
| Insert (middle/start) | O(n) | O(n) | Yes | Every element after the insertion point shifts right. Insert at index 0 = worst case. |
| Delete (middle/start) | O(n) | O(n) | Yes | Every element after the deletion point shifts left. Delete at index 0 = worst case. |
| Delete (end) | O(1) | O(1) | Yes | Just decrement the length pointer. No shifting needed. |
| Search (unsorted) | O(1) | O(n) | Yes | Best case: found at index 0. Worst: last element or not present. |
| Search (sorted) | O(1) | O(log n) | Yes | Binary search — halves search space each step. Requires sorted array. |
| Space usage | O(n) | O(n) | — | No per-element overhead. Tightest possible memory layout. |
Python's list and Java's ArrayList are dynamic arrays: when full, they
allocate a new block roughly 2× the current size and copy everything over.
That copy is O(n) — but it happens so rarely (exponentially spaced out) that the
amortised cost per append is O(1). This is not magic; it is mathematics.
Still, if you're appending 10 million items and want zero pauses, pre-allocate with
list = [None] * N upfront.
Python Arrays — Code & Output
1 · Creating and Indexing
# Python's built-in list IS a dynamic array under the hood
seats = [12, 7, 45, 3, 28, 16, 9]
# O(1) index reads
print("First element :", seats[0]) # index 0
print("Middle element:", seats[3]) # index 3
print("Last element :", seats[-1]) # negative index wraps from end
# Slice — creates a new list, O(k) where k = slice length
print("Indices 1–3 :", seats[1:4]) # [7, 45, 3]
# Length — O(1), stored as metadata
print("Length :", len(seats))
seats[-1] is equivalent to seats[len(seats) - 1]. Python computes
the real index before performing the memory lookup. The cost is still O(1) — just one extra
subtraction. There is no backwards scan.
2 · Insertion and Deletion
cinema = [10, 20, 30, 40, 50]
# INSERT at index 2 — O(n): elements 30, 40, 50 shift right
cinema.insert(2, 99)
print("After insert(2, 99):", cinema)
# DELETE by index — O(n): elements after index 2 shift left
del cinema[2]
print("After del [2] :", cinema)
# APPEND to end — amortised O(1)
cinema.append(60)
print("After append(60) :", cinema)
# POP from end — O(1): no shifting
last = cinema.pop()
print(f"Popped: {last}, list now:", cinema)
3 · Linear Search vs Binary Search
# ── Linear Search — works on any list, O(n) ──────────────
def linear_search(arr, target):
for i, val in enumerate(arr):
if val == target:
return i
return -1
# ── Binary Search — sorted array only, O(log n) ──────────
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target: return mid
elif arr[mid] < target: lo = mid + 1
else: hi = mid - 1
return -1
unsorted = [12, 7, 45, 3, 28, 16, 9]
sorted_a = sorted(unsorted) # [3, 7, 9, 12, 16, 28, 45]
print("Linear search for 28:", linear_search(unsorted, 28))
print("Binary search for 28:", binary_search(sorted_a, 28))
print("Linear search for 99:", linear_search(unsorted, 99))
print("Binary search for 99:", binary_search(sorted_a, 99))
For an array of 1 billion elements, binary search needs at most 30 comparisons (log₂(1,000,000,000) ≈ 30). Linear search needs up to 1 billion. The only requirement: the array must be sorted first. If you search the same array thousands of times, the one-time O(n log n) sort cost is repaid almost immediately.
Static vs Dynamic Arrays
ndarray.
list, Java ArrayList, C++ vector, JS Array.
When Arrays Win & When They Don't
| Frequent insertions/deletions in the middle |
| Size is completely unknown and changes wildly |
| You need fast membership tests on large sets |
| Key-value lookups (use a dict/hash map instead) |
| Ordered insertions keeping sorted order (use a heap/BST) |
| Random access by index is the primary operation |
| Data is read far more often than written |
| Append-only workloads (logs, buffers, queues) |
| Cache performance matters (contiguous in memory) |
| Numeric computation (NumPy, matrix operations) |
Modern CPUs load memory in cache lines of 64 bytes. When you read
arr[0], the CPU automatically fetches arr[0] through roughly
arr[15] (for 4-byte integers) into the L1 cache. Iterating sequentially over
an array is therefore far faster in practice than the O(n) notation suggests — cache hits
are nearly free. Linked lists, by contrast, scatter nodes across memory and suffer
frequent cache misses.
Pipeline: What Happens Inside arr[i]
0 ≤ i < len(arr). If not, it raises IndexError before touching memory. C/C++ skip this — hence buffer overflow vulnerabilities.address = base_ptr + i × element_size. For a Python list, element_size is the size of a pointer (8 bytes on 64-bit). One multiply and one add.6 Golden Rules for Arrays
arr.append(x) is amortised O(1).
arr.insert(0, x) is always O(n) — it shifts every single element. If you need a fast
front-insert, use collections.deque instead.
bisect.bisect_left(arr, x) in Python gives you O(log n) search in one line.
The precondition is that the array is sorted; maintaining that sort is your responsibility.
[x * 2 for x in arr] is both faster and more readable than a manual loop with appends.
sortedcontainers.SortedList. Know when the array has served its purpose.
Code Execution Animator — Watch Every Line Run
Select an operation, then press Play or click Step →. The highlighted line in the code panel shows exactly which instruction is executing, while the memory panel mirrors the result in real time.
The purple arrow ▶ marks the currently executing line. The Memory panel reflects the array state after that line runs. Blue cells = being read, green = written/created, amber = shifting, red = being scanned, purple = found/result. The Variable Watch table tracks every live variable in real time.