跳到内容

数组

Vol 2:算法森林探险 · 数组特辑


遭遇战:林间驿站的两条岔路

你走在算法森林小径上,面前两条岔路。

木牌 A(数组路): 笔直开阔的大路,路边整齐排列 30 个树洞驿站,门牌号固定刻在石板上——找 17 号?直接走过去就行。

木牌 B(链表路): 弯弯曲曲的小路,棚屋散落,靠细绳串在一起,路线随时在变。

本章先走木牌 A。


什么是数组?

连续的内存块,像一排电影院座位。座位号就是下标。

地址: 基址  基+4  基+8  基+12  基+16
索引:  [0]   [1]   [2]   [3]   [4]
       ┌──────┬──────┬──────┬──────┬──────┐
       │  10  │  20  │  30  │  40  │  50  │
       └──────┴──────┴──────┴──────┴──────┘

静态数组

遇到问题: 想看第 17 号驿站?门牌号固定——知道在哪一格就行。

python
from array import array

static_arr = array('i', [10, 20, 30, 40, 50])
print(static_arr[2])  # 30
# 输出: 30

为什么随机访问是 O(1)? arr[k] = 基地址 + k × 元素大小。一个乘法 + 一个加法——与 n 无关。

动态数组

遇到问题: 想新增驿站?静态数组长度不能变——得另找空地搬家。

Python list、Java ArrayList、C++ std::vector 底层还是连续数组,但容量不够时自动扩容

python
arr = []
for i in range(10):
    arr.append(i)
# 扩容因子约 1.125 倍(Python 3.x)
# ArrayList 1.5 倍,std::vector 2 倍

每次扩容 O(n)(分配 + 拷贝),但均摊到单次 append 是 O(1)


复杂度表

操作静态数组动态数组
随机访问 arr[k]O(1)O(1)
末尾插入不可变O(1) / O(n) / 摊销 O(1)
中间/开头插入O(n)
按值查找O(n)O(n)
删除末尾O(1)
删除中间O(n)

插一句: 知道元素数量时,Java 用 new ArrayList<>(initialCapacity),C++ 用 reserve(n) 预分配,避免多次扩容。


常见陷阱

说法真相
"Python list 是链表"❌ 动态数组(连续内存 + O(1) 索引)
"数组插入慢,链表一定更快"❌ 找位置也是 O(n),加上缓存差异数组往往更优
"动态数组是无限的"❌ 只是伪装成无限。知道上限时一次性分配最省事

通关挑战

  1. 手写动态数组 resize:容量满时翻倍,拷贝旧元素
  2. 用数组实现环形队列(循环利用删除位置)

验收标准

  • [ ] 能画出数组连续内存布局
  • [ ] 能解释 arr[k] 为什么是 O(1)
  • [ ] 能解释"动态数组末尾插入是摊销 O(1)"
  • [ ] 能说出数组最怕什么操作(中间插入/删除)

旅人笔记

连续内存 + O(1) 随机访问 = 数据结构世界的基石。95% 实战场景,动态数组就是默认选择。


下一站预告

上一章:复杂度分析

领教了数组的直来直往,该拐进木牌 B 了——链表。没有下标,全靠指针。插入 O(1),但找第 k 个元素?一个一个数过去吧。

继续阅读:链表

Built with VitePress | Software Systems Atlas