V

算法可视化

ZHEN
分享到微博分享到 XGitHub 仓库个人主页
学习工具算法复杂度速查算法学习路径
数据结构数组链表栈队列树堆哈希表图字典树并查集LRU 缓存跳表线段树B+ 树布隆过滤器树状数组
经典排序算法冒泡排序鸡尾酒排序双调排序选择排序插入排序二分插入排序希尔排序归并排序自顶向下归并快速排序三路快排双轴快排堆排序计数排序基数排序桶排序
图算法Dijkstra 最短路Kruskal 最小生成树Prim 最小生成树Bellman-Ford 最短路拓扑排序Floyd 多源最短路强连通分量2-SAT最大流二分图匹配LCA 倍增欧拉路径
动态规划编辑距离0-1 背包完全背包最长公共子序列最长递增子序列硬币找零方案数石子合并旅行商 TSP树形 DP数位 DP换根 DP
回溯与搜索N 皇后子集生成全排列组合总和迷宫寻路岛屿数量单词搜索数独A* 寻路
字符串KMP 字符串匹配Rabin-KarpBoyer-MooreManacher后缀数组LCP / height 数组AC 自动机Z 函数
数学与数论埃氏筛线性筛欧几里得算法快速幂扩展欧几里得中国剩余定理欧拉函数米勒-拉宾FFTPollard's Rho
计算几何凸包旋转卡壳最近点对线段相交扫描线求交
查找二分查找二分边界旋转数组搜索二分答案三分查找

数组 Array

数据结构 · 连续内存里的带下标序列

什么是数组

数组是一段连续的内存,里面挨个排着同类型的元素。每个元素都有一个从 0 开始的下标(index)。要取第 i 个元素,直接按下标一步到位、不用从头挨个找——这叫随机访问,时间复杂度 O(1)。

但代价藏在改动里:因为元素必须保持连续,往中间插入一个,下标 i 往后的元素就得整体右移一格腾位;删除一个,后面的又得左移补位。这两步都是 O(n)。下面亲手试试——点一个格子选中它的下标,再用工具栏操作。

亲手试试
1
2
3
4
↑0
↑1
↑2
↑3

点一个格子选中下标,再用上面的按钮操作

点中下标 2 按「访问」,它瞬间高亮,不管数组多长都一样快。再按「在 2 处插入」,看下标 2 起的元素怎么排队右移给新元素让位;删除则反过来左移补位;而「尾部追加」放在末尾,谁也不用动, O(1)。插几次后你会发现:下标和值不再相等——下标是位置,值是内容,本就是两码事。

动态数组:容量满了怎么办——翻倍扩容

上面这个数组容量是固定的。可现实里我们天天用的「数组」——JS 的 Array、C++ 的 vector、Java 的 ArrayList、Python 的 list——却能一直 append 下去,靠的就是动态扩容。

它的办法是:当容量正好装满、还要再放,就开一个2 倍大的新数组,把旧元素逐个拷过去(O(n)),再放新元素。拷贝很贵,但只在「装满」那一刻才发生。一直点「追加」试试。

亲手试试
1
2
3

长度 3 / 容量 4

append 0 次 · 总拷贝 0 · 均摊 0.0 次/append(≈ O(1))

容量满了会怎样?一直点「追加」看看。

关键全在「翻倍」上:容量 4 → 8 → 16 → … 指数增长,扩容越来越稀疏。把每次扩容的拷贝成本摊到它之后那一大批 append 上,平均下来每次 append 只花常数次操作——这就是「尾部追加均摊 O(1)」的摊还分析。看右边那个均摊读数,无论追加多少次它始终有界,不会越长越慢。

数组在哪里用

几乎无处不在,凡是「按编号快速取用、整体顺序存放」的场景都靠它:

查表 / 缓冲区:按下标 O(1) 直取,最适合频繁随机读。
矩阵 / 图像像素:二维数组就是「数组的数组」。
其它结构的底座:栈、队列、堆、哈希表的底层多半都是一段数组。

数组胜在随机访问、败在中间增删要搬移。下一篇讲链表——它把元素用指针串起来,增删只改指针、不搬移,正好和数组互为镜像。