一、静态数组
cpp
静态数组在创建的时候就要确定数组的元素类型,元素数量。只有在C++、Java、Golong这类语言中财提供了创建静态数组的方式,类似Python、JS并没有提供。
静态数组的用法比较原始,实际开发中很少用到,算法题没必要用,我们一般直接用动态数组。
定义静态数组:
cpp
// 定义一个大小为 10 的静态数组
int arr[10];
// 用 memset 函数把数组的值初始化为 0
memset(arr, 0, sizeof(arr));
// 使用索引赋值
arr[0] = 1;
arr[1] = 2;
// 使用索引取值
int a = arr[0];
int arr[10]主要执行步骤:
- 在内存中开辟了一段连续的内存空间,大小是10*sizeof(int)字节。
- 定义了一个名为arr的数组指针,指向这段内存空间的首地址。
arr[1]=2执行步骤:
- 计算arr的首地址加上1*sizeof(int)字节的偏移量,找到内存空间中的第二个元素的首地址。
- 从这个地址开始的4个字节的内存空间中写入了整数2.
- 1、为什么数组的索引从 0 开始?就是方便取地址。arr[0] 就是 arr 的首地址,从这个地址往后的 4 个字节存储着第一个元素的值;arr[1] 就是 arr 的首地址加上 1 * 4 字节,也就是第二个元素的首地址,这个地址往后的 4 个字节存储着第二个元素的值。arr[2], arr[3] 以此类推。
- 2、因为数组的名字 arr 就指向整块内存的首地址,所以数组名 arr 就是一个指针。你直接取这个地址的值,就是第一个元素的值。也就是说,*arr 的值就是 arr[0],即第一个元素的值。
- 3、如果不用 memset 这种函数初始化数组的值,那么数组内的值是不确定的。因为 int arr[10] 这个语句只是请操作系统在内存中开辟了一块连续的内存空间,你也不知道这块空间是谁使用过的二手内存,你也不知道里面存了什么奇奇怪怪的东西。所以一般我们会用 memset 函数把这块内存空间的值初始化一下再使用。
所以,我们获得了数组的超能力「随机访问」:只要给定任何一个数组索引,我可以在 O(1) 的时间内直接获取到对应元素的值。
综上,数组的随机访问的时间复杂度是O(1)。
二、静态数组的增删改查
1.增
(1)情况一,数组末尾追加(append)元素
O(1)
cpp
// 大小为 10 的数组已经装了 4 个元素
int arr[10];
for (int i = 0; i < 4; i++) {
arr[i] = i;
}
// 现在想在数组末尾追加一个元素 4
arr[4] = 4;
// 再在数组末尾追加一个元素 5
arr[5] = 5;
// 依此类推
// ...
(2)情况二,数组中间插入(insert)元素
O(n)
cpp
// 大小为 10 的数组已经装了 4 个元素
int arr[10];
for (int i = 0; i < 4; i++) {
arr[i] = i;
}
// 在索引 2 置插入元素 666
// 需要把索引 2 以及之后的元素都往后移动一位
// 注意要倒着遍历数组中已有元素避免覆盖,不懂的话请看下方可视化面板
for (int i = 4; i > 2; i--) {
arr[i] = arr[i - 1];
}
// 现在第 3 个位置空出来了,可以插入新元素
arr[2] = 666;
(3)情况三,数组空间已满
O(n)
那怎么办呢?只能重新申请一块更大的内存空间,把原来的数据复制过去,再插入新的元素,这就是数组的「扩容」操作。
比方说,我重新创建一个更大的数组 int arr[20],然后把原来的 10 个元素复制过去,这样就有空余位置插入新的元素了。
大概的逻辑是这样的:
cpp
// 大小为 10 的数组已经装满了
int arr[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
// 现在想在数组末尾追加一个元素 10
// 需要先扩容数组
int newArr[20];
// 把原来的 10 个元素复制过去
for (int i = 0; i < 10; i++) {
newArr[i] = arr[i];
}
// 释放旧数组的内存空间
// ...
// 在新的大数组中追加新元素
newArr[10] = 10;
2.删
(1)情况一,删除末尾元素
O(1)
cpp
// 大小为 10 的数组已经装了 5 个元素
int arr[10];
for (int i = 0; i < 5; i++) {
arr[i] = i;
}
// 删除末尾元素,暂时用 -1 代表元素已删除
arr[4] = -1;
(2)情况二,删除中间元素
O(n)
cpp
// 大小为 10 的数组已经装了 5 个元素
int arr[10];
for (int i = 0; i < 5; i++) {
arr[i] = i;
}
// 删除 arr[1]
// 需要把 arr[1] 之后的元素都往前移动一位
// 注意要正着遍历数组中已有元素避免覆盖,不懂的话请看下方可视化面板
for (int i = 1; i < 4; i++) {
arr[i] = arr[i + 1];
}
// 最后一个元素置为 -1 代表已删除
arr[4] = -1;
3.改
给定指定的索引值,修改索引对应的元素的值,时间复杂度O(1).
4.查
给定指定的索引值,查询索引对应的元素的值,时间复杂度O(1).
三、动态数组
动态数组底层还是静态数组,只是自动帮我们进行数组空间的扩缩容,并把增删查改操作进行了封装,让我们使用起来更方便而已。
基本使用方法:
cpp
// 创建动态数组
// 不用显式指定数组大小,它会根据实际存储的元素数量自动扩缩容
vector<int> arr;
for (int i = 0; i < 10; i++) {
// 在末尾追加元素,时间复杂度 O(1)
arr.push_back(i);
}
// 在中间插入元素,时间复杂度 O(N)
// 在索引 2 的位置插入元素 666
arr.insert(arr.begin() + 2, 666);
// 在头部插入元素,时间复杂度 O(N)
arr.insert(arr.begin(), -1);
// 删除末尾元素,时间复杂度 O(1)
arr.pop_back();
// 删除中间元素,时间复杂度 O(N)
// 删除索引 2 的元素
arr.erase(arr.begin() + 2);
// 根据索引查询元素,时间复杂度 O(1)
int a = arr[0];
// 根据索引修改元素,时间复杂度 O(1)
arr[0] = 100;
// 根据元素值查找索引,时间复杂度 O(N)
int index = find(arr.begin(), arr.end(), 666) - arr.begin();
1.自动扩缩容
在实际使用动态数组时,缩容也是重要的优化手段。比方说一个动态数组开辟了能够存储 1000 个元素的连续内存空间,但是实际只存了 10个元素,那就有 990 个空间是空闲的。为了避免资源浪费,我们其实可以适当缩小存储空间,这就是缩容。
我们这里就实现一个简单的扩缩容的策略:
- 当数组元素个数达到底层静态数组的容量上限时,扩容为原来的 2 倍;
- 当数组元素个数缩减到底层静态数组的容量的 1/4 时,缩容为原来的
1/2。
2.索引越界的检查
下面的代码实现中,有两个检查越界的方法,分别是 checkElementIndex 和 checkPositionIndex,你可以看到它俩的区别仅仅在于 index < size 和 index <= size。
为什么 checkPositionIndex 可以允许 index == size 呢,因为这个 checkPositionIndex 是专门用来处理在数组中插入元素的情况。
比方说有这样一个 nums 数组,对于每个元素来说,合法的索引一定是 index < size:
cpp
nums = [5, 6, 7, 8]
index 0 1 2 3
但如果是要在数组中插入新元素,那么新元素可能的插入位置并不是元素的索引,而是索引之间的空隙:
cpp
nums = [ | 5 | 6 | 7 | 8 | ]
index 0 1 2 3 4
这些空隙都是合法的插入位置,所以说 index == size 也是合法的。这就是 checkPositionIndex 和 checkElementIndex 的区别
3.删除元素谨防内存泄漏
单从算法的角度,其实并不需要关心被删掉的元素应该如何处理,但是具体到代码实现,我们需要注意可能出现的内存泄漏。
在我给出的代码实现中,删除元素时,我都会把被删除的元素置为 null,以 Java 为例:
cpp
// 删
public E removeLast() {
E deletedVal = data[size - 1];
// 删除最后一个元素
// 必须给最后一个元素置为 null,否则会内存泄漏
data[size - 1] = null;
size--;
return deletedVal;
}
Java 的垃圾回收机制是基于
图算法 的可达性分析,如果一个对象再也无法被访问到,那么这个对象占用的内存才会被释放;否则,垃圾回收器会认为这个对象还在使用中,就不会释放这个对象占用的内存。
如果你不执行 data[size - 1] = null 这行代码,那么 data[size - 1] 这个引用就会一直存在,你可以通过 data[size - 1] 访问这个对象,所以这个对象被认为是可达的,它的内存就一直不会被释放,进而造成内存泄漏。
其他带垃圾回收功能的语言应该也是类似的,你可以具体了解一下你使用的编程语言的垃圾回收机制,这是写出无 bug 代码的基本要求。
四、动态数组代码实现
cpp
#include <iostream>
#include <stdexcept>
#include <vector>
template<typename E>
class MyArrayList {
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;
}
MyArrayList(int initCapacity) {
this->data = new E[initCapacity];
this->size = 0;
this->cap = initCapacity;
}
// 增
void addLast(E e) {
// 看 data 数组容量够不够
if (size == cap) {
resize(2 * cap);
}
// 在尾部插入元素
data[size] = e;
size++;
}
void add(int index, E e) {
// 检查索引越界
checkPositionIndex(index);
// 看 data 数组容量够不够
if (size == cap) {
resize(2 * cap);
}
// 搬移数据 data[index..] -> data[index+1..]
// 给新元素腾出位置
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
// 插入新元素
data[index] = e;
size++;
}
void addFirst(E e) {
add(0, e);
}
// 删
E removeLast() {
if (size == 0) {
throw std::out_of_range("NoSuchElementException");
}
// 可以缩容,节约空间
if (size == cap / 4) {
resize(cap / 2);
}
E deletedVal = data[size - 1];
// 删除最后一个元素
// 必须给最后一个元素置为 null,否则会内存泄漏
data[size - 1] = E();
size--;
return deletedVal;
}
E remove(int index) {
// 检查索引越界
checkElementIndex(index);
// 可以缩容,节约空间
if (size == cap / 4) {
resize(cap / 2);
}
E deletedVal = data[index];
// 搬移数据 data[index+1..] -> 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);
}
// 查
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;
}
// 将 data 的容量改为 newCap
void resize(int newCap) {
E* temp = new E[newCap];
for (int i = 0; i < size; i++) {
temp[i] = data[i];
}
// 释放原数组内存
delete[] data;
data = temp;
cap = newCap;
}
bool isElementIndex(int index) {
return index >= 0 && index < size;
}
bool isPositionIndex(int index) {
return index >= 0 && index <= size;
}
// 检查 index 索引位置是否可以存在元素
void checkElementIndex(int index) {
if (!isElementIndex(index)) {
throw std::out_of_range("Index out of bounds");
}
}
// 检查 index 索引位置是否可以添加元素
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() {
// 初始容量设置为 3
MyArrayList<int> arr(3);
// 添加 5 个元素
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;
}