动态数组代码实现
Key Points
Below, I will directly provide a simple dynamic array code implementation, including basic functions such as adding, deleting, searching, and updating elements. Here are a few key points to focus on while reading the code.
Key Point 1: Automatic Expansion and Contraction
In the previous chapter Array Basics, we only mentioned that an array might need to expand when adding elements, but we didn't talk about contraction.
In practice, contraction is also an important optimization for dynamic arrays. For example, if a dynamic array has allocated space for 1000 elements but only stores 10 elements, there are 990 free spaces. To avoid resource waste, we can appropriately reduce the storage space, which is contraction.
Here, we implement a simple strategy for expansion and contraction:
- When the number of elements in the array reaches the capacity limit of the underlying static array, expand to twice the original size.
- When the number of elements in the array reduces to 1/4 of the capacity of the underlying static array, contract to half the original size.
Key Point 2: Index Out-of-Bounds Check
In the following code implementation, there are two methods to check for out-of-bounds errors, checkElementIndex
and checkPositionIndex
. The difference between them is just index < size
and index <= size
.
Why can checkPositionIndex
allow index == size
? Because checkPositionIndex
is specifically used for handling the insertion of elements into the array.
For example, consider an array nums
. For each element, a valid index must be index < size
:
nums = [5, 6, 7, 8]
index 0 1 2 3
But if you want to insert a new element into the array, the possible positions for the new element are not the element indices but 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 is the difference between checkPositionIndex
and checkElementIndex
.
Key Point 3: Preventing Memory Leaks When Deleting Elements
From an algorithmic perspective, you don't need to worry about how the deleted elements are handled, but in the specific code implementation, you need to be aware of potential memory leaks.
In the code implementation I provided, when deleting an element, I set the deleted element 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 from graph algorithms. If an object can no longer be accessed, its memory will be released. Otherwise, the garbage collector will consider the object still in use and won't free its memory.
If you do not execute the line data[size - 1] = null
, the reference at data[size - 1]
will persist. You can access the object through data[size - 1]
, so the object is considered reachable, and its memory will not be freed, leading to a memory leak.
Other languages with garbage collection should work similarly. It's essential to understand the garbage collection mechanism of the programming language you use to write bug-free code.
Other Optimization Details
The following code isn't a perfect implementation and has several points that can be further optimized. For example, I used a for loop to copy array data, which is inefficient. Most programming languages provide more efficient ways to copy arrays, such as Java's System.arraycopy
.
However, no matter how you optimize it, the essence is still data movement, and the time complexity remains O(n)
. The focus of this article is to help you understand the basic implementation ideas and time complexity of array operations like add, delete, search, and update. If you are interested in these details, you can delve into the source code of the standard library of your programming language.
How to Verify Your Implementation?
You can use LeetCode Problem 707 "Design Linked List" to verify your implementation. Although this problem is about linked lists, it doesn't actually know whether you're using a linked list underneath. We mainly use its test cases to verify whether your add, 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: " + index + ", Size: " + size);
}
}
// Check if an element can be added at the index position
void checkPositionIndex(int index) {
if (!isPositionIndex(index)) {
throw std::out_of_range("Index: " + index + ", Size: " + size);
}
}
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;
}
};
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 IndexOutOfBoundsException(f"Index: {index}, Size: {self.size}")
def _check_position_index(self, index):
if not self._is_position_index(index):
raise IndexOutOfBoundsException(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()
for i in range(arr.size):
print(arr.get(i))
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)
val, _ := arr.RemoveLast()
for i := 0; i < arr.Size(); i++ {
val, _ := arr.Get(i)
fmt.Println(val)
}
}
var MyArrayList = function() {
// 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(arguments[0]);
};
MyArrayList.prototype.init = function(initCapacity) {
var capacity = initCapacity || this.INIT_CAP;
this.data = new Array(capacity);
this.size = 0;
};
// add
MyArrayList.prototype.addLast = function(e) {
var 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++;
};
MyArrayList.prototype.add = function(index, e) {
// check for index out of bounds
this.checkPositionIndex(index);
var 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 (var i = this.size - 1; i >= index; i--) {
this.data[i + 1] = this.data[i];
}
// insert new element
this.data[index] = e;
this.size++;
};
MyArrayList.prototype.addFirst = function(e) {
this.add(0, e);
};
// remove
MyArrayList.prototype.removeLast = function() {
if (this.size === 0) {
throw new Error("NoSuchElementException");
}
var cap = this.data.length;
// can reduce capacity to save space
if (this.size === Math.floor(cap / 4)) {
this.resize(Math.floor(cap / 2));
}
var 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;
};
MyArrayList.prototype.remove = function(index) {
// check for index out of bounds
this.checkElementIndex(index);
var cap = this.data.length;
// can reduce capacity to save space
if (this.size === Math.floor(cap / 4)) {
this.resize(Math.floor(cap / 2));
}
var deletedVal = this.data[index];
// move data data[index+1..] -> data[index..]
for (var i = index + 1; i < this.size; i++) {
this.data[i - 1] = this.data[i];
}
this.data[this.size - 1] = null;
this.size--;
return deletedVal;
};
MyArrayList.prototype.removeFirst = function() {
return this.remove(0);
};
// get
MyArrayList.prototype.get = function(index) {
// check for index out of bounds
this.checkElementIndex(index);
return this.data[index];
};
// set
MyArrayList.prototype.set = function(index, element) {
// check for index out of bounds
this.checkElementIndex(index);
// modify data
var oldVal = this.data[index];
this.data[index] = element;
return oldVal;
};
// utility methods
MyArrayList.prototype.size = function() {
return this.size;
};
MyArrayList.prototype.isEmpty = function() {
return this.size === 0;
};
// change the capacity of data to newCap
MyArrayList.prototype.resize = function(newCap) {
var temp = new Array(newCap);
for (var i = 0; i < this.size; i++) {
temp[i] = this.data[i];
}
this.data = temp;
};
MyArrayList.prototype.isElementIndex = function(index) {
return index >= 0 && index < this.size;
};
MyArrayList.prototype.isPositionIndex = function(index) {
return index >= 0 && index <= this.size;
};
// check if the index position can contain an element
MyArrayList.prototype.checkElementIndex = function(index) {
if (!this.isElementIndex(index)) {
throw new Error("Index: " + index + ", Size: " + this.size);
}
};
// check if the index position can add an element
MyArrayList.prototype.checkPositionIndex = function(index) {
if (!this.isPositionIndex(index)) {
throw new Error("Index: " + index + ", Size: " + this.size);
}
};
MyArrayList.prototype.display = function() {
console.log("size = " + this.size + " cap = " + this.data.length);
console.log(this.data);
};
// set the initial capacity to 3
var arr = new MyArrayList(3);
// add 5 elements
for (var i = 1; i <= 5; i++) {
arr.addLast(i);
}
arr.remove(3);
arr.add(1, 9);
arr.addFirst(100);
var val = arr.removeLast();
for (var i = 0; i < arr.size; i++) {
console.log(arr.get(i));
}