Back

Array

4 October 2025 (2w ago)

An array is a contiguous block in RAM. For example [1, 3, 5] (integers), each takes 4 bytes (32 bits), so addresses might be $0, $4, $8. For chars like ['A', 'B', 'C'], its 1 byte each: $0, $1, $2. The key is everything is next to each other, and we access via indexes starting at 0.

Note that integers are often 32-bit: 1 is 31 zeros and a 1 at the end in binary. Arrays are simple, their memory layout matches how I think of them in code.

Types of Arrays

There are basically two types of arrays:

Note: Python and JS/TS default to dynamic arrays, unlike C/C++/Java.

Static Arrays

Reading

Reading is fast, O(1) because RAM is random access (can jump to any address instantly). Index maps directly to the memory address.

Looping through is O(n), obviously, since you hit each element.

my_array = [0] * 3
my_array[0] = 1
my_array[1] = 3
my_array[2] = 5
 
# Read index 0
print(my_array[0])  # 1
 
# Loop
for value in my_array:
    print(value)  # 1 3 5

Writing

If you want to overwrite an existing spot, just place the new value at the index, O(1).

Adding to the end (if there's space)? Also O(1): Track a count var, write there, increment.

Removing? Overwrite with None/null and decrement count, still O(1).

Insert/Remove in the Middle

This is where it hurts: O(n) worst case (on average) because you have to shift elements to keep contiguity.

Example: Inserting at the start (worst shift).

my_array = [5, 6, 0]
for i in range(len(my_array) - 1, 0, -1): # Shift right
    my_array[i] = my_array[i - 1];
my_array[0] = 4
print(my_array) # [4, 5, 6]

Trade offs

Good for fast access if size known. Bad for frequent inserts/removes in middle or growing in size. In Python/TS, arrays are dynamic by default, so I rarely hit this, but good to know for lower-level stuff.

Implementation

Full code with tests in the git repo.

class StaticArray:
    def __init__(self, capacity):
        self._array = [0] * capacity
        self._length = 0
        self._capacity = capacity
    
    def insert_end(self, n):
        if self._length < self._capacity:
            self._array[self._length] = n
            self._length += 1
            return True
        return False # Full, no insertion
 
    def remove_end(self):
        if self._length > 0:
            self._array[self._length - 1] = 0
            self._length -= 1
            return True
        return False # Empty, no removal
 
    def insert_middle(self, i, n):
        if i < 0 or i > self._length or self._length >= self._capacity:
            return False # Invalid index or full
        # Shift starting from the end to i
        for index in range(self._length - 1, i - 1, -1):
            self._array[index + 1] = self._array[index]
        # Insert at i
        self._array[i] = n
        self._length += 1
        return True
 
    def remove_middle(self, i):
        if i < 0 or i >= self._length:
            return False # Invalid index
        # Shift starting from i + 1 to the end
        for index in range(i + 1, self._length):
            self._array[index - 1] = self._array[index]
        # No need to "remove" self._array[i], since we shifted
        self._array[self._length - 1] = 0 # Optional: clear the last spot
        self._length -= 1
        return True
 
    def print_array(self):
        for i in range(self._capacity):
            print(self._array[i])
    
    def is_full(self):
        return self._length == self._capacity
 
    def is_empty(self):
        return self._length == 0
 
    def get_length(self):
        return self._length
 
    def get_capacity(self):
        return self._capacity

Dynamic Arrays

Dynamic arrays fix the main issue with static ones: the fixed size. They can grow (or shrink) as needed, making them way more flexible. In Python and JS this is the default array type. In Java, if you want a dynamic array you use ArrayList, and vector in C++.

When you create a dynamic array, it starts with some initial capacity (often some default like 10, but varies). The "length" tracks how many elements are actually in use, starting at 0. You don't have to specify a size upfront.

How it works

If you run out of space when appending, it allocates a new array double the current capacity, copies all elements over (O(n) for that step), then adds the new one. The old array gets deallocated.

Why double? It minimises how often you resize. Resizing is O(n), but it happens infrequently, this gives it amortised O(1) time for appends overall, meaning on average its constant time. (The cost is dominated by the last resize, totalling ~2n operations for n elements, which simplifies down to O(n) total, or O(1) per append).

Reading/writing by index is still O(1), like static. Looping is O(n). Insert/remove in the middle is O(n) worst case, no amortisation here lol.

Trade offs

Great for variable sizes and frequent appends/pops. The resizing overhead is rare, so it's efficient. Downside: occasional O(n) spikes on resize and still bad for middle inserts/removes. Use when you need flexibility without knowing size in advance.

Examples

my_list = []  # Starts empty, dynamic
my_list.append(1)  # Push
my_list.append(3)
my_list.append(5)
print(my_list[0])  # 1, O(1) read
print(my_list.pop())  # 5, O(1) pop
for value in my_list:
    print(value)  # 1 3

Inserting in the middle (e.g. at index 1)

my_list = [1, 3, 5]
my_list.insert(1, 2)  # Shifts right from index 1
print(my_list)  # [1, 2, 3, 5]

Implementation

Full code with tests in the git repo.

class DynamicArray:
    def __init__(self):
        self._array = [0] * 1 # Start small, capacity of 2.
        self._length = 0
        self._capacity = 1
    
    def resize(self):
        # Create new array of double capacity
        self._capacity *= 2
        new_array = [0] * self._capacity
 
        #Copy elements to new_array
        for i in range(self._length):
            new_array[i] = self._array[i]
        self._array = new_array
    
    # Insert n in the last position of the array
    def pushback(self, n):
        if self._length == self._capacity:
            self.resize()
        # Insert at next empty position
        self._array[self._length] = n
        self._length += 1
 
    # Remove the last element in the array
    def popback(self):
        if self._length > 0:
            self._length -= 1
 
    # Get value at i-th index
    def get(self, i):
        if i < self._length:
            return self._array[i]
        raise IndexError("Index out of bounds")
 
    # Insert at the i-th index
    def insert(self, i, n):
        if i < 0 or i > self._length:
            raise IndexError("Index out of bounds")
        if self._length == self._capacity:
            self.resize()
        # Shift right from i to end
        for j in range(self._length, i, -1):
            self._array[j] = self._array[j - 1]
        self._array[i] = n
        self._length += 1
        
    def print(self):
        for i in range(self._length):
            print(self._array[i])
        print()