

一文秒杀四道原地修改数组的算法题
source link: https://labuladong.github.io/algo/%E9%AB%98%E9%A2%91%E9%9D%A2%E8%AF%95%E7%B3%BB%E5%88%97/%E5%8E%9F%E5%9C%B0%E4%BF%AE%E6%94%B9%E6%95%B0%E7%BB%84.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何去除有序数组的重复元素
👆让天下没有难刷的算法!若 GitBook 访问太慢,可尝试 GiteePages 或 GitHubPages!
相关推荐:
读完本文,你不仅学会了算法套路,还可以顺便去 LeetCode 上拿下如下题目:
我们知道对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。
所以上篇文章 O(1)时间删除/查找数组中的任意元素 就讲了一种技巧,把待删除元素交换到最后一个,然后再删除,就可以避免数据搬移。
有序数组/链表去重
先讲讲如何对一个有序数组去重,先看下题目:

函数签名如下:
int removeDuplicates(int[] nums);
显然,由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难,但如果毎找到一个重复元素就立即删除它,就是在数组中间进行删除操作,整个时间复杂度是会达到 O(N^2)。
简单解释一下什么是原地修改:
如果不是原地修改的话,我们直接 new 一个 int[]
数组,把去重之后的元素放进这个新数组中,然后返回这个新数组即可。
但是原地删除,不允许我们 new 新数组,只能在原数组上操作,然后返回一个长度,这样就可以通过返回的长度和原始数组得到我们去重后的元素有哪些了。
这种需求在数组相关的算法题中时非常常见的,通用解法就是我们前文 双指针技巧 中的快慢指针技巧。
我们让慢指针 slow
走在后面,快指针 fast
走在前面探路,找到一个不重复的元素就告诉 slow
并让 slow
前进一步。这样当 fast
指针遍历完整个数组 nums
后,nums[0..slow]
就是不重复元素。
int removeDuplicates(int[] nums) {
if (nums.length == 0) {
return 0;
}
int slow = 0, fast = 0;
while (fast < nums.length) {
if (nums[fast] != nums[slow]) {
slow++;
// 维护 nums[0..slow] 无重复
nums[slow] = nums[fast];
}
fast++;
}
// 数组长度为索引 + 1
return slow + 1;
}
看下算法执行的过程:

再简单扩展一下,如果给你一个有序链表,如何去重呢?这是力扣第 83 题,其实和数组去重是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已:
ListNode deleteDuplicates(ListNode head) {
if (head == null) return null;
ListNode slow = head, fast = head;
while (fast != null) {
if (fast.val != slow.val) {
// nums[slow] = nums[fast];
slow.next = fast;
// slow++;
slow = slow.next;
}
// fast++
fast = fast.next;
}
// 断开与后面重复元素的连接
slow.next = null;
return head;
}

这是力扣第 27 题,看下题目:

函数签名如下:
int removeElement(int[] nums, int val);
题目要求我们把 nums
中所有值为 val
的元素原地删除,依然需要使用 双指针技巧 中的快慢指针:
如果 fast
遇到需要去除的元素,则直接跳过,否则就告诉 slow
指针,并让 slow
前进一步。
这和前面说到的数组去重问题解法思路是完全一样的,就不画 GIF 了,直接看代码:
int removeElement(int[] nums, int val) {
int fast = 0, slow = 0;
while (fast < nums.length) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
注意这里和有序数组去重的解法有一个重要不同,我们这里是先给 nums[slow]
赋值然后再给 slow++
,这样可以保证 nums[0..slow-1]
是不包含值为 val
的元素的,最后的结果数组长度就是 slow
。
这是力扣第 283 题,我来描述下题目:
给你输入一个数组 nums
,请你原地修改,将数组中的所有值为 0 的元素移到数组末尾,函数签名如下:
void moveZeroes(int[] nums);
比如说给你输入 nums = [0,1,4,0,2]
,你的算法没有返回值,但是会把 nums
数组原地修改成 [1,4,2,0,0]
。
结合之前说到的几个题目,你是否有已经有了答案呢?
题目让我们将所有 0 移到最后,其实就相当于移除 nums
中的所有 0,然后再把后面的元素都赋值为 0 即可。
所以我们可以复用上一题的 removeElement
函数:
void moveZeroes(int[] nums) {
// 去除 nums 中的所有 0
// 返回去除 0 之后的数组长度
int p = removeElement(nums, 0);
// 将 p 之后的所有元素赋值为 0
for (; p < nums.length; p++) {
nums[p] = 0;
}
}
// 见上文代码实现
int removeElement(int[] nums, int val);
至此,四道「原地修改」的算法问题就讲完了,其实核心还是快慢指针技巧,你学会了吗?
_____________
《labuladong 的算法小抄》已经出版,关注公众号「labuladong」查看详情;后台回复关键词「进群」可加入算法群,大家一起刷题/内推:
Recommend
-
46
-
41
属于第一代民营企业家的时代过去了
-
40
原文链接 对于编程中的一些类型判定问题(尤其是数学问题)最优雅的解决方案是使用递归,因为递归可以直接把这个问题转变成一个有关于数学定义的问题。不幸的...
-
31
Android - @PostMeridiem18 - **天下苦微信久矣**稍有常识的人都可以看出,苹果并不会因为微信不适配 iOS13 的暗色模式 SDK 而单独点名微信,更不会因为微信不做适配就在 App Store 下架——因为暗色模式仅只
-
12
-
7
四道题让你变猪头,敢不敢试试?? 作者: 张志强 ...
-
5
最优原地后缀排序算法 本章介绍线性时间复杂度的后缀排序的就地算法1(Optimal In-Place Suffix So...
-
6
双指针技巧秒杀七道数组题目
-
5
一文秒杀所有岛屿题目
-
9
百济神州2024年的四道考题 “大展宏图”的口号背后,实则是百济屡受波折的身影。 2023年12月初百济神州举办的年会上,公司高调提出了“做内企想做而不能做的,做外企能做而不可做的”的新口号。这与三年前“恰逢好拾光”的谦卑标语风格迥然。
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK