LeetCode第26题“删除有序数组中的重复项”是我见过最典型的“简单题困难写”的例子。做这道题之前,先回答我一个问题:如果面试官给你 [1,1,2],要你在不新建数组的前提下原地去重,你能一次写对吗?我见过不少人最后返回了长度 2,但数组还是 [1,1,2] 一点没变——他们只理解了返回值,没理解“数组前 k 位也要被修改”。
这道题很适合收进刷题日记里反复看:它看起来只考一个数组去重,实际上考的是对“有序”这个前提条件的敏感度,以及双指针到底在维护什么。哪怕面试时没有直接考原题,面试官也很容易把难度往“最多保留两个重复项”“合并两个有序数组”“链表去重”这些方向延伸。所以我这篇笔记不准备只给一段能 AC 的代码,而是想把为什么要用双指针、代码里每一行在干什么、哪些细节最容易写错,一次讲透。
1. 把题目读透:“有序”和“原地删除”并不是修饰词
很多人拿到这道题就直接背双指针模板,背完也不理解为什么自己用 HashSet 会超空间。我建议先把题干里的限定条件拆开看,因为面试官后面追问的所有变体,本质上都在这些限定条件上做文章。
1.1 “有序数组”隐藏的语义
题目给的是“有序数组”,这是全题最大的礼物。数组有序意味着所有相同的值只会连续出现,不会出现 [1, 2, 1, 1] 这种交错情况。每当扫描到数组中的某个值,它要么和当前已经保留的最后一个值相等,那这一步就是重复;要么比它大,那就是一个新值。
这一点直接决定了我们不需要额外的哈希表来记住“历史上出现过哪些值”。如果数组无序,比如 [3,1,2,1,3],想不借用额外空间做去重,那就只能对每个元素往前找一遍,时间复杂度会退化成 O(n^2),或者你先排序再继续走——都会破坏“保持相对顺序”的要求。所以面试官一旦听到“有序”,你就应该立刻往指针扫描的方向想。这也是为什么那些“看起来会用双指针的人”代码不一定写得对,但“为什么用双指针”一定要能答出来的人更容易过关。
还有一个容易被忽略的语义:题目要求数组的“相对顺序”保持一致。不能因为某个数重复了,就把它取出来放到数组尾部去“凑数”,那样会打乱顺序。所以正确做法必须是把不重复的值稳定地往数组前面搬。
1.2 “原地删除”背后的空间契约
数组的物理长度在 Java 里是固定的,你不可能像一个真正
