LeetCode笔记:原地修改数组
自大学开始,我便陆陆续续的学习一些 算法和数据结构 方面的内容,同时也开始在一些平台刷题,也会参加一些大大小小的算法竞赛。但是平时刷题缺少目的性、系统性,最终导致算法方面进步缓慢。最终,为了自己的未来,我决定开始在LeetCode上进行系统的学习和练习,同时将刷题的轨迹整理记录,分享出来与大家共勉。
参考教材: labuladong
参考资料: 如何去除有序数组的重复元素
我们知道对于数组来说,在尾部插入、删除元素是比较高效的,时间复杂度是 O(1),但是如果在中间或者开头插入、删除元素,就会涉及数据的搬移,时间复杂度为 O(N),效率较低。
在文章 O(1)时间删除/查找数组中的任意元素 就讲了一种技巧,把待删除元素交换到最后一个,然后再删除,就可以避免数据搬移。
一、有序数组/链表去重
先讲讲如何对一个有序数组去重,先看下题目:
26. 删除排序数组中的重复项
难度:简单
给定一个排序数组,你需要在** 原地** 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
函数签名如下:
int removeDuplicates(int[] nums);
显然,由于数组已经排序,所以重复的元素一定连在一起,找出它们并不难,但如果毎找到一个重复元素就立即删除它,就是在数组中间进行删除操作,整个时间复杂度是会达到 O(N^2)。
简单解释一下什么是原地修改:
如果不是原地修改的话,我们直接 new 一个 int[]
数组,把去重之后的元素放进这个新数组中,然后返回这个新数组即可。
但是原地删除,不允许我们 new 新数组,只能在原数组上操作,然后返回一个长度,这样就可以通过返回的长度和原始数组得到我们去重后的元素有哪些了。
这种需求在数组相关的算法题中时非常常见的,通用解法就是我们前文 双指针技巧 中的快慢指针技巧。
我们让慢指针 slow
走在后面,快指针 fast
走在前面探路,找到一个不重复的元素就告诉 slow
并让 slow
前进一步。这样当 fast
指针遍历完整个数组 nums
后,nums[0..slow]
就是不重复元素。
题解: Java
class Solution{
public int removeDuplicates(int[] nums) {
if (nums.length == 0)
return 0;
int fast = 0, slow = 0;
while (fast < nums.length) {
if (nums[fast] != nums[slow]) {
slow++;
// 维护 nums[0..slow] 无重复
nums[slow] = nums[fast];
}
fast++;
}
// 数组长度为索引 + 1
return slow + 1;
}
}
看下算法执行的过程:
再简单扩展一下,如果给你一个有序链表,如何去重呢?这是力扣第 83 题,其实和数组去重是一模一样的,唯一的区别是把数组赋值操作变成操作指针而已:
83. 删除排序链表中的重复元素
难度:简单
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:
输入: 1->1->2->3->3
输出: 1->2->3
题解: Java
class Solution {
public 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.next;
}
//断开后面与重复元素的链接
slow.next=null;
return head;
}
}
二、移除元素
27. 移除元素
难度:简单
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
函数签名如下:
int removeElement(int[] nums, int val);
题目要求我们把 nums
中所有值为 val
的元素原地删除,依然需要使用 双指针技巧 中的快慢指针:
如果 fast
遇到需要去除的元素,则直接跳过,否则就告诉 slow
指针,并让 slow
前进一步。
这和前面说到的数组去重问题解法思路是完全一样的,就不画 GIF 了,直接看代码:
题解: Java
class Solution{
public int removeElement(int[] nums, int val) {
if (nums.length == 0)
return 0;
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
移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
函数签名如下:
void moveZeroes(int[] nums);
比如说给你输入 nums = [0,1,4,0,2]
,你的算法没有返回值,但是会把 nums
数组原地修改成 [1,4,2,0,0]
。
结合之前说到的几个题目,你是否有已经有了答案呢?
题目让我们将所有 0 移到最后,其实就相当于移除 nums
中的所有 0,然后再把后面的元素都赋值为 0 即可。
所以我们可以复用上一题的 removeElement
函数:
题解: Java
class Solution{
public 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) {
int fast = 0, slow = 0;
while (fast < nums.length) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
至此,四道「原地修改」的算法问题就讲完了,其实核心还是快慢指针技巧,你学会了吗?