一、题目介绍
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
说明: 必须在原数组上操作,不能拷贝额外的数组。 尽量减少操作次数。
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
来源:力扣(leetcode)
链接: moveZero.
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
二、解题思路
1.暴力解法
对于没有头绪的算法题,我们一般可以先尝试暴力解法,然后层层递进优化,下面是我参考力扣题解里老汤老师的讲解,总结这道题
① 我们想要求的结果是一个数组,那么一开始就可以构造一个长度等于nums的数组,初始化为0。
② 现在要做的就是把nums数组里非0的元素依次放入tmp数组中。
做法:遍历Nums数组,如果非0(i指针),将其放入tmp中第一个0元素(j指针)。依次增加
然后,题目要求,输入的数组还是nums数组,即还需要将tmp数组一个个拷贝进入nums数组;
代码如下(暴力解法):
//时间复杂度O(n)
//空间复杂度O(n)
public void moveZeros1(int[] nums){
if(nums == null || nums.length == 0){
return;
}
int[] tmp = new int[nums.length];
//记录tmp数组第一个为0的元素
int j = 0;
for (int i = 0; i < nums.length ; i++) {
if(nums[i] != 0){
tmp[j] = nums[i];
j++;
}
}
for (int i = 0; i < nums.length; i++) {
nums[i] = tmp[i];
}
}
2.双指针优化空间
优化:在常量级别的条件下,即O(1)
在nums一个数组上 引入两个指针,i(代表当前元素),j(代表i指的当前非0元素将要移入的位置)
原地算法:在原始输入数组上完成的算法,没有申请额外的空间
代码如下(双指针算法):
//时间复杂度为o(n) 遍历一遍数组
//空间复杂度 O(1)
public void moveZeros(int[] nums){
if(nums == null || nums.length == 0){
return ;
}
//下一个非零元素存储的位置
int j = 0;
for (int i = 0; i < nums.length; i++) {
if(nums[i] != 0){
//交换i和j指向的元素
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
j++;
}
}
}
3.引出快慢指针的概念
本题目中,根据以上分析,i走的比较快,j指针走的比较慢,为提高代码阅读效率。
将指针 i 名字改成 fast,指针 j 改成slow。这里仅仅引入快慢指针的概念。
4.继续优化,减少操作数量
观察第三步的代码,思考还有无进一步优化的空间。
判断数组为空不可以省略,接下来还必须判断元素是否为0,所以for循环不可以再有所改进,再观察for循环体内,当前是元素不为0时,交换元素。
此时考虑能否减少元素交换的次数,来提升算法性能。
思考:什么时候可以减少元素交换?
① 当fast == slow时,元素没有必要去交换
所以,改进代码如下:
//多加一个判断,来减少元素交换次数
if(slow != fast){
//进行交换
}
② 不进行两个元素交换,而是去更新赋值操作
交换元素,需要对元素访问两次,但是赋值操作只需要访问一次内存就可以
如下:
想想我们的初始目的是:将非0元素全部移动到前面,要实现这样的操作,除了去交换元素,也可以去直接去给nums[slow]赋值。
(直接将nums[slow] == nums[fast])
当fast跳出循环之后,剩下的操作是,将slow所指后面的元素其他值赋值为0。
代码改写如下:
public void moveZeroes(int[] nums) {
if (nums == null || nums.length == 0) {
return;
}
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
// 减少赋值的次数
if (slow != fast) {
//不在交换两个元素,而是对nums[slow]赋值
nums[slow] = nums[fast];
}
slow++;
}
}
for (; slow < nums.length; slow++) {
nums[slow] = 0;
}
}