LeetCode 283: 移动零
题目
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
这道题我一开始的想法是使用稳定的排序算法(略加改装),比如冒泡排序,插入排序。但是有点太复杂了,时间复杂度为O(N^2),如下:
#include <stdio.h>
void moveZeroes_InsertionSort(int* nums, int numsSize) {
if (numsSize <= 1) {
return;
}
// i 从 1 开始,代表“未排序”部分的第一个元素
for (int i = 1; i < numsSize; i++) {
int key = nums[i]; // 这是我们这次要处理的牌
// 如果这张牌是0,我们根本不关心它,直接跳过,让它待在后面
if (key == 0) {
continue;
}
// j 是“已排序”部分的末尾
int j = i - 1;
// 现在 key 是一个非零数,我们要给它找位置
// 我们需要把前面所有是0的牌都往后挪
// 只要 j 还没越界,并且指着一张0牌,就把它往后挪
while (j >= 0 && nums[j] == 0) {
nums[j + 1] = nums[j]; // 0往后挪
j--;
}
// 循环结束,j+1 就是这个非零数 key 该插入的位置
nums[j + 1] = key;
}
}
// (这里可以放一个main函数来测试)
int main() {
int arr[] = {0, 1, 0, 3, 12};
int size = sizeof(arr) / sizeof(arr[0]);
moveZeroes_InsertionSort(arr, size);
printf("Result: ");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n"); // 应该输出: 1 3 12 0 0
return 0;
}
如上可见,这是非常垃圾的答案,所以开始寻找别的解法,最终找到了双指针法,第一版代码如下,用一个while循环让指针i跳过所有的0,找到第一个非零数。用另一个while循环让指针j跳过所有的非零数,找到第一个零。然后把它们交换:
void swap(int *nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
void moveZeroes(int* nums, int numsSize) {
int i=0,j=0;
while(j<numsSize&&i<numsSize){
while(i<numsSize&&nums[i]==0){
i++;
if(i>=numsSize){
return;
}
}
while(j<numsSize&&nums[j]!=0){
j++;
if(j>=numsSize){
return;
}
}
swap(nums,i,j);
i++;
j++;
}
}
正确的思路:双指针法(铲雪车模型)
需要两个指针:
慢指针 slow (或者叫 insertPos):
它的作用,是指向下一个非零元素应该被放置的位置。一开始索引为0。
快指针 fast (或者叫 i):
它的作用,是去遍历整个数组,寻找非零元素。它从头走到尾,不放过任何一个元素。
算法流程:
初始化 slow = 0。
fast 指针从 0 遍历到数组末尾。
在遍历过程中,只要 fast 指针发现了一个非零元素 (nums[fast] != 0):
就把这个非零元素“铲”到 slow 指针指向的位置上,即 nums[slow] = nums[fast]。
然后,让 slow 指针向前移动一格,slow++,为下一个非零元素腾出位置。
当 fast 指针走完全程后,所有非零元素就已经被我们紧凑地、且按原有顺序地移动到了数组的开头。slow 指针此时就停在了最后一个非零元素的下一个位置。
最后,从 slow 的位置开始,把数组剩下的所有位置全填成0。
这道题的收获就是不要思维定式了,要以结果为导向,并不是一定要交换元素来实现结果的,像这样两次遍历会更方便也更好理解