资讯动态

Hello 算法:数组与链表章节小结——存储方式、操作效率与缓存友好的选型决策

发布时间:2026/9/7 3:50:07 来源:尧图企业网站定制
Hello 算法数组与链表章节小结——存储方式、操作效率与缓存友好的选型决策【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo本篇基于《Hello 算法》数组和链表一章的小结文档系统梳理数组、链表、列表三种数据结构的存储特性与操作效率差异并结合仓库中的 Python 参考实现MyList动态数组、链表节点操作逐条深入讲解本章 11 个高频 QA。读完后你将能够准确解释为什么数组通常比链表缓存命中率更高并掌握列表扩容机制、Python 引用语义等底层细节为后续章节中栈、队列、哈希表等结构的选择打下基础。核心结论回顾连续存储与分散存储的互补性本章小结summary.md给出的重点可以归纳为四条主线它们分别对应章内四篇正文数组、链表、列表与内存与缓存。两种基本存储范式数组和链表代表数据在内存中的两种存储方式——连续空间存储与分散空间存储两者的特点呈互补关系。数组的强与弱支持随机访问、占用内存较少但插入和删除效率低且初始化后长度不可变。链表的强与弱通过更改引用指针实现高效的节点插入与删除可灵活调整长度但节点访问效率低、占用内存较多。常见类型包括单向链表、环形链表、双向链表。列表是数组的实用化增强列表是一种支持增删查改的元素有序集合通常基于动态数组实现保留了数组的随机访问优势且长度可变但可能导致部分内存空间浪费。缓存视角的最终裁决缓存通过缓存行、预取机制以及空间局部性和时间局部性等数据加载机制为 CPU 提供快速数据访问。由于数组具有更高的缓存命中率它通常比链表更高效——选择数据结构时应根据具体场景决定而非一刀切。仓库中的 Python 实现完整印证了这些结论。array.py 中的random_access、insert、remove、find展示了数组的随机访问与 O(n) 移位的插入删除而 linked_list.py 中的insert与remove则体现了链表只需修改引用的 O(1) 特性# 来源codes/python/chapter_array_and_linkedlist/linked_list.py#L14-L28 def insert(n0: ListNode, P: ListNode): 在链表的节点 n0 之后插入节点 P n1 n0.next P.next n1 n0.next P def remove(n0: ListNode): 删除链表的节点 n0 之后的首个节点 if not n0.next: return # n0 - P - n1 P n0.next n1 P.next n0.next n1对照之下array.py 中数组的插入需要把索引 index 以及之后的所有元素向后移动一位删除则需向前移位——这正是两者效率差异的源码级证据。列表List用动态数组弥补数组长度不可变的短板列表保留了数组的随机访问能力同时可以灵活调整长度。其实现关键在于容量与长度分离的设计。仓库中的 MyList 类 给出了最小可用实现# 来源codes/python/chapter_array_and_linkedlist/my_list.py#L11-L16 def __init__(self): 构造方法 self._capacity: int 10 # 列表容量 self._arr: list[int] [0] * self._capacity # 数组存储列表元素 self._size: int 0 # 列表长度当前元素数量 self._extend_ratio: int 2 # 每次列表扩容的倍数其中三个参数的含义直接回答了小结中的两个 QA初始容量_capacity 10对应列表都会设定一个初始长度我们不一定需要用这么多的空间浪费来源扩容倍数_extend_ratio 2对应为了防止频繁扩容扩容一般会乘以一个系数代价是扩容后出现无法完全填满的空位。扩容逻辑在 extend_capacity 中申请一块原容量 2 倍的新数组并整体复制这是一次 O(n) 操作def extend_capacity(self): 列表扩容 # 新建一个长度为原数组 _extend_ratio 倍的新数组并将原数组复制到新数组 self._arr self._arr [0] * self.capacity() * (self._extend_ratio - 1) # 更新列表容量 self._capacity len(self._arr)add与insert在size capacity时都会触发该扩容见 my_list.py#L39-L59。这与 array.py 的 extend 函数 手动扩容的思路一致新建更长的数组逐个复制旧元素。区别在于MyList把这一过程封装成了透明的自动机制。内存与缓存效率为什么数组通常更快小结中两条关于内存的结论需要分别理解内存空间效率方面数组元素紧密排列、无须为节点间的引用指针分配额外空间因此空间利用率更高链表以节点为单位动态分配与回收内存灵活但每个节点都携带额外指针开销——从 modules/list_node.py 的ListNode定义看每个节点除val外还必须保存next引用。另外反复申请与释放内存会加剧碎片化数组的连续分配方式相对不易产生碎片。缓存效率方面关键机制有四个缓存行缓存以行为单位批量加载数据而非逐字节传输预取机制处理器预测访问模式顺序、固定步长跳跃等并提前加载空间局部性被访问数据的邻居近期很可能被访问时间局部性被访问数据不久的将来很可能再次被访问。这四条机制几乎全部偏向数组链表数据分散在内存各处按行加载时命中无效数据的比例更高且访问模式难以预测。因此数组的缓存命中率更高操作效率通常优于链表这也是算法题中优先用数组的底层原因。但小结也特别强调高缓存效率不等于数组永远更优——当数据量极大、动态性很高、规模难以估计时链表实现例如链表版栈反而能避免扩容开销。高频问题 QA 深度解析以下逐条展开小结中的 11 个问题这是本章最具检索价值的部分。Q1数组放在栈上还是堆上对效率有影响吗栈上和堆上的数组都存储在连续内存空间内数据操作效率基本一致差异来自内存区域本身的特性分配与释放效率栈是一块较小的内存分配由编译器自动完成堆内存更大、可在代码中动态分配但更容易碎片化因此堆上的分配与释放通常比栈上慢。大小限制栈内存相对较小堆的大小一般受限于可用内存因此堆更适合存储大型数组。灵活性栈上数组的大小需要在编译时确定堆上数组的大小可以在运行时动态确定。仓库中 array.cpp 的注释也展示了这一区分int arr[5]存储在栈上而int* arr1 new int[5]分配在堆上且需要手动释放。Q2为什么数组要求元素同类型链表却不强调链表由节点组成节点之间通过引用连接各节点可以存储不同类型的数据int、double、string、object等。而数组元素必须同类型因为随机访问依赖下面的偏移量公式# 元素内存地址 数组内存地址首元素内存地址 元素长度 * 元素索引如果数组中混有int4 字节和long8 字节两种类型就存在两种元素长度该公式无法成立。这正是数组连续空间 定长元素先验信息能换来 O(1) 随机访问的代价。Q3删除节点 P 后需要把 P.next 设为 None 吗不修改P.next也可以。删除完成后从头节点遍历到尾节点已经遇不到PP已从链表中脱离它指向哪里都不会影响该链表。从算法做题角度看不断开没有关系只要程序逻辑正确即可从标准库角度看断开更安全、逻辑更清晰——如果被删除节点未被正常回收它持有的引用会阻碍后继节点的内存回收。对照 linked_list.py 的 remove 实现删除后并未修改P.next而局部变量P在函数结束后失去引用P即可被回收恰好说明断开与否取决于语言与回收机制。Q4链表的增删是 O(1)为什么查找 删除却是 O(n)先查找元素、再删除元素总时间复杂度确实是 O(n)。但链表 O(1) 增删的优势在能维护端点指针的场景中体现例如双向队列用链表实现维护始终指向头节点、尾节点的指针变量每次端点插入与删除都是 O(1)。O(1) 的标称复杂度描述的是已知目标位置引用时的操作代价。Q5示意图中节点的指针占一块内存地址吗示意图只是定性表示定量大小需具体分析不同类型节点值占用空间不同如int、long、double和实例对象指针引用占用的空间取决于操作系统与编译环境大多为 8 字节64 位或 4 字节32 位。Q6在列表末尾添加元素是否时时刻刻都是 O(1)不是。当元素数量超出列表容量时需要先扩容再添加系统申请一块新内存并把原列表所有元素搬运过去这一次操作是 O(n)。从 MyList.add 的源码看扩容仅在self.size() self.capacity()的瞬间触发。因此列表尾部添加更准确的描述是均摊 O(1)大部分添加是 O(1)偶发一次 O(n) 的扩容被平摊到后续一系列添加中。Q7列表的内存空间浪费指什么主要来自两方面对应 MyList 初始化 中的两个参数初始长度列表创建时会分配一个初始容量如 10 个槽位未必都用得完扩容系数为防止频繁扩容扩容通常乘以系数如 ×1.5 或 ×2扩容后的数组中会出现大量尚未填满的空位。这些空位就是部分内存空间浪费的含义——它不是某个变量本身的开销而是容量与长度之间的差值。Q8Python 列表中相同数字的地址为何可以不连续以n [1, 2, 3]和m [2, 1, 3]为例m中元素的id与n中相同数字的id一致但地址不连续。原因有两层列表存的是引用不是对象本身。即使元素换成链表节点n [n1, n2, n3, n4, n5]节点对象通常也分散存储在内存各处但只要给定索引仍可在 O(1) 内通过基地址 偏移算出存储槽位、取出节点引用——数组的连续性体现在引用槽位上而非被引用的对象上Python 的数字本身也是对象列表中存的是对数字的引用因此两个列表中的相同数字拥有同一个id且这些数字对象的内存地址无须连续。Q9C 的std::list双向链表为什么在算法中不常用从源码结构看两个原因正是小结互补性论断的实例空间开销双向链表每个元素需要两个额外指针前驱、后继std::list比std::vector更占空间缓存不友好节点分散存放缓存利用率低一般情况下std::vector性能更好。真正必须用链表的场景主要是二叉树和图的节点连接栈和队列往往直接使用语言标准库提供的stack、queue而非手动用链表实现。Q10res [[0]] * n的每个[0]是独立的吗不是独立的。该表达式生成的是一个引用了同一个[0]列表对象的 n 元素列表修改其中一个会同时改变所有行。若需要 n 个相互独立的[0]应使用列表推导式res [[0] for _ in range(n)] # 初始化 n 个独立的 [0] 列表对象其原理是每次循环都新建一个列表对象而非复制同一引用。Q11res [0] * n的每个整数 0 是独立的吗所有整数 0 指向同一个对象——Python 对小整数通常 -5 到 256采用缓存池机制以最大化对象复用。但仍可独立修改每个元素因为 Python 整数是不可变对象修改某个元素实际上是让该槽位切换到另一个对象的引用而非修改原对象。然而当元素是可变对象列表、字典、类实例等时修改某个元素会直接改变对象本身所有引用它的槽位都会产生相同变化——这与 Q10 的二维列表陷阱是同一原理的两面。小结与选型建议维度数组链表存储方式连续内存空间分散内存空间容量扩展长度不可变需整体搬迁扩容可灵活扩展内存效率元素占用内存少但可能浪费空间每元素附带指针占用更多内存访问元素O(1)O(n)添加/删除元素已知位置引用时O(n)O(1)可以据此得出三条可操作的选型准则默认偏向数组需要随机访问、批量遍历或缓存敏感时排序、二分查找、堆、邻接矩阵优先选择数组或动态数组列表实现需要 O(1) 端点增删且规模难估时选链表如大型动态缓冲、以链表实现的栈/队列此时 O(1) 的端点操作优势才能兑现警惕O(1) 宣传值列表尾部添加、链表中部增删都要叠加前置查找或偶发扩容的 O(n) 成本评估复杂度时必须把完整操作链算进去。本章的可运行参考代码均位于 codes/python/chapter_array_and_linkedlist 目录array.py、linked_list.py、list.py、my_list.py可直接执行观察各操作的输出验证上文关于容量、扩容与引用语义的论述。【免费下载链接】hello-algo《Hello 算法》动画图解、一键运行的数据结构与算法教程。支持简中、繁中、English、日本語提供 Python, Java, C, C, C#, JS, Go, Swift, Rust, Ruby, Kotlin, TS, Dart 等代码实现项目地址: https://gitcode.com/GitHub_Trending/he/hello-algo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价