当前位置: 首页 > news >正文

数据结构<C++>——数组

一、静态数组

静态数组在创建的时候就要确定数组的元素类型,元素数量。只有在C++、Java、Golong这类语言中财提供了创建静态数组的方式,类似Python、JS并没有提供。
静态数组的用法比较原始,实际开发中很少用到,算法题没必要用,我们一般直接用动态数组。

定义静态数组:

// 定义一个大小为 10 的静态数组
int arr[10];// 用 memset 函数把数组的值初始化为 0
memset(arr, 0, sizeof(arr));// 使用索引赋值
arr[0] = 1;
arr[1] = 2;// 使用索引取值
int a = arr[0];

int arr[10]主要执行步骤:

  1. 在内存中开辟了一段连续的内存空间,大小是10*sizeof(int)字节。
  2. 定义了一个名为arr的数组指针,指向这段内存空间的首地址。

arr[1]=2执行步骤:

  1. 计算arr的首地址加上1*sizeof(int)字节的偏移量,找到内存空间中的第二个元素的首地址。
  2. 从这个地址开始的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)

// 大小为 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)

// 大小为 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 个元素复制过去,这样就有空余位置插入新的元素了。

大概的逻辑是这样的:

// 大小为 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)

// 大小为 10 的数组已经装了 5 个元素
int arr[10];
for (int i = 0; i < 5; i++) {arr[i] = i;
}// 删除末尾元素,暂时用 -1 代表元素已删除
arr[4] = -1;

(2)情况二,删除中间元素

O(n)

// 大小为 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).

三、动态数组

动态数组底层还是静态数组,只是自动帮我们进行数组空间的扩缩容,并把增删查改操作进行了封装,让我们使用起来更方便而已。

基本使用方法:

// 创建动态数组
// 不用显式指定数组大小,它会根据实际存储的元素数量自动扩缩容
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:

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 的区别

3.删除元素谨防内存泄漏

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

在我给出的代码实现中,删除元素时,我都会把被删除的元素置为 null,以 Java 为例:

// 删
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 代码的基本要求。

四、动态数组代码实现

#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 的容量改为 newCapvoid 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() {// 初始容量设置为 3MyArrayList<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 3for (int i = 0; i < arr.getSize(); i++) {std::cout << arr.get(i) << std::endl;}return 0;
}
http://www.dtcms.com/a/532597.html

相关文章:

  • vidhub v1.3.13 |聚合主流网盘,自动刮削整理影视资源,有网盘会员的可入,或者使用不限速网盘
  • 专业网站制作地址杭州网站怎么制作
  • 免费素材网站排行榜征求网站建设
  • 汉口网站制作公司营销网站模板下载
  • 将有序数组转换为二叉搜索树解题思路
  • c语言实现栈【由浅入深-数据结构】
  • 教做家常菜的视频网站wordpress 搭建个人博客
  • 【Go】C++ 转 Go 第(五)天:Goroutine 与 Channel | Go 并发编程基础
  • 算法:283. 移动零
  • 设计微信公众号的网站吗举例说明seo
  • 欧米伽男士手表官方网站wordpress下载类型主题
  • Chrome离线版下载版,Chrome离线版安装文件,Chrome离线包
  • 上饶网站建设多少钱分销网站有哪些
  • 阿里云 Qwen 模型的 OpenAI SDK 调用
  • 什么是提示词(Prompt),提示词类型、结构解析
  • MES系列-制造流程数字化的实现
  • 我想在网站上卖食品怎么做百度知道网址
  • 对于使用队列实现栈以及用栈实现队列的题目的解析
  • Spring Boot3零基础教程,事件驱动开发,设计登录成功后增加积分记录信息功能,笔记61
  • 网站开发进度表网络电话免费版
  • 两种Redis序列化对比
  • 精确优化长尾关键词以提升SEO效果的战略分析
  • 分析对手网站wordpress制作功能型网站
  • Spring AOP注解配置实战:从概念到代码实现的深度解析(含核心关键词)
  • 【图像算法 - 31】基于深度学习的太阳能板缺陷检测系统:YOLOv12 + UI界面 + 数据集实现
  • 火山方舟 Responses API 实战指南:从概念到「公司尽调 Dossier 生成器」
  • 【推荐系统3】向量召回:i2i召回、u2i召回
  • 网站建设及系统开发wordpress仿微信菜单栏
  • 网站死链接怎么处理网页版浏览器怎么设置
  • 【仿RabbitMQ的发布订阅式消息队列】--- 介绍