动态数组代码实现
接下来我会给出一组简单的动态数组,包括了基本的增删改查的功能。
必要的头文件
#include <iostream>
#include <stdexcept>
#include <vector>
类定义
template<typename E>
class MyArrayList {
定义了一个模板类 MyArrayList,可以存储任意类型 E 的元素。
私有成员变量
private:
E* data; // 底层数组,用于存储数据
int size; // 当前数组中元素的数量
int cap; // 数组的最大容量
static const int INIT_CAP = 1; // 默认初始容量
构造函数
public:
MyArrayList() {
this->data = new E[INIT_CAP];
this->size = 0;
this->cap = INIT_CAP;
}
默认构造函数,初始化一个容量为 INIT_CAP 的数组。
MyArrayList(int initCapacity) {
this->data = new E[initCapacity];
this->size = 0;
this->cap = initCapacity;
}
自定义构造函数,允许用户指定初始容量。
添加元素
void addLast(E e) {
if (size == cap) {
resize(2 * cap); // 如果容量不足,扩容为原来的两倍
}
data[size] = e; // 在数组末尾插入元素
size++; // 元素数量加 1
}
在数组末尾添加一个元素。
如果容量不足,调用 resize 方法扩容。
void add(int index, E e) {
checkPositionIndex(index); // 检查索引是否合法
if (size == cap) {
resize(2 * cap); // 如果容量不足,扩容
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i]; // 将索引 index 及之后的元素向后移动
}
data[index] = e; // 在索引 index 处插入新元素
size++; // 元素数量加 1
}
在指定索引位置插入一个元素。
如果容量不足,扩容。
将索引 index 及之后的元素向后移动,为新元素腾出位置。
void addFirst(E e) {
add(0, e); // 在数组开头插入元素
}
在数组开头插入一个元素,调用 add 方法。
删除元素
E removeLast() {
if (size == 0) {
throw std::out_of_range("NoSuchElementException"); // 如果数组为空,抛出异常
}
if (size == cap / 4) {
resize(cap / 2); // 如果元素数量是容量的 1/4,缩容为一半
}
E deletedVal = data[size - 1]; // 获取最后一个元素
data[size - 1] = E(); // 将最后一个元素置为空
size--; // 元素数量减 1
return deletedVal; // 返回被删除的元素
}
删除数组末尾的元素。
如果元素数量较少,缩容以节省空间。
E remove(int index) {
checkElementIndex(index); // 检查索引是否合法
if (size == cap / 4) {
resize(cap / 2); // 如果元素数量是容量的 1/4,缩容
}
E deletedVal = data[index]; // 获取被删除的元素
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i]; // 将索引 index 之后的元素向前移动
}
data[size - 1] = E(); // 将最后一个元素置为空
size--; // 元素数量减 1
return deletedVal; // 返回被删除的元素
}
删除指定索引位置的元素。
将索引 index 之后的元素向前移动。
E removeFirst() {
return remove(0); // 删除数组开头的元素
}
删除数组开头的元素,调用 remove 方法。
获取元素
E get(int index) {
checkElementIndex(index); // 检查索引是否合法
return data[index]; // 返回指定索引位置的元素
}
获取指定索引位置的元素。
修改元素
E set(int index, E element) {
checkElementIndex(index); // 检查索引是否合法
E oldVal = data[index]; // 获取旧值
data[index] = element; // 修改为新值
return oldVal; // 返回旧值
}
修改指定索引位置的元素。
工具方法
int getSize() {
return size; // 返回数组中元素的数量
}
bool isEmpty() {
return size == 0; // 判断数组是否为空
}
提供获取数组大小和判断是否为空的方法。
动态调整容量
void resize(int newCap) {
E* temp = new E[newCap]; // 创建一个新数组
for (int i = 0; i < size; i++) {
temp[i] = data[i]; // 将原数组的数据复制到新数组
}
delete[] data; // 释放原数组的内存
data = temp; // 更新 data 指向新数组
cap = newCap; // 更新容量
}
动态调整数组的容量。
索引检查
bool isElementIndex(int index) {
return index >= 0 && index < size; // 检查索引是否在合法范围内
}
bool isPositionIndex(int index) {
return index >= 0 && index <= size; // 检查索引是否在合法范围内(允许等于 size)
}
void checkElementIndex(int index) {
if (!isElementIndex(index)) {
throw std::out_of_range("Index out of bounds"); // 如果索引不合法,抛出异常
}
}
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() {
MyArrayList<int> arr(3); // 创建一个初始容量为 3 的数组
for (int i = 1; i <= 5; i++) {
arr.addLast(i); // 添加 5 个元素
}
arr.remove(3); // 删除索引 3 的元素
arr.add(1, 9); // 在索引 1 插入 9
arr.addFirst(100); // 在开头插入 100
int val = arr.removeLast(); // 删除最后一个元素
// 打印数组内容
for (int i = 0; i < arr.getSize(); i++) {
std::cout << arr.get(i) << std::endl;
}
return 0;
}
测试 MyArrayList 的功能:
创建一个数组。
添加、删除、插入元素。
打印数组内容。
几个需要关注的地方
自动扩缩容
在实际使用动态数组时,缩容也是重要的优化手段。比方说一个动态数组开辟了能够存储 1000 个元素的连续内存空间,但是实际只存了 10 个元素,那就有 990 个空间是空闲的。为了避免资源浪费,我们其实可以适当缩小存储空间,这就是缩容。
-
我们这里就实现一个简单的扩缩容的策略:
- 当数组元素个数达到底层静态数组的容量上限时,扩容为原来的 2 倍;
- 当数组元素个数缩减到底层静态数组的容量的 1/4 时,缩容为原来的 1/2。
注意:有很多同学问道为啥是1/4:
其实1/4作为缩容的阈值是一个折中方案,可以较好地平衡内存使用和操作效率。当然,这个值也不是绝对的,可以根据具体的应用场景和需求进行调整。
索引越界的检查
下面的代码实现中,有两个检查越界的方法,分别是 checkElementIndex 和 checkPositionIndex,你可以看到它俩的区别仅仅在于 index < size 和 index <= size。
为什么 checkPositionIndex 可以允许 index == size 呢,因为这个 checkPositionIndex 是专门用来处理在数组中插入元素的情况。
比方说有这样一个 nums 数组,对于每个元素来说,合法的索引一定是 index < size:
nums = [5, 6, 7, 8]
index 0 1 2 3
但如果是要在数组中插入新元素,那么新元素可能的插入位置并不是元素的索引,而是索引之间的空隙:
nums = [ | 5 | 6 | 7 | 8 | ]
index 0 1 2 3 4
这些空隙都是合法的插入位置,所以说 index == size 也是合法的。这就是 checkPositionIndex 和 checkElementIndex 的区别。
删除元素谨防内存泄漏
单从算法的角度,其实并不需要关心被删掉的元素应该如何处理,但是具体到代码实现,我们需要注意可能出现的内存泄漏。
在我给出的代码实现中,删除元素时,我都会把被删除的元素置为空
不管我们怎么优化,本质上也是要搬移数据,时间复杂度都是 O(n)。本文的重点在于让你理解数组增删查改 API 的基本实现思路以及时间复杂度,如果对这些细节感兴趣,可以找到编程语言标准库的源码深入研究。
小结
有些同学会问这个MyArrayList和我们平时用的vector有啥区别?
MyArrayList 是一个自定义的类,它不像标准库中的 std::vector 那样,可以通过包含头文件(如 <vector>)直接使用。MyArrayList 是你(或其他人)自己实现的一个类,所以你需要自己编写其中的函数,或者使用已经实现好的代码。
可以这样理解:MyArrayList 是一个简化的动态数组实现,它展示了 std::vector 中一些核心功能的底层实现方式。虽然 MyArrayList 和 std::vector 在功能上有很多相似之处,但它们的实现细节和优化程度有所不同。
我们在这里是为了学习动态数组的实现原理,MyArrayList 是一个很好的练习。但在实际算法题目练习中,建议使用 std::vector,因为它功能丰富且经过优化。
后面我们将继续学习各种算法程序,一起学习,有任何问题评论区里面相互交流!!!
1万+

被折叠的 条评论
为什么被折叠?



