数据结构——数组

在数据结构的世界里,数组(Array)是最基础、最古老、也是应用最广泛的结构之一。它就像一个整齐划一的公寓楼------每个房间编号连续,大小相同,通过门牌号(索引)可以瞬间找到任何一间房间。正是这种连续内存随机访问的特性,使数组成为几乎所有高级数据结构的底层构建模块。

本文将从数组的定义、内存模型、基本操作、动态数组的实现、多维数组、复杂度分析以及典型应用等方面,带你全面理解数组。


一、什么是数组?

数组是一种线性数据结构 ,它用一组连续的内存空间存储相同类型的数据元素,并通过索引(下标)来访问每个元素。在大多数编程语言中,数组的索引从 0 开始。

数组的核心特点可以概括为:

  • 连续内存:所有元素在内存中紧密排列,没有空隙。

  • 固定类型:数组中的元素通常具有相同的数据类型(在静态类型语言中强制,在动态语言如 Python 中虽可混合但底层仍是同质)。

  • 随机访问:通过索引可以在 O(1) 时间内直接访问任意元素。

  • 静态或动态:有的语言提供静态数组(长度固定),有的提供动态数组(可自动扩容)。

二、数组的内存模型

理解数组的关键是理解它的内存布局。假设我们声明一个整型数组 int arr[5],内存模型如下:

地址 元素
0x1000 arr[0]
0x1004 arr[1]
0x1008 arr[2]
0x100C arr[3]
0x1010 arr[4]

由于每个 int 占 4 字节(在大多数系统上),相邻元素的内存地址相差 4 字节。当我们写 arr[i] 时,编译器会计算实际地址:base_address + i * element_size,从而直接定位到目标元素,无需遍历。

这种"公式计算"就是数组 O(1) 随机访问的本质。

三、数组的基本操作

数组支持的操作相对简单,但不同操作的时间复杂度差异巨大。

1. 访问(Access)

给定索引 i,直接获取元素值。时间复杂度 O(1)

python 复制代码
# Python 列表作为动态数组
arr = [10, 20, 30, 40, 50]
element = arr[2]   # 30

2. 搜索(Search)

查找某个值是否存在,通常需要遍历数组。时间复杂度 O(n)(无序情况下)。

python 复制代码
def search(arr, target):
    for i, val in enumerate(arr):
        if val == target:
            return i
    return -1

3. 插入(Insert)

  • 末尾插入:通常 O(1)(动态数组需要考虑扩容)。

  • 中间/开头插入:需要移动后续元素,平均 O(n)。

python 复制代码
# Python 列表的 insert 操作
arr = [1, 2, 3, 4, 5]
arr.insert(2, 99)   # 在索引 2 处插入 99,后面的元素右移
print(arr)          # [1, 2, 99, 3, 4, 5]

4. 删除(Delete)

  • 删除末尾元素:O(1)。

  • 删除中间/开头元素:需要移动后续元素填补空缺,平均 O(n)。

python 复制代码
# 删除索引 2 的元素
del arr[2]          # 后面元素左移

5. 更新(Update)

通过索引赋值,O(1)。

python 复制代码
arr[2] = 100

四、数组的实现:静态数组与动态数组

1. 静态数组(Static Array)

长度在声明时确定,之后不可改变。C 语言中的原生数组是典型代表。

cs 复制代码
int arr[10];   // 固定容量为 10

优点:内存分配一次完成,无扩容开销。

缺点:容量固定,可能浪费空间或不够用。

2. 动态数组(Dynamic Array)

可以自动扩容的数组,在 Python 中就是 list,在 Java 中是 ArrayList,在 C++ 中是 vector

动态数组的实现原理:

  • 底层使用静态数组存储元素。

  • 维护一个容量(capacity)和一个实际大小(size)。

  • 当 size 达到 capacity 时,创建一块更大的新数组(通常扩容为原容量的 1.5 倍或 2 倍),复制旧元素,然后释放旧数组。

Python 列表的简化实现示例:

python 复制代码
class DynamicArray:
    def __init__(self, capacity=10):
        self._data = [None] * capacity
        self._capacity = capacity
        self._size = 0

    def append(self, item):
        if self._size == self._capacity:
            self._resize(2 * self._capacity)   # 扩容为 2 倍
        self._data[self._size] = item
        self._size += 1

    def insert(self, index, item):
        if self._size == self._capacity:
            self._resize(2 * self._capacity)
        # 将 index 及之后的元素右移
        for i in range(self._size, index, -1):
            self._data[i] = self._data[i-1]
        self._data[index] = item
        self._size += 1

    def pop(self):
        if self._size == 0:
            raise IndexError("pop from empty array")
        item = self._data[self._size - 1]
        self._size -= 1
        # 可选:缩容,避免浪费过多空间
        if self._size < self._capacity // 4:
            self._resize(self._capacity // 2)
        return item

    def _resize(self, new_capacity):
        new_data = [None] * new_capacity
        for i in range(self._size):
            new_data[i] = self._data[i]
        self._data = new_data
        self._capacity = new_capacity

    def __getitem__(self, index):
        if index < 0 or index >= self._size:
            raise IndexError("index out of range")
        return self._data[index]

    def __setitem__(self, index, value):
        if index < 0 or index >= self._size:
            raise IndexError("index out of range")
        self._data[index] = value

    def __len__(self):
        return self._size

扩容策略:通常采用倍数扩容(如 2 倍),使得均摊时间复杂度为 O(1)。如果每次只增加固定大小,会导致频繁扩容,性能下降。

五、多维数组

数组可以有多维形式,最常见的是二维数组(矩阵)。在内存中,多维数组通常以行优先 (Row-major)或列优先(Column-major)的方式存储。

python 复制代码
# Python 中使用嵌套列表表示二维数组
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

# 访问第二行第三列(索引从0开始)
element = matrix[1][2]   # 6

二维数组的随机访问依然 O(1):matrix[i][j] 实际地址 = base + (i * cols + j) * elem_size(行优先)。

多维数组在图像处理、科学计算、机器学习等领域应用广泛。

六、数组与链表的对比

数组和链表是两种最基础的线性数据结构,各有优劣。

特性 数组 链表
内存分配 连续内存,静态或动态扩容 非连续内存,节点分散
随机访问 O(1) O(n)
插入/删除(头部/中间) O(n)(需移动元素) O(1)(给定节点位置)
插入/删除(尾部) 均摊 O(1)(动态数组) O(1)(有尾指针)
内存开销 低(仅元素本身) 高(额外存储指针)
缓存友好 非常友好(连续内存) 不友好(节点可能不连续)

选择数组还是链表,取决于对随机访问、插入删除频率的要求。对于大量随机访问的场景,数组是首选;对于频繁在中间插入删除的场景,链表可能更合适。

七、数组的复杂度总结

操作 静态数组 动态数组(均摊)
随机访问 O(1) O(1)
末尾插入 不支持 O(1)
中间插入 O(n) O(n)
头部插入 O(n) O(n)
删除末尾 不支持 O(1)
删除中间 O(n) O(n)
搜索 O(n) O(n)

*动态数组的末尾插入均摊 O(1) 依赖于扩容时的复制操作平摊到各次插入中。

八、数组的经典应用

数组作为数据结构之母,应用无处不在:

  1. 存储同类型数据集合:例如学生成绩、温度记录、图像像素。

  2. 实现其他数据结构:栈、队列、哈希表(开放寻址法)、堆(完全二叉树)等底层常用数组。

  3. 矩阵运算:在科学计算、机器学习中,矩阵用二维数组表示,进行加减乘除、转置等操作。

  4. 字符串处理:字符串本质上就是字符数组。

  5. 缓存与缓冲区:如音频缓冲区、环形缓冲区(循环数组)用于流式数据处理。

  6. 排序与查找算法:几乎所有的排序(快排、归并等)和查找(二分查找)都基于数组。

九、数组的局限与优化

  • 插入删除效率低:因为需要移动大量元素。如果这类操作频繁,考虑使用链表或平衡树。

  • 固定类型(静态语言):限制了灵活性,但提高了安全性和性能。

  • 空间预分配:动态数组扩容可能导致短暂的内存复制开销,但可以通过合理的扩容策略(如 1.5 倍)来平衡。

在现代编程中,Python 的 list、NumPy 的 ndarray(提供了更高效的数值计算)都是数组的进化形态,开发者可以根据需要选择。

十、总结

数组是一种简单而强大的数据结构,它凭借连续内存和随机访问的特性,成为编程中最基础的抽象之一。通过本文,你应该掌握了:

  • 数组的本质:连续内存 + 同类型元素 + 索引访问。

  • 基本操作及其时间复杂度:访问 O(1)、搜索 O(n)、插入删除 O(n)(末尾 O(1))。

  • 静态数组与动态数组的区别与实现。

  • 多维数组的存储方式。

  • 数组与链表的对比及适用场景。

  • 数组在计算机科学中的广泛应用。

数组虽小,却承载着程序设计的核心思想。无论是初学者还是资深开发者,深入理解数组,都能在算法设计、性能优化中游刃有余。

相关推荐
左左右右左右摇晃1 小时前
数据结构——队列
数据结构
nainaire2 小时前
速通LeetCode hot100——(1~9 哈希,双指针,滑动窗口)
c++·笔记·算法·leetcode
2501_924952692 小时前
分布式缓存一致性
开发语言·c++·算法
hmbbcsm2 小时前
动手学习深度学习学习笔记(一)
笔记·学习
春水碧于天,画船听雨眠2 小时前
jQuery学习笔记
笔记·学习·jquery
XiYang-DING2 小时前
【LeetCode】LCR 019. 验证回文串 II
算法·leetcode·职场和发展
灰色小旋风2 小时前
力扣18 四数之和(C++)
数据结构·算法·leetcode
噜啦噜啦嘞好2 小时前
算法篇:前缀和
数据结构·算法