← 返回题目列表

块状链表是什么?它如何在数组和链表之间做折中?

困难 第 27 / 28 题 更新于 2026/07/30
链表块状链表数组局部性

简化版

块状链表是每个链表节点里存一小段数组,而不是只存一个元素。它用块内连续数组改善缓存局部性,又用块之间链表连接降低大规模插入删除的搬移成本,是数组和链表之间的折中结构。

详细版

普通链表每个节点一个元素,指针开销高、缓存局部性差。块状链表把多个元素放进一个块:

  • 块内部是数组,顺序访问快。
  • 块之间用指针连接,插入删除只影响局部块。
  • 块满时可以分裂,块太空时可以合并或借元素。
  • 常用于文本编辑、序列维护、需要折中随机访问和插入删除的场景。

它不是为了替代所有数组或链表,而是在“元素很多、局部编辑多、又希望减少指针开销”时提供折中。

完整版教学

一、普通链表和数组各自的痛点

数组连续存储,遍历和随机访问很快,但中间插入删除可能要搬移大量元素。链表插入删除可以只改指针,但每个节点一个元素,指针多、分配多、缓存局部性差。

块状链表尝试折中:

[1,2,3,4] -> [5,6,7,8] -> [9,10]

每个节点不是一个值,而是一小段数组。块内享受数组连续性,块间享受链表可拼接性。

二、块大小为什么重要

块太小,就退化得接近普通链表,指针开销和跳转仍然多。块太大,中间插入删除时块内搬移成本又接近数组。

假设块容量是 64。插入一个元素时,通常只需要在某个块内移动最多几十个元素;如果块满了,就把一半元素分裂到新块。

插入前:[1,2,3,4] -> [8,9]
在第一个块插入 5,若容量允许:
[1,2,3,4,5] -> [8,9]

块大小常根据缓存行、元素大小、操作模式调参。它不是固定公式,而是性能工程里的折中参数。

三、插入时如何分裂块

当目标块已满时,可以分裂成两个块。例如容量 4 的块 [1,2,3,4] 要插入 5,可以先分裂:

[1,2] -> [3,4]

再把 5 插入合适的块:

[1,2] -> [3,4,5]

这样一次插入不会导致整条序列都移动,只影响一个或两个块。复杂度通常和块大小有关,而不是和总元素数完全绑定。当然,如果还要按下标定位块,可能需要额外索引或从头累计长度。

四、删除时为什么可能需要合并块

删除元素后,某些块可能变得很空。如果长期不处理,会出现大量半空块,浪费内存并增加链表跳转。

常见策略:

情况处理方式
块仍较满只在块内删除
块低于阈值向相邻块借元素
两个相邻块都较空合并成一个块
删除后块为空移除该块

这和 B 树节点分裂合并有一点相似:都在维护“每个节点不要太满也不要太空”的平衡。

五、它为什么改善缓存局部性

普通链表每访问一个元素都可能跳到另一个内存位置。块状链表每进入一个块,可以连续访问块内多个元素。

如果一个块有 32 个整数,那么遍历这 32 个值时接近数组访问;只有跨块时才发生指针跳转。相比每个元素一个节点,跳转次数大幅减少。

普通链表:跳 1000 次访问 1000 个元素
块状链表:块大小 50 时,大约跳 20 次

这就是它在大量顺序扫描和局部编辑混合场景下有意义的原因。

六、适用场景和限制

块状链表适合长序列的局部插入删除、文本缓冲、编辑器内部结构、需要减少单节点链表开销的场景。它不适合极端随机访问,因为按下标找块仍然可能需要遍历块列表,除非再加树状索引。

记忆钩子:块状链表是“链表节点里装数组”,用块内连续性换缓存,用块间指针换局部修改。

七、常见误区与追问

  • 误区:块状链表随机访问一定是 O(1)。 只有块内下标是 O(1),找到第几个块可能仍要遍历或依赖额外索引。
  • 误区:块越大越好。 块太大会增加块内搬移成本,接近数组中间插入。
  • 误区:块越小越灵活。 块太小会退化成普通链表,指针和缓存问题又回来。
  • 追问:它和普通链表最大区别是什么? 一个节点存多个元素,减少指针数量并改善局部性。
  • 追问:删除后为什么要合并块? 防止大量低利用率块造成内存浪费和遍历跳转增加。

八、加强记忆

块状链表可以记成“数组和链表握手”。它不追求单点最优,而是让块内像数组、块间像链表。回答时围绕三个词讲:块大小、分裂合并、缓存局部性,就能把这个低频结构讲得很清楚。