Back to List

Contiguous Memory and Array Indexing O(1)

Understand why arrays can be accessed instantly by index, the principle of contiguous memory, and cache friendliness.

Intermediate
|
10min
|
Verified (2026-07)
contiguous memoryarray indexingO(1) accessmemory addresscache
Progress0/23 (0%)

Contiguous Memory and Array Indexing: O(1)

After This Topic

You will be able to mathematically explain why arrays have O(1) index access, understand the advantages and disadvantages of contiguous memory, and grasp the performance differences between arrays and linked lists.


Why are Arrays Fast?

python
numbers = [10, 20, 30, 40, 50]
print(numbers[3]) # 40 β€” Instant access!

Whether it's 5 elements or 5 million, the time to access an element by index is always the same. This is O(1) β€” constant time access.

Why is this possible? Because arrays are stored contiguously in memory.


Contiguous Memory: The Secret of Arrays

text
Memory Address:  1000  1004  1008  1012  1016
Value:          [10]  [20]  [30]  [40]  [50]
Index:          0     1     2     3     4

Each element of the array is stored right next to each other in memory. If each integer occupies 4 bytes:

  • numbers[0] β†’ Address 1000
  • numbers[1] β†’ Address 1004
  • numbers[2] β†’ Address 1008
  • numbers[3] β†’ Address 1012

Indexing Formula

text
Element Address = Starting Address + (Index Γ— Element Size)

numbers[3] = 1000 + (3 Γ— 4) = 1012

This calculation involves one addition and one multiplication. It doesn't matter if the array has 5 elements or 5 billion elements. Therefore, it's O(1).


Comparison with Linked Lists

In a linked list, each node stores the address of the next node. The nodes are scattered in memory.

text
Memory:  [10|β†’1500]  ...  [20|β†’2300]  ...  [30|β†’1800]  ...  [40|β†’2100]

To find the 3rd element:
  Node 0 (1000) β†’ Follow the pointer
  Node 1 (1500) β†’ Follow the pointer
  Node 2 (2300) β†’ Follow the pointer
  Node 3 (1800) ← Here!

To find the 3rd element, you have to traverse from the 0th element. To find the nth element, you have to traverse n times β†’ O(n).

OperationArrayLinked List
Index AccessO(1)O(n)
Insertion at the beginningO(n) β€” Shift all elementsO(1)
Insertion in the middleO(n) β€” Shift the rest of the elementsO(1) β€” Only change pointers
Appending to the endO(1) (if space is available)O(1) (if you have a tail pointer)
Search (Unsorted)O(n)O(n)

The Reality of Python Lists

Python's list is different from a C array. It's an array of pointers.

text
Python list:  [ptr0][ptr1][ptr2][ptr3]
                ↓     ↓     ↓     ↓
              [10]  ["hi"] [3.14] [[1,2]]

Pointers (addresses) have a fixed size (8 bytes on a 64-bit system), so the pointer array itself is stored in contiguous memory. The indexing formula still applies, making it O(1).

python
# Python list β€” Can store different types (because it's an array of pointers)
mixed = [42, "hello", 3.14, [1, 2, 3]]
print(mixed[2]) # 3.14 β€” O(1)

However, the actual data is scattered throughout memory, so it's less cache-efficient than a C array.

NumPy Arrays: True Contiguous Memory

python
import numpy as np
# NumPy stores data contiguously, like a C array
arr = np.array([1, 2, 3, 4, 5], dtype=np.int32)
# Memory: [00000001 00000002 00000003 00000004 00000005]
# 4 bytes each, stored contiguously

One of the reasons NumPy is tens of times faster than Python lists is this contiguous memory layout.


Cache Friendliness

When the CPU retrieves data from memory, it also loads the data around the requested address into the cache line (usually 64 bytes).

text
Sequential array access:
  Access arr[0] β†’ Loads arr[0]~arr[15] into the cache
  Access arr[1] β†’ Cache hit! (Already loaded)
  Access arr[2] β†’ Cache hit!
  ...

Linked list access:
  Access node0 β†’ Loads surrounding data into the cache
  Access node1 β†’ Different address! Cache miss β†’ Re-loads from memory
  Access node2 β†’ Another different address! Cache miss
  ...

Arrays have a high cache hit rate because they are contiguous, while linked lists have frequent cache misses because they are scattered. In modern CPUs, a cache miss is more than 100 times slower than a cache hit.


Limitations of Arrays: Insertion and Deletion

text
Insertion in the middle of an array (insert 25 at index 2):

Before: [10][20][30][40][50]
                 ↓
Step 1: [10][20][  ][30][40][50]  ← Shift 30, 40, 50 one position to the right
Step 2: [10][20][25][30][40][50]  ← Insert 25 into the empty space

All the elements after the insertion point must be moved, resulting in O(n). Deletion is the same β€” you have to move the elements forward to fill the empty space.


Memory Layout of 2D Arrays

text
Row-major (C, Python, NumPy default):
[[1, 2, 3],     Memory: [1][2][3][4][5][6]
 [4, 5, 6]]     β†’ Stored in row order

Column-major (Fortran, MATLAB):
[[1, 2, 3],     Memory: [1][4][2][5][3][6]
 [4, 5, 6]]     β†’ Stored in column order
python
import numpy as np
arr = np.array([[1, 2, 3], [4, 5, 6]])
# C order (row-major, default)
print(arr.flags['C_CONTIGUOUS']) # True
# Fortran order
arr_f = np.asfortranarray(arr)
print(arr_f.flags['F_CONTIGUOUS']) # True

If you have many row-wise traversals, Row-major is more cache-efficient. If you have many column-wise traversals, Column-major is more cache-efficient. NumPy uses C order (Row-major) by default. This is why row-wise operations (axis=1) are often faster than column-wise operations (axis=0) β€” because the memory access pattern is more cache-friendly.


Dynamic Arrays: Arrays that Grow

Arrays have a fixed size. Python lists are dynamic arrays, which allocate and copy a larger array when they are full.

python
import sys
items = []
prev_size = 0
for i in range(20):
items.append(i)
size = sys.getsizeof(items)
if size != prev_size:
print(f"len={len(items):2d}, capacity changed: {prev_size} β†’ {size} bytes")
prev_size = size
# len= 1, capacity changed: 56 β†’ 88 bytes
# len= 5, capacity changed: 88 β†’ 120 bytes
# len= 9, capacity changed: 120 β†’ 184 bytes
# len=17, capacity changed: 184 β†’ 248 bytes

Python typically allocates space that is 1.125 times the current size plus a constant. If it allocated one element at a time, it would have to copy everything every time (O(n)). However, by allocating in multiples, it achieves amortized O(1).


Key Takeaways

ConceptSummary
Contiguous MemoryElements are stored right next to each other
O(1) IndexingAddress = Starting + (Index Γ— Size). Independent of array size
Cache FriendlinessContiguous memory β†’ High CPU cache hit rate β†’ Fast
Python listArray of pointers. Indexing is O(1), but less cache-efficient
NumPyStores data contiguously. Similar performance to C arrays
Dynamic ArrayAllocates and copies a larger array when full. Amortized O(1) append

The reason arrays are O(1) is the simple mathematics of "contiguous memory + one multiplication." Understanding this principle naturally connects to why NumPy is faster than Python lists, why databases use indexes, and why cache optimization is important.

In practice: If you need a lot of index access, use an array. If you need a lot of insertions/deletions, use a linked list. Python lists are suitable for most cases, but if you're doing numerical calculations, using NumPy is tens of times faster β€” because it uses true contiguous memory.

πŸ’¬ Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...