Dynamic Array Code Implementation
Prerequisites
Before reading this article, you should first learn:
Key Points
Below, I will show a simple implementation of a dynamic array. It includes basic functions like add, delete, search, and update. Here are some key points that you should pay attention to while reading the code.
Key Point 1: Auto Expanding and Shrinking Capacity
In the previous article Basic Knowledge of Arrays, we talked about expanding an array when adding a new element, but did not mention shrinking.
In real use cases, shrinking the capacity of a dynamic array is also important. For example, if a dynamic array has space for 1000 elements but only stores 10, then there are 990 unused spots. To avoid wasting resources, we can shrink the storage space as needed. This is called shrinking.
Here we use a simple expanding-shrinking strategy:
- When the number of elements reaches the capacity of the underlying array, double the capacity;
- When the number of elements drops to 1/4 of the capacity, shrink the capacity by half.
Key Point 2: Index Out-of-Bounds Check
In the code below, there are two methods to check index boundaries: checkElementIndex and checkPositionIndex. The only difference between them is index < size and index <= size.
Why does checkPositionIndex allow index == size? This is because checkPositionIndex is used when inserting an element into the array.
For example, given the array nums, valid indices for elements are index < size:
nums = [5, 6, 7, 8]
index   0  1  2  3But when inserting a new element, possible positions are not just the indices of existing elements, but also the “gaps” between them:
nums = [ | 5 | 6 | 7 | 8 | ]
index    0   1   2   3   4All these gaps are valid insert positions. So index == size is also valid. This is the difference between checkPositionIndex and checkElementIndex.
Key Point 3: Prevent Memory Leaks When Deleting Elements
Algorithmically, you might not care about what happens to deleted elements. But when writing real code, we need to think about possible memory leaks.
In the code given below, when deleting an element, I set the deleted spot to null. For example, in Java:
// delete
public E removeLast() {
    E deletedVal = data[size - 1];
    // delete the last element
    // must set the last element to null, otherwise it will cause memory leak
    data[size - 1] = null;
    size--;
    return deletedVal;
}Java's garbage collection mechanism is based on reachability analysis using graph algorithms. If an object can no longer be accessed, the memory occupied by that object will be released. Otherwise, the garbage collector considers the object still in use and will not release its memory.
If you do not execute the line data[size - 1] = null, the reference data[size - 1] will persist. You can still access the object through data[size - 1], so the object is considered reachable, and its memory will not be released, leading to a memory leak.
Other languages with garbage collection mechanisms likely follow a similar approach. It is essential to understand the garbage collection mechanism of the programming language you are using, as this is a fundamental requirement for writing bug-free code.
Other Optimization Details
The following code is not a fully optimized implementation and has several areas for improvement. For example, I used a for loop to copy array data, which is relatively inefficient. Most programming languages provide more efficient methods for array copying, such as Java's System.arraycopy.
However, no matter how much you optimize, the core operation still involves moving data, resulting in a time complexity of . The focus of this article is to help you understand the basic implementation and time complexity of array operations like insertion, deletion, search, and modification. If you are interested in these details, you can delve into the source code of your programming language's standard library.
How to Verify Your Implementation?
You can use LeetCode problem 707, "Design Linked List," to verify the correctness of your implementation. Although this problem is about linked lists, it does not enforce the use of a linked list for the underlying implementation. The main purpose is to use its test cases to verify the correctness of your insertion, deletion, search, and modification functions.
Dynamic Array Code Implementation
import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class MyArrayList<E> {
    // The underlying array that actually stores data
    private E[] data;
    // Tracks the current number of elements
    private int size;
    // Default initial capacity
    private static final int INIT_CAP = 1;
    public MyArrayList() {
        this(INIT_CAP);
    }
    public MyArrayList(int initCapacity) {
        data = (E[]) new Object[initCapacity];
        size = 0;
    }
    // Add
    public void addLast(E e) {
        int cap = data.length;
        // Check if the array capacity is sufficient
        if (size == cap) {
            resize(2 * cap);
        }
        // Insert the element at the end
        data[size] = e;
        size++;
    }
    public void add(int index, E e) {
        // Check for index out of bounds
        checkPositionIndex(index);
        int cap = data.length;
        // Check if the array capacity is sufficient
        if (size == cap) {
            resize(2 * cap);
        }
        // Shift data from data[index..] to data[index+1..]
        // Make space for the new element
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        // Insert the new element
        data[index] = e;
        size++;
    }
    public void addFirst(E e) {
        add(0, e);
    }
    // Remove
    public E removeLast() {
        if (size == 0) {
            throw new NoSuchElementException();
        }
        int cap = data.length;
        // Can shrink to save space
        if (size == cap / 4) {
            resize(cap / 2);
        }
        E deletedVal = data[size - 1];
        // Remove the last element
        // Must set the last element to null to avoid memory leaks
        data[size - 1] = null;
        size--;
        return deletedVal;
    }
    public E remove(int index) {
        // Check for index out of bounds
        checkElementIndex(index);
        int cap = data.length;
        // Can shrink to save space
        if (size == cap / 4) {
            resize(cap / 2);
        }
        E deletedVal = data[index];
        // Shift data from data[index+1..] to data[index..]
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        data[size - 1] = null;
        size--;
        return deletedVal;
    }
    public E removeFirst() {
        return remove(0);
    }
    // Get
    public E get(int index) {
        // Check for index out of bounds
        checkElementIndex(index);
        return data[index];
    }
    // Set
    public E set(int index, E element) {
        // Check for index out of bounds
        checkElementIndex(index);
        // Modify the data
        E oldVal = data[index];
        data[index] = element;
        return oldVal;
    }
    // Utility methods
    public int size() {
        return size;
    }
    public boolean isEmpty() {
        return size == 0;
    }
    // Resize the array to newCap
    private void resize(int newCap) {
        E[] temp = (E[]) new Object[newCap];
        for (int i = 0; i < size; i++) {
            temp[i] = data[i];
        }
        data = temp;
    }
    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }
    
    // Check if the index position can hold an element
    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    }
    
    // Check if the index position can add an element
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
    }
    private void display() {
        System.out.println("size = " + size + " cap = " + data.length);
        System.out.println(Arrays.toString(data));
    }
    public static void main(String[] args) {
        // Set initial capacity to 3
        MyArrayList<Integer> arr = new MyArrayList<>(3);
        // Add 5 elements
        for (int i = 1; i <= 5; i++) {
            arr.addLast(i);
        }
        arr.remove(3);
        arr.add(1, 9);
        arr.addFirst(100);
        int val = arr.removeLast();
        for (int i = 0; i < arr.size(); i++) {
            System.out.println(arr.get(i));
        }
    }
}#include <iostream>
#include <stdexcept>
#include <vector>
template<typename E>
class MyArrayList {
private:
    // The underlying array that actually stores the data
    E* data;
    // Record the current number of elements
    int size;
    // Maximum element capacity
    int cap;
    // Default initial capacity
    static const int INIT_CAP = 1;
public:
    MyArrayList() {
        this->data = new E[INIT_CAP];
        this->size = 0;
        this->cap = INIT_CAP;
    }
    MyArrayList(int initCapacity) {
        this->data = new E[initCapacity];
        this->size = 0;
        this->cap = initCapacity;
    }
    // Add
    void addLast(E e) {
        // Check if the capacity of the data array is enough
        if (size == cap) {
            resize(2 * cap);
        }
        // Insert the element at the end
        data[size] = e;
        size++;
    }
    void add(int index, E e) {
        // Check if the index is out of bounds
        checkPositionIndex(index);
        // Check if the capacity of the data array is enough
        if (size == cap) {
            resize(2 * cap);
        }
        // Shift data from data[index..] to data[index+1..]
        // Make room for the new element
        for (int i = size - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        // Insert the new element
        data[index] = e;
        size++;
    }
    void addFirst(E e) {
        add(0, e);
    }
    // Remove
    E removeLast() {
        if (size == 0) {
            throw std::out_of_range("NoSuchElementException");
        }
        // Can shrink capacity to save space
        if (size == cap / 4) {
            resize(cap / 2);
        }
        E deletedVal = data[size - 1];
        // Remove the last element
        // Must set the last element to null to prevent memory leak
        data[size - 1] = E();
        size--;
        return deletedVal;
    }
    E remove(int index) {
        // Check if the index is out of bounds
        checkElementIndex(index);
        // Can shrink capacity to save space
        if (size == cap / 4) {
            resize(cap / 2);
        }
        E deletedVal = data[index];
        // Shift data from data[index+1..] to data[index..]
        for (int i = index + 1; i < size; i++) {
            data[i - 1] = data[i];
        }
        data[size - 1] = E();
        size--;
        return deletedVal;
    }
    E removeFirst() {
        return remove(0);
    }
    // Get
    E get(int index) {
        // Check if the index is out of bounds
        checkElementIndex(index);
        return data[index];
    }
    // Set
    E set(int index, E element) {
        // Check if the index is out of bounds
        checkElementIndex(index);
        // Modify data
        E oldVal = data[index];
        data[index] = element;
        return oldVal;
    }
    // Utility methods
    int getSize() {
        return size;
    }
    bool isEmpty() {
        return size == 0;
    }
    // Resize the data array to newCap
    void resize(int newCap) {
        E* temp = new E[newCap];
        for (int i = 0; i < size; i++) {
            temp[i] = data[i];
        }
        // Release the memory of the original array
        delete[] data;
        data = temp;
        cap = newCap;
    }
    bool isElementIndex(int index) {
        return index >= 0 && index < size;
    }
    bool isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }
    // Check if an element can exist at the index position
    void checkElementIndex(int index) {
        if (!isElementIndex(index)) {
            throw std::out_of_range("Index out of bounds");
        }
    }
    // Check if an element can be added at the index position
    void checkPositionIndex(int index) {
        if (!isPositionIndex(index)) {
            throw std::out_of_range("Index out of bounds");
        }
    }
    void display() {
        std::cout << "size = " << size << " cap = " << cap << std::endl;
        for (int i = 0; i < size; i++) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    }
    ~MyArrayList() {
        delete[] data;
    }
};
int main() {
    // Set initial capacity to 3
    MyArrayList<int> arr(3);
    // Add 5 elements
    for (int i = 1; i <= 5; i++) {
        arr.addLast(i);
    }
    arr.remove(3);
    arr.add(1, 9);
    arr.addFirst(100);
    int val = arr.removeLast();
    // 100 1 9 2 3
    for (int i = 0; i < arr.getSize(); i++) {
        std::cout << arr.get(i) << std::endl;
    }
    return 0;
}class MyArrayList:
    # Default initial capacity
    INIT_CAP = 1
    def __init__(self, init_capacity=None):
        self.data = [None] * (init_capacity if init_capacity is not None else self.__class__.INIT_CAP)
        self.size = 0
    
    # Add
    def add_last(self, e):
        cap = len(self.data)
        # Check if data array has enough capacity
        if self.size == cap:
            self._resize(2 * cap)
        # Insert element at the end
        self.data[self.size] = e
        self.size += 1
    def add(self, index, e):
        # Check for index out of bounds
        self._check_position_index(index)
        cap = len(self.data)
        # Check if data array has enough capacity
        if self.size == cap:
            self._resize(2 * cap)
        # Shift data data[index..] -> data[index+1..]
        # Make room for the new element
        for i in range(self.size-1, index-1, -1):
            self.data[i+1] = self.data[i]
        
        # Insert new element
        self.data[index] = e
        self.size += 1
    def add_first(self, e):
        self.add(0, e)
    # Remove
    def remove_last(self):
        if self.size == 0:
            raise Exception("NoSuchElementException")
        cap = len(self.data)
        # Can shrink to save space
        if self.size == cap // 4:
            self._resize(cap // 2)
        deleted_val = self.data[self.size - 1]
        # Remove the last element
        self.data[self.size - 1] = None
        self.size -= 1
        return deleted_val
    def remove(self, index):
        # Check for index out of bounds
        self._check_element_index(index)
        cap = len(self.data)
        # Can shrink to save space
        if self.size == cap // 4:
            self._resize(cap // 2)
        deleted_val = self.data[index]
        # Shift data data[index+1..] -> data[index..]
        for i in range(index + 1, self.size):
            self.data[i - 1] = self.data[i]
        self.data[self.size - 1] = None
        self.size -= 1
        return deleted_val
    def remove_first(self):
        return self.remove(0)
    # Retrieve
    def get(self, index):
        # Check for index out of bounds
        self._check_element_index(index)
        return self.data[index]
    # Update
    def set(self, index, element):
        # Check for index out of bounds
        self._check_element_index(index)
        # Modify data
        old_val = self.data[index]
        self.data[index] = element
        return old_val
    # Utility methods
    def get_size(self):
        return self.size
    def is_empty(self):
        return self.size == 0
    # Resize data capacity to newCap
    def _resize(self, new_cap):
        temp = [None] * new_cap
        for i in range(self.size):
            temp[i] = self.data[i]
        self.data = temp
    def _is_element_index(self, index):
        return 0 <= index < self.size
    def _is_position_index(self, index):
        return 0 <= index <= self.size
    def _check_element_index(self, index):
        if not self._is_element_index(index):
            raise IndexError(f"Index: {index}, Size: {self.size}")
    def _check_position_index(self, index):
        if not self._is_position_index(index):
            raise IndexError(f"Index: {index}, Size: {self.size}")
    def display(self):
        print(f"size = {self.size}, cap = {len(self.data)}")
        print(self.data)
# Usage example
if __name__ == "__main__":
    arr = MyArrayList(init_capacity=3)
    # Add 5 elements
    for i in range(1, 6):
        arr.add_last(i)
    arr.remove(3)
    arr.add(1, 9)
    arr.add_first(100)
    val = arr.remove_last()
    # 100 1 9 2 3
    for i in range(arr.get_size()):
        print(arr.get(i))package main
import (
	"errors"
	"fmt"
)
type MyArrayList struct {
	// the underlying array that actually stores data
	data []interface{}
	// record the current number of elements
	size int
}
const INIT_CAP = 1
func NewMyArrayList() *MyArrayList {
	return NewMyArrayListWithCapacity(INIT_CAP)
}
func NewMyArrayListWithCapacity(initCapacity int) *MyArrayList {
	return &MyArrayList{
		data: make([]interface{}, initCapacity),
		size: 0,
	}
}
// add
func (list *MyArrayList) AddLast(value interface{}) {
	cap := len(list.data)
	// check if the capacity of the data array is sufficient
	if list.size == cap {
		list.resize(2 * cap)
	}
	// insert the element at the end
	list.data[list.size] = value
	list.size++
}
func (list *MyArrayList) Add(index int, value interface{}) error {
	// check for index out of bounds
	if err := list.checkPositionIndex(index); err != nil {
		return err
	}
	cap := len(list.data)
	// check if the capacity of the data array is sufficient
	if list.size == cap {
		list.resize(2 * cap)
	}
	// move data from data[index..] to data[index+1..]
	// make room for the new element
	for i := list.size - 1; i >= index; i-- {
		list.data[i+1] = list.data[i]
	}
	// insert the new element
	list.data[index] = value
	list.size++
	return nil
}
func (list *MyArrayList) AddFirst(value interface{}) error {
	return list.Add(0, value)
}
// delete
func (list *MyArrayList) RemoveLast() (interface{}, error) {
	if list.size == 0 {
		return nil, errors.New("No such element")
	}
	cap := len(list.data)
	// potentially shrink the capacity to save space
	if list.size == cap/4 {
		list.resize(cap / 2)
	}
	deletedVal := list.data[list.size-1]
	// remove the last element
	// must set the last element to nil to prevent memory leaks
	list.data[list.size-1] = nil
	list.size--
	return deletedVal, nil
}
func (list *MyArrayList) Remove(index int) (interface{}, error) {
	// check for index out of bounds
	if err := list.checkElementIndex(index); err != nil {
		return nil, err
	}
	cap := len(list.data)
	// potentially shrink the capacity to save space
	if list.size == cap/4 {
		list.resize(cap / 2)
	}
	deletedVal := list.data[index]
	// move data from data[index+1..] to data[index..]
	for i := index + 1; i < list.size; i++ {
		list.data[i-1] = list.data[i]
	}
	list.data[list.size-1] = nil
	list.size--
	return deletedVal, nil
}
func (list *MyArrayList) RemoveFirst() (interface{}, error) {
	return list.Remove(0)
}
// find
func (list *MyArrayList) Get(index int) (interface{}, error) {
	// check for index out of bounds
	if err := list.checkElementIndex(index); err != nil {
		return nil, err
	}
	return list.data[index], nil
}
// update
func (list *MyArrayList) Set(index int, value interface{}) (interface{}, error) {
	// check for index out of bounds
	if err := list.checkElementIndex(index); err != nil {
		return nil, err
	}
	// modify the data
	oldVal := list.data[index]
	list.data[index] = value
	return oldVal, nil
}
// utility methods
func (list *MyArrayList) Size() int {
	return list.size
}
func (list *MyArrayList) IsEmpty() bool {
	return list.size == 0
}
// resize the capacity of data to newCap
func (list *MyArrayList) resize(newCap int) {
	temp := make([]interface{}, newCap)
	for i := 0; i < list.size; i++ {
		temp[i] = list.data[i]
	}
	list.data = temp
}
func (list *MyArrayList) isElementIndex(index int) bool {
	return index >= 0 && index < list.size
}
func (list *MyArrayList) isPositionIndex(index int) bool {
	return index >= 0 && index <= list.size
}
// check if the index position can hold an element
func (list *MyArrayList) checkElementIndex(index int) error {
	if !list.isElementIndex(index) {
		return fmt.Errorf("Index: %d, Size: %d", index, list.size)
	}
	return nil
}
// check if the index position can add an element
func (list *MyArrayList) checkPositionIndex(index int) error {
	if !list.isPositionIndex(index) {
		return fmt.Errorf("Index: %d, Size: %d", index, list.size)
	}
	return nil
}
func (list *MyArrayList) Display() {
	fmt.Printf("size = %d cap = %d\n", list.size, len(list.data))
	fmt.Println(list.data)
}
func main() {
	// set initial capacity to 3
	arr := NewMyArrayListWithCapacity(3)
	// add 5 elements
	for i := 1; i <= 5; i++ {
		arr.AddLast(i)
	}
	arr.Remove(3)
	arr.Add(1, 9)
	arr.AddFirst(100)
	arr.RemoveLast()
	// 100 1 9 2 3
	for i := 0; i < arr.Size(); i++ {
		val, _ := arr.Get(i)
		fmt.Println(val)
	}
}class MyArrayList {
    constructor(initCapacity) {
        // the underlying array that actually stores the data
        this.data = [];
        // record the current number of elements
        this.size = 0;
        // default initial capacity
        this.INIT_CAP = 1;
        // initialization
        this.init(initCapacity);
    }
    init(initCapacity) {
        const capacity = initCapacity || this.INIT_CAP;
        this.data = new Array(capacity);
        this.size = 0;
    }
    // add
    addLast(e) {
        const cap = this.data.length;
        // check if the capacity of the data array is enough
        if (this.size === cap) {
            this.resize(2 * cap);
        }
        // insert element at the end
        this.data[this.size] = e;
        this.size++;
    }
    add(index, e) {
        // check for index out of bounds
        this.checkPositionIndex(index);
        const cap = this.data.length;
        // check if the capacity of the data array is enough
        if (this.size === cap) {
            this.resize(2 * cap);
        }
        // move data data[index..] -> data[index+1..]
        // make space for the new element
        for (let i = this.size - 1; i >= index; i--) {
            this.data[i + 1] = this.data[i];
        }
        // insert new element
        this.data[index] = e;
        this.size++;
    }
    addFirst(e) {
        this.add(0, e);
    }
    // remove
    removeLast() {
        if (this.size === 0) {
            throw new Error("NoSuchElementException");
        }
        const cap = this.data.length;
        // can reduce capacity to save space
        if (this.size === Math.floor(cap / 4)) {
            this.resize(Math.floor(cap / 2));
        }
        const deletedVal = this.data[this.size - 1];
        // remove the last element
        // must set the last element to null to prevent memory leak
        this.data[this.size - 1] = null;
        this.size--;
        return deletedVal;
    }
    remove(index) {
        // check for index out of bounds
        this.checkElementIndex(index);
        const cap = this.data.length;
        // can reduce capacity to save space
        if (this.size === Math.floor(cap / 4)) {
            this.resize(Math.floor(cap / 2));
        }
        const deletedVal = this.data[index];
        // move data data[index+1..] -> data[index..]
        for (let i = index + 1; i < this.size; i++) {
            this.data[i - 1] = this.data[i];
        }
        this.data[this.size - 1] = null;
        this.size--;
        return deletedVal;
    }
    removeFirst() {
        return this.remove(0);
    }
    // get
    get(index) {
        // check for index out of bounds
        this.checkElementIndex(index);
        return this.data[index];
    }
    // set
    set(index, element) {
        // check for index out of bounds
        this.checkElementIndex(index);
        // modify data
        const oldVal = this.data[index];
        this.data[index] = element;
        return oldVal;
    }
    // utility methods
    getSize() {
        return this.size;
    }
    isEmpty() {
        return this.size === 0;
    }
    // change the capacity of data to newCap
    resize(newCap) {
        const temp = new Array(newCap);
        for (let i = 0; i < this.size; i++) {
            temp[i] = this.data[i];
        }
        this.data = temp;
    }
    isElementIndex(index) {
        return index >= 0 && index < this.size;
    }
    isPositionIndex(index) {
        return index >= 0 && index <= this.size;
    }
    // check if the index position can contain an element
    checkElementIndex(index) {
        if (!this.isElementIndex(index)) {
            throw new Error("Index: " + index + ", Size: " + this.size);
        }
    }
    // check if the index position can add an element
    checkPositionIndex(index) {
        if (!this.isPositionIndex(index)) {
            throw new Error("Index: " + index + ", Size: " + this.size);
        }
    }
    display() {
        console.log("size = " + this.size + " cap = " + this.data.length);
        console.log(this.data);
    }
}
// set the initial capacity to 3
const arr = new MyArrayList(3);
// add 5 elements
for (let i = 1; i <= 5; i++) {
    arr.addLast(i);
}
arr.remove(3);
arr.add(1, 9);
arr.addFirst(100);
const val = arr.removeLast();
// 100 1 9 2 3
for (let i = 0; i < arr.getSize(); i++) {
    console.log(arr.get(i));
}