云计算百科
云计算领域专业知识百科平台

DAY10: LeetCode 26 + 80|删除有序数组重复项:从 `i - 1` 一步推到 `i - k`

LeetCode 26 + 80|删除有序数组重复项:从 i – 1 一步推到 i – k

这两道题放在一起看会比较有意思。

LeetCode 26 要求:每个数字最多保留 1 个 LeetCode 80 则变成:每个数字最多保留 2 个

一开始我以为只是把“去重”稍微改一下,但真正写到第二题时才发现:

如果还按照第一题的方式和前一个数字比较,第二个重复值也会被误删。

最后把两题放在一起,反而可以得到一个很统一的规律:

最多保留 1 个 → 看 i – 1
最多保留 2 个 → 看 i – 2
最多保留 k 个 → 看 i – k

这篇主要记录这个思路是怎么一步一步推出来的。

LeetCode 26:LeetCode 26

LeetCode 80:LeetCode 80

文章目录

  • LeetCode 26 + 80|删除有序数组重复项:从 `i – 1` 一步推到 `i – k`
  • 一、这两道题为什么可以用双指针?
  • 二、先统一两个指针的含义
  • 三、LeetCode 26:每个数字最多保留 1 个
    • 第一步:第一个数字一定可以保留
    • 怎么判断 nums[j] 要不要保留?
    • 用一个例子走一遍
    • LeetCode 26 完整代码
  • 四、来到 LeetCode 80:现在允许出现两次
    • 那怎么判断它会不会成为第 3 个?
    • 如果真的已经有两个相同数字呢?
    • 用完整例子走一遍
    • LeetCode 80 完整代码
  • 五、把 26 和 80 放在一起看
    • LeetCode 26
    • LeetCode 80
    • 通用的 `i – k` 模板
    • 这个方法成立 -> 有序是关键
  • 六、时间复杂度和空间复杂度

一、这两道题为什么可以用双指针?

两道题都有两个很重要的条件:

1. 数组已经按照递增顺序排列
26:非严格递增
80:有序数组
2. 要求原地修改数组

先看:递增

意思就是:

从小到大排列

但允许相等 –> 非严格

例如:

[0, 0, 0, 1, 1, 2, 3, 3]

因为数组已经有序,所以相同数字一定会挨在一起。

这就给了我们一个很重要的条件:

只需要一路向后扫描,就可以判断当前数字是不是应该保留。

另外题目要求:

原地修改

也就是说不能简单地重新创建一个结果数组,再把答案放进去。

所以可以考虑:

一个指针负责向后寻找
另一个指针负责往前写入

这就是这两道题的快慢双指针。


二、先统一两个指针的含义

为了方便后面从 26 推到 80,我把两道题里的 i 和 j 都统一成同一个含义。

j → 负责向后扫描原数组

i → 下一个可以写入的位置

也就是说:

nums[0:i]

始终表示:

已经整理好的有效区域。

注意这里 Python 切片:

nums[0:i]

是不包含 i 的。

所以如果:

i = 3

说明:

nums[0]
nums[1]
nums[2]

已经是整理好的部分。

一共有:

3 个有效元素

而:

nums[3]

正好就是下一个准备写入的位置。

因此:

当 i 被定义为“下一个写入位置”时,最后的 i 本身也正好等于有效元素数量。 因为数组下标从 0 开始。

所以最终可以直接:

return i


三、LeetCode 26:每个数字最多保留 1 个

先看第一道题。

例如:

nums = [0,0,1,1,1,2,2,3,3,4]

题目希望数组前面最终变成:

[0,1,2,3,4,…]

也就是说:

每个数字只保留 1 个


第一步:第一个数字一定可以保留

因为数组至少有一个元素,所以:

nums[0]

一定可以留下。

因此可以直接认为:

nums[0:1]

已经是整理好的区域。

也就是:

i = 1

此时:

i

指向:

下一个可以写入的位置

然后让:

j

从第二个数字开始寻找:

for j in range(1, len(nums)):


怎么判断 nums[j] 要不要保留?

因为这道题:

每个数字只能出现 1 次

所以当前的:

nums[j]

只需要和:

最后一个已经保留下来的数字

比较。

而已经整理好的区域是:

nums[0:i]

最后一个有效元素下标就是:

i – 1

所以判断:

nums[j] != nums[i 1]

如果不同:

说明出现了一个新的数字

那就应该把它写到:

下一个可以写入的位置 i

也就是:

nums[i] = nums[j]

然后:

i += 1

把写入位置继续向后移动。


用一个例子走一遍

例如:

nums = [0,0,0,1,1,2]

开始:

i = 1

可以理解成:

[0 | 0,0,1,1,2]

i

前面的:

0

已经保留。


j = 1

当前:

nums[j] = 0

最后一个保留值:

nums[i – 1] = nums[0] = 0

相同:

0 == 0

说明是重复值。

跳过。


j = 2

还是:

0

和最后一个保留值:

0

一样。

继续跳过。


j = 3

现在:

nums[j] = 1

而:

nums[i – 1] = 0

不同。

说明:

1

是一个新的数字。

所以:

nums[i] = nums[j]

也就是:

nums[1] = 1

数组前面变成:

[0,1,…]

然后:

i += 1

变成:

i = 2


后面继续

再次遇到:

1

因为:

1 == nums[i – 1]

跳过。

遇到:

2

因为:

2 != nums[i – 1]

写进去。

最后前面变成:

[0,1,2,…]

而:

i = 3

说明一共有:

3 个有效元素

所以直接:

return i


LeetCode 26 完整代码

class Solution:
def removeDuplicates(self, nums: List[int]) > int:
i = 1

for j in range(1, len(nums)):
if nums[j] != nums[i 1]:
nums[i] = nums[j]
i += 1

return i

整个过程可以简单记成:

j → 往后寻找新的数字

i → 指向下一个写入位置

而判断条件:

nums[j] != nums[i 1]

实际上是在问:

当前数字和最后一个已经保留的数字不同吗?

如果不同:

就是新数字 → 保留


四、来到 LeetCode 80:现在允许出现两次

第二题和第一题很像。

但规则变成:

每个数字最多保留 2 个

例如:

[1,1,1,1,2,3,3]

最终应该变成:

[1,1,2,3,3,…]

这里真正麻烦的地方也出现了。

如果继续沿用 26 的判断:

nums[j] != nums[i 1]

就会出问题:

假设现在已经整理成:

[1,1,2,3,…]

现在又遇到一个:

3

如果按照第一题的判断:

当前 3

前一个 3

相同。

那么就会认为:

重复了 → 跳过

但这是错的。

因为这道题允许:

3 出现两次

所以:

3,3

完全合法。

这说明:

LeetCode 80 不能再通过 “是否和前一个一样” 判断要不要保留。

真正应该问的是:

如果我把当前数字放进去,它会不会成为第 3 个相同数字?


那怎么判断它会不会成为第 3 个?

例如已经整理好的部分是:

[1,1,2,3]

现在又来了:

3

当前下一个写入位置:

i = 4

如果我们只看:

i – 1

得到的是:

nums[3] = 3

当然相同。

但这只能证明:

前面已经有 1 个 3

这并不违规。

我们真正关心的是:

前面是不是已经有两个 3?

所以应该再往前看一位:

i – 2

现在:

i = 4

所以:

i – 2 = 2

而:

nums[2] = 2

当前:

nums[j] = 3

于是:

3 != 2

说明:

当前这个 3 放进去以后,
最多只是第二个 3

所以可以保留。

最终:

[1,1,2,3,3]

正好合法。


如果真的已经有两个相同数字呢?

例如:

[1,1,…]

此时:

i = 2

又来了一个:

1

比较:

nums[j]

nums[i – 2]

也就是:

1

nums[0]

结果:

1 == 1

这说明:

在当前写入位置之前,
已经有两个 1 被保留下来了

如果再写当前这个:

1

就会变成:

1,1,1

也就是第 3 个。

所以不能保留。


用完整例子走一遍

例如:

nums = [1,1,1,1,2,3,3]

因为最多允许两个,所以前两个元素无论如何都可以保留。

因此:

i = 2

前面:

[1,1]

已经是有效区域。

让 j 从 2 开始。


j = 2

当前:

nums[j] = 1

比较:

nums[i – 2] = nums[2-2] = nums[0] = 1

于是:

1 == 1

说明如果再放进去,就会成为第 3 个 1。

跳过。


j = 3

还是:

1

因为前面没有写入新元素,所以:i 仍然是 2

继续比较:

nums[i – 2] = nums[0] = 1

还是相同。

继续跳过。


j = 4

当前:

nums[j] = 2

比较:

nums[i – 2] = nums[0] = 1

不同:

2 != 1

说明 2 可以保留。

所以:

nums[i] = nums[j]

变成:

[1,1,2,…]

然后:

i = i + 1 = 3


j = 5

当前值为:

nums[j] = 3

比较:

nums[i – 2] = numms[3-2] = nums[1] = 1

于是:

3 != 1

可以保留。

变成:

[1,1,2,3,…]

然后:

i = i + 1 = 4


j = 6

又来了一个:

3

看:

nums[i – 2] = nums[4-2] = 2

于是:

3 != 2

说明当前这个 3 只是第二个 3。

可以保留。

最终:

[1,1,2,3,3,…]

正好符合题目要求。


LeetCode 80 完整代码

class Solution:
def removeDuplicates(self, nums: List[int]) > int:
n = len(nums)

if n <= 2:
return n

i = 2

for j in range(2, n):
if nums[j] != nums[i 2]:
nums[i] = nums[j]
i += 1

return i

这里:

i → 下一个可以写入的位置

j → 负责扫描原数组

而:

nums[j] != nums[i 2]

其实是在判断:

当前数字如果写进去,会不会成为第 3 个?

如果:

不等于 i – 2 位置的数字

说明还没有超过允许的两个。

所以可以写入。


五、把 26 和 80 放在一起看

现在再回头看两道题。

LeetCode 26

要求:

最多保留 1 个

所以:

i = 1

当前数字要不要保留,看:

nums[i 1]

也就是:

已经保留区域中往前第 1 个数字。

代码:

if nums[j] != nums[i 1]:


LeetCode 80

要求:

最多保留 2 个

所以:

i = 2

当前数字要不要保留,看:

nums[i 2]

也就是:

已经保留区域中往前第 2 个数字。

代码:

if nums[j] != nums[i 2]:

放在一起:

最多保留 1 个

看 i – 1

最多保留 2 个

看 i – 2

到这里其实已经很容易发现规律了。


如果题目变成“最多保留 k 个”呢?

假设题目规定:

每个数字最多可以出现 k 次

那么前:

k 个元素

一定可以直接保留。

所以:

i = k

然后对于后面的:

nums[j]

真正需要判断的是:

当前数字如果再写进去,会不会成为第 k + 1 个?

因此只需要看:

nums[i k]

如果:

nums[j] == nums[i k]

说明:

前面已经保留了 k 个相同数字

再写进去就超出限制。

所以跳过。

反过来:

nums[j] != nums[i k]

说明当前数字还没有达到 k 个。

可以写入。


通用的 i – k 模板

于是可以抽象成:

def removeDuplicates(nums, k):
n = len(nums)

if n <= k:
return n

i = k

for j in range(k, n):
if nums[j] != nums[i k]:
nums[i] = nums[j]
i += 1

return i

整个逻辑就是:

前 k 个元素 → 直接保留

后面的每个 nums[j]

和 nums[i – k] 比较

相同
→ 如果放进去就会超过 k 个
→ 跳过

不同
→ 还没有超过 k 个
→ 写到 nums[i]
→ i += 1


这个方法成立 -> 有序是关键

这里还有一个不能忽略的前提:

数组是有序的

例如:

[1,1,1,2,2,3]

相同数字都会聚在一起。

因此,如果:

nums[j] == nums[i – k]

我们就可以确定:

在已经保留的区域里,当前数字已经至少出现了 k 次。

但如果数组是乱序的,例如:

[1,2,1,3,1]

相同数字没有连在一起,

那只通过:

nums[i k]

就无法判断当前数字到底已经出现了多少次。

所以:

i – k 这种写法能够成立,一个非常重要的基础就是数组已经有序。


六、时间复杂度和空间复杂度

无论是 LeetCode 26 还是 LeetCode 80:j 都只会从左到右扫描一次数组。

所以:

时间复杂度:O(n)

整个过程只使用了:

i
j
n

这些额外变量。

没有创建与输入规模相关的新数组。

所以:

额外空间复杂度:O(1)

并且满足题目要求的:

原地修改

赞(0)
未经允许不得转载:网硕互联帮助中心 » DAY10: LeetCode 26 + 80|删除有序数组重复项:从 `i - 1` 一步推到 `i - k`
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!