Dynamic Array Code Implementation
Prerequisite Knowledge
Before reading this article, you need to first learn:
Key Points
Below, I will provide a simple implementation of a dynamic array, including basic functionalities such as addition, deletion, search, and update. Here are some key points to pay attention to when reviewing the code.
Key Point 1: Automatic Expansion and Reduction
In the previous chapter Array Basics, we only discussed expanding the array when adding elements, without mentioning reduction.
In practical use, reducing the size of a dynamic array is also an important optimization technique. For example, if a dynamic array has allocated memory space for 1000 elements but only stores 10 elements, there are 990 spaces that are unused. To avoid resource waste, we can appropriately reduce the storage space, known as reduction.
We implement a simple strategy for automatic expansion and reduction:
- When the number of array elements reaches the capacity limit of the underlying static array, expand to twice its original size.
- When the number of array elements is reduced to 1/4 of the capacity of the underlying static array, reduce to half its original size.
Key Point 2: Index Out of Bounds Check
In the code implementation below, there are two methods for checking index boundaries, checkElementIndex
and checkPositionIndex
. The difference between them lies in index < size
and index <= size
.
Why does checkPositionIndex
allow index == size
? This is because checkPositionIndex
is specifically used for handling the insertion of elements into the array.
For example, in an array nums
, for each element, a valid index is always index < size
:
nums = [5, 6, 7, 8]
index 0 1 2 3
However, when inserting a new element into the array, the possible insertion position is not at the element's index, but in the gaps between indices:
nums = [ | 5 | 6 | 7 | 8 | ]
index 0 1 2 3 4
These gaps are all valid insertion positions, so index == size
is also valid. This highlights the distinction between checkPositionIndex
and checkElementIndex
.
Key Point 3: Preventing Memory Leak When Deleting Elements
From an algorithmic perspective, we don't necessarily need to concern ourselves with how to handle deleted elements. However, in code implementation, we must be cautious of potential memory leaks.
In the code implementation I provided, when deleting elements, I always set the deleted element to null
, using Java as an example:
// 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;
}
The garbage collection mechanism in Java is based on reachability analysis from graph algorithms. If an object can no longer be accessed, the memory it occupies will be released. Otherwise, the garbage collector considers this 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, allowing you to access the object through data[size - 1]
. Therefore, the object is considered reachable, and its memory will not be released, which can lead to a memory leak.
Other languages with garbage collection likely work similarly. It is essential to understand the garbage collection mechanism of the programming language you use to write bug-free code.
Other Details for Optimization
The following code is not a complete implementation and has many areas for potential optimization. For instance, I used a for
loop to copy array data, which is inefficient. Most programming languages provide more efficient methods for array copying, such as Java's System.arraycopy
.
However, no matter how you optimize, the essence is that data must be moved, and the time complexity remains . The focus of this article is to help you understand the basic implementation and time complexity of array operations like insert, delete, search, and update. If these details interest you, you can explore the source code of the programming language's standard library for further study.
How to Verify Your Implementation?
You can use LeetCode's problem 707 "Design Linked List" to verify your implementation. Although this problem is about linked lists, it does not require you to implement a linked list specifically. The main goal is to use its test cases to check if your insert, delete, search, and update functions are correct.
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;
// Default initial capacity
static const int INIT_CAP = 1;
public:
MyArrayList() {
this->data = new E[INIT_CAP];
this->size = 0;
}
MyArrayList(int initCapacity) {
this->data = new E[initCapacity];
this->size = 0;
}
// Add
void addLast(E e) {
int cap = sizeof(data) / sizeof(data[0]);
// 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);
int cap = sizeof(data) / sizeof(data[0]);
// 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");
}
int cap = sizeof(data) / sizeof(data[0]);
// 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] = NULL;
size--;
return deletedVal;
}
E remove(int index) {
// Check if the index is out of bounds
checkElementIndex(index);
int cap = sizeof(data) / sizeof(data[0]);
// 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] = NULL;
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;
}
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 = " << sizeof(data) / sizeof(data[0]) << 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 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 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.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
size() {
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.size; i++) {
console.log(arr.get(i));
}