数据结构与算法:数组的基本原理
在讲解数据结构时,数组总是一个大家早期接触的主题。但你可能会发现,不同编程语言中“数组”这一概念的使用方式和 API 有所区别。其实我们可以把「数组」分为两大类:
静态数组
静态数组在创建时就已经确定了元素的个数以及连续的内存空间。我们可以通过下标索引访问数组中存储的元素,这正是数组最原始的形式,也是其 “随机访问” 能力得以实现的根本原因。
动态数组
动态数组是在静态数组基础上,为了方便编程引入的一层封装。动态数组在内部依然使用静态数组存储数据,但会自动进行扩容和提供一些常用的增删查改 API,如 push
、insert
、remove
等。了解动态数组的底层原理,有助于我们深入理解后续实现其他数据结构(例如队列、栈、哈希表等)的核心思想。
本文将围绕静态数组的基本原理,并用 C 语言代码手把手实现简单版本的动态数组,展示其增删查改操作。
静态数组的基本使用
在 C 语言中,我们可以直接通过如下方式定义一个大小为 10 的静态数组:
1 |
|
在上面的代码中:
数组
arr
在内存中占用了连续的 10 * sizeof(int)
字节空间(通常一个 int
占 4 字节,总共 40 字节)。数组名
arr
就代表这块内存的首地址,因此 arr[0]
就是起始地址处的数据,而 arr[1]
则是偏移 4 字节后存储的数据。
这种连续内存的特性保证了数组的“随机访问”,即给定任何下标,都可以在 O(1)
时间内通过首地址和偏移量直接计算目标元素的内存位置。
数组的增删改查
对于静态数组来说,查改操作比较简单,需要给定下标直接访问数据,时间复杂度均为 O(1)
。而增删操作则需要考虑两种情况:
在数组末尾增加或删除元素
在数组中间插入或删除元素(这就需要进行数据搬移)
数组的增删改查
假设我们有一个大小为 10 的数组,前 4 个位置存储数据,现在直接在末尾追加一个元素:
1 |
|
在末尾追加数据时只需一次赋值操作,时间复杂度为 O(1)
。
在中间插入元素
如果要在数组的某个中间位置插入一个新元素,就需要先将后面的元素向后搬移,为新元素腾出空间。例如,在下标 2(第三个位置)插入新数据 666:
1 |
|
这里需要将位置 pos
之后的所有元素依次后移一位,因此时间复杂度为 O(N)
。
删除元素
删除元素同样分两种情况:
删除末尾元素时,只需要将数组长度减少一位,时间复杂度为
O(1)
。删除数组中间的元素时,需要将目标元素后面的数据前移一位,时间复杂度为
O(N)
。
删除末尾元素示例
1 |
|
删除中间元素示例
例如删除下标 1(第二个元素):
1 |
|
数组的扩容思想
静态数组的最大问题在于:在创建数组时就必须确定内存大小,一旦数组满了,无法直接在连续内存后面追加更多数据。我们只能重新开辟一块更大的内存,将原有数据复制到新数组中,然后插入新元素。这就是“扩容”操作,其时间复杂度为 O(N)
。
以下示例展示如何扩容一个静态数组:
1 |
|
在实际开发中,动态数组的扩容不会在每次添加元素时都发生,而是采用均摊时间复杂度(摊还分析)来评估其操作效率。正因为这种策略,使得在尾部追加元素的均摊时间复杂度依然是 O(1)
。
动态数组的代码实现与关键点
动态数组是在静态数组基础上进行封装,以便更方便地进行增删查改操作。动态数组解决了静态数组大小固定的问题,最主要的改进包括:
自动扩缩容:当数组元素个数达到容量上限时,扩容为原来的 2 倍;当元素个数减少到容量的 1/4 时,可以缩容为原来的 1/2。
索引越界检查:在插入、查找和修改时分别进行检查,保证操作合法。
防止内存泄漏:删除元素时清除相关数据,确保不再使用的数据能被正确释放。
下面我们通过一个完整的 C 语言示例来实现一个只存储整数的动态数组,该示例包含了自动扩容和缩容、插入、删除、查找以及修改等功能。
1 |
|
代码说明
- 自动扩缩容
在添加元素时,如果
size == capacity
,调用 resize
扩容为原来的 2 倍。删除元素后,如果
size == capacity / 4(且容量不小于 1)
,进行缩容以节约内存。
- 索引越界检查
使用
checkElementIndex
对已有元素的索引进行检查,确保查找、修改和删除操作在合法范围内。使用
checkPositionIndex
检查插入操作时允许 index == size
的情况,从而支持在末尾插入新元素。
- 防止内存泄漏
- 删除元素后,将对应位置的数据置为
0
(如果存放的是指针类型,则应调用适当的释放函数),以防止出现悬挂引用的问题。
- 其他细节优化
- 数据搬移在示例中采用
for
循环实现,这有助于理解底层算法本质;但在生产环境中,可以考虑使用更高效的内存复制函数。
总结
本文融合了静态数组与动态数组的相关内容,主要介绍了:
静态数组的内存连续性和基本增删改查操作,其中在中间插入和删除元素需要搬移数据。
动态数组在静态数组之上封装了自动扩缩容、索引越界检查与内存清理等功能,使我们在实际开发中能够更灵活、更安全地操作数组。
通过 C 语言完整示例展示了动态数组的实现细节,从而帮助大家深入理解其背后的工作原理和设计思路。
希望这篇博客能够帮助你全面理解数组的基本原理及动态数组的实现方法,并为你后续学习队列、栈、哈希表等更复杂的数据结构打下坚实基础!