← 返回题目列表

数组压缩时稳定和非稳定有什么区别?删除元素时怎么选择策略?

中等 第 23 / 30 题 更新于 2026/07/30
数组原地删除稳定性双指针

简化版

稳定压缩会保留剩余元素的相对顺序,常用读写双指针覆盖;非稳定压缩不保证顺序,可以用末尾元素交换填坑,删除更快但会打乱数组。

详细版

数组删除元素时,核心问题是是否必须保持顺序。

  • 稳定压缩:保留相对顺序,适合列表、日志、用户可见数据。
  • 非稳定压缩:不保序,适合集合、候选池、内部缓存。
  • 稳定删除通常 O(n),因为要扫描并覆盖。
  • 非稳定删除单个位置可 O(1),用最后一个元素填到被删位置。
  • 选择策略要看业务是否依赖顺序,而不是只看速度。

完整版教学

一、为什么数组删除会牵涉稳定性

数组是连续存储,中间删除一个元素后会留下空洞。最朴素做法是把后面的元素都往前搬一格,这样顺序不变,但搬动成本高。另一种做法是把最后一个元素搬到空洞位置,数组长度减一,这样很快,但顺序变了。稳定性讨论的就是:删除后剩余元素的相对顺序要不要保持。

原数组: [A, B, C, D, E]
删除 C,稳定:   [A, B, D, E]
删除 C,非稳定: [A, B, E, D]

记忆钩子:稳定压缩要“排队秩序”,非稳定压缩只要“坑被填上”。

二、稳定压缩怎么做

稳定压缩常用读写双指针。读指针从左到右扫描所有元素,遇到要保留的元素就写到 write 位置,然后 write++。因为扫描顺序就是原数组顺序,所以写出的元素自然保持相对顺序。这个方法只覆盖有效前缀,不需要为每次删除单独搬移后缀,适合一次性删除多个元素。

let write = 0;
for (let read = 0; read < arr.length; read++) {
  if (arr[read] !== target) {
    arr[write++] = arr[read];
  }
}
arr.length = write;

三、非稳定删除怎么做

如果顺序不重要,删除某个下标 i 时可以直接把最后一个元素放到 i,然后缩短数组。这样不用移动 i 后面的所有元素,单次删除是 O(1)。很多游戏实体池、任务候选集合、随机集合都会用这种方式,因为它们只关心元素是否存在,不关心顺序。代价是遍历时要小心,被换过来的元素还没处理。

function removeAtUnstable(arr, i) {
  arr[i] = arr[arr.length - 1];
  arr.pop();
}

四、带数字比较成本

长度 100 万的数组,如果删除第 10 个元素并保持顺序,后面约 999990 个元素都要前移。非稳定删除只做一次赋值和一次缩短。差距巨大。但如果用户界面列表顺序不能乱,非稳定删除再快也不能用。性能优化不能破坏语义,这是面试里很重要的判断。

删除方式是否保序单次删除成本
后缀整体前移O(n)
双指针批量压缩O(n) 总扫描
末尾填坑O(1)

五、批量删除时为什么双指针更好

如果要删除所有满足条件的元素,逐个删除并搬移会重复移动同一批元素,最坏可能 O(n²)。双指针一次扫描把要保留的元素压到前面,总成本 O(n)。这就是为什么很多数组原地删除题都推荐“读写指针”,而不是遇到一个删一个。它既保序,又避免重复搬移。

逐个稳定删除:每删一次搬一次后缀
双指针压缩:每个元素最多读一次、写一次

六、删除时还要考虑引用释放

在一些语言中,数组缩短或覆盖后,尾部旧引用如果仍然存在,可能影响垃圾回收。比如 Java 的 ArrayList 删除元素后,会把不再使用的位置置为 null,帮助 GC 回收对象。底层数组操作不只是算法复杂度,还涉及内存管理。面试如果聊到工程实现,可以补这个细节。

有效区: [A, B, D, E]
旧尾部: [E] 如果仍被引用,可能影响对象回收

七、常见误区与追问

  • 误区:数组删除一定要保持顺序。 是否保序取决于业务语义,内部集合可以非稳定删除。
  • 误区:双指针删除单个元素总是最优。 单个元素且不保序时,末尾填坑可以 O(1)。
  • 误区:非稳定删除不会影响遍历。 换到当前位置的新元素可能还没处理,循环下标要谨慎。
  • 追问:为什么批量删除不用逐个 remove? 逐个 remove 会重复搬移,双指针一次扫描更稳。
  • 追问:稳定压缩为什么保序? 因为读指针按原顺序扫描,保留元素按遇到顺序写入。

八、加强记忆

数组压缩的选择可以记成“要秩序还是要速度”。要保留顺序,就用稳定压缩和读写双指针;不要顺序,就用末尾元素填坑。面试回答时不要只背代码,要先问顺序是否有语义,再给复杂度和遍历注意点,这样才像真正做工程决策。