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 3
But 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 4
All 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));
}