动态数组代码实现

动态数组代码实现

接下来我会给出一组简单的动态数组,包括了基本的增删改查的功能。

必要的头文件
#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作为缩容的阈值是一个折中方案,可以较好地平衡内存使用和操作效率。当然,这个值也不是绝对的,可以根据具体的应用场景和需求进行调整。

索引越界的检查

下面的代码实现中,有两个检查越界的方法,分别是 checkElementIndexcheckPositionIndex,你可以看到它俩的区别仅仅在于 index < sizeindex <= 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 也是合法的。这就是 checkPositionIndexcheckElementIndex 的区别。

删除元素谨防内存泄漏

单从算法的角度,其实并不需要关心被删掉的元素应该如何处理,但是具体到代码实现,我们需要注意可能出现的内存泄漏。
在我给出的代码实现中,删除元素时,我都会把被删除的元素置为空

不管我们怎么优化,本质上也是要搬移数据,时间复杂度都是 O(n)。本文的重点在于让你理解数组增删查改 API 的基本实现思路以及时间复杂度,如果对这些细节感兴趣,可以找到编程语言标准库的源码深入研究。

小结

有些同学会问这个MyArrayList和我们平时用的vector有啥区别?
MyArrayList 是一个自定义的类,它不像标准库中的 std::vector 那样,可以通过包含头文件(如 <vector>)直接使用。MyArrayList 是你(或其他人)自己实现的一个类,所以你需要自己编写其中的函数,或者使用已经实现好的代码。

可以这样理解:MyArrayList 是一个简化的动态数组实现,它展示了 std::vector 中一些核心功能的底层实现方式。虽然 MyArrayListstd::vector 在功能上有很多相似之处,但它们的实现细节和优化程度有所不同。

我们在这里是为了学习动态数组的实现原理,MyArrayList 是一个很好的练习。但在实际算法题目练习中,建议使用 std::vector,因为它功能丰富且经过优化。

后面我们将继续学习各种算法程序,一起学习,有任何问题评论区里面相互交流!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值