283. 移动零
简 单 \color{#00AF9B}{简单} 简单
给定一个数组
nums
,编写一个函数将所有0
移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
- 必须在原数组上操作,不能拷贝额外的数组。
- 尽量减少操作次数。
注意
- 这道题并未给出数组的最大长度,为了保险起见我们还是将指针类型设为
size_t
,也就是vector
数组的size()
方法的返回值,以免数组长度超出int
能表示的范围。
(实际上,在该题的实际提交测试案例中,没有长度超出int
范围的数组,因此设为int
也能通过。但我们还是或多或少要注重这个思想,以免在某些重要场合漏了马脚。) - 当数组为空数组,或者只有一个元素时,是不存在交换操作的,即
nums.size()<=1
。因此可以直接return
终止函数。
方法1:快慢指针
这道题的双指针解法与往常的双指针不太一样。之前我们设置的是左右指针,left
和right
,分别指向数组的起始项和末尾项,遍历时两个指针往中间进发。而这道题由于要保持元素的相对位置,不能使用该方法。
这题要用到的双指针法是快慢指针。两个指针从同一位置出发,快指针fast
移动的较快,或者说始终处于慢指针的前方,找到下一个快指针要找的值;慢指针slow
移动的较慢,保留慢指针目标值的位置,等到快指针确定后,两者进行交换等操作。
本题中,我们可以定义慢指针slow
先找到0
的位置,然后快指针fast
从slow+1
的位置往后找到第一个值不为0
的位置,最后执行交换操作。
过程演示
#include <vector>
using namespace std;
class Solution
{
public:
void moveZeroes(vector<int> &nums)
{
const size_t n = nums.size();
if (n <= 1)
return;
size_t fast = 0, slow = 0;
while (true)
{
while (slow < n - 1 && nums[slow] != 0)
{
slow++;
}
if (slow == n - 1)
return;
fast = slow + 1;
while (fast < n && nums[fast] == 0)
{
fast++;
}
if (fast == n)
return;
swap(nums[slow], nums[fast]);
}
}
};
注:
(此项可以不考虑)慢指针slow
指向的是0
所在的位置,当其为n-1
时,代表找到了一个存在于数组末尾的0
,而此时这个0
不需要和任何元素交换,因此slow
在指向n-1
位置时就可以跳出循环了。
复杂度分析
时间复杂度:O(n),每个元素至多被遍历2次,O(2n)=O(n)。
空间复杂度:O(1)。我们只需要常量空间存放若干变量。
参考结果
Accepted
74/74 cases passed (136 ms)
Your runtime beats 5.69 % of cpp submissions
Your memory usage beats 49.59 % of cpp submissions (18.6 MB)
方法1(变化):快慢指针
方法1中的代码显得稍微有些长,我们可以使用下文中的方法减少代码量。不同于方法1中让fast
和slow
分别出发然后各自找到自己的目标,这个方法让fast
和slow
同时出发,若是遇到了0
,则slow
不自增,而fast
自己继续前进找到第一个不为0
的值。这样既符合快慢指针的概念,也是实现快慢指针的另一种思路。
流程如下:
fast
和slow
一起走。如果fast
指到了非零元素,则:- 若
fast!=slow
,则进行交换。slow
自增。 - 若
fast==slow
,则代表两者还在同步前进,两个都执行同一个元素,不需要交换。slow
自增。
- 若
fast
指到的是0
,则不用管slow
如何,自己自增。
流程演示
#include <vector>
using namespace std;
class Solution
{
public:
void moveZeroes(vector<int> &nums)
{
size_t n = nums.size();
size_t fast = 0, slow = 0;
while (fast < n)
{
if (nums[fast] != 0)
{
if (fast != slow)
swap(nums[slow], nums[fast]);
slow++;
}
fast++;
}
}
};
复杂度分析
时间复杂度:O(n),每个元素至多被遍历2次,O(2n)=O(n)。
空间复杂度:O(1)。我们只需要常量空间存放若干变量。
参考结果
Accepted
74/74 cases passed (20 ms)
Your runtime beats 46.48 % of cpp submissions
Your memory usage beats 38.19 % of cpp submissions (18.7 MB)
167. 两数之和 II - 输入有序数组
简 单 \color{#00AF9B}{简单} 简单
给定一个已按照 非递减顺序排列 的整数数组
numbers
,请你从数组中找出两个数满足相加之和等于目标数target
。
函数应该以长度为2
的整数数组的形式返回这两个数的下标值。numbers
的下标 从 1 开始计数 ,所以答案数组应当满足1 <= answer[0] < answer[1] <= numbers.length
。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
提示:
- 2 <= numbers.length <= 3 * 104
- -1000 <= numbers[i] <= 1000
- numbers 按 非递减顺序 排列
- -1000 <= target <= 1000
- 仅存在一个有效答案
注意
题目要求答案应从下标1开始计数。我们在过程中还是保持从0开始计数,仅在返回结果时+1,这样可以省去很多麻烦。
方法1:二分查找
最粗暴的方式就是O(n2)的两次完全遍历查找:先确定一个数,然后一轮n次的遍历查找第二个数,这样的查找最多做n轮。
我们可以将内层的循环改为二分查找,即确定一个数,然后进行一轮O(logn)的二分查找。这样的查找最多做n轮,因此时间复杂度为O(nlogn)
#include <vector>
using namespace std;
class Solution
{
public:
vector<int> twoSum(vector<int> &numbers, int target)
{
const int n = numbers.size();
for (int i = 0; i < n - 1; i++)
{
int left = i + 1, right = n - 1;
while (left <= right)
{
int mid = ((right - left) >> 1) + left;
if (numbers[i] + numbers[mid] == target)
{
return {i + 1, mid + 1};
}
else if (numbers[i] + numbers[mid] > target)
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}
}
// 虽然题目保证必有解,即上个循环中必定会返回值
// 但由于函数在各分支必须有返回,因此需要象征性地返回
return {};
}
};
复杂度分析
时间复杂度:O(nlogn)。对于每一个数,我们都需要执行一次O(logn)
的二分查找;而我们最外层最多遍历n
次(细节一点的话是n-1
次,因为外层确定的是最后一个数的话,它后面没有数可以找了),因此为n
次logn
。
空间复杂度:O(1)。我们只需要常量空间存放若干变量。
参考结果
Accepted
19/19 cases passed (4 ms)
Your runtime beats 86.14 % of cpp submissions
Your memory usage beats 97.18 % of cpp submissions (9.2 MB)
方法2:双指针
设置指针left
和right
分别指向数组的开头和末尾。按以下思路即可完成:
- 若
[left]+[right]==target
,则返回{left, right}
; - 若
[left]+[right]<target
,由于大的值[right]
不能再大了,因此[left]
要变大,left
要往右走(自增); - 若
[left]+[right]>target
,由于小的值[left]
不能再小了,因此[right]
要变小,right
要往左走(自减)。
#include <vector>
using namespace std;
class Solution
{
public:
vector<int> twoSum(vector<int> &numbers, int target)
{
int left = 0, right = numbers.size() - 1;
while (left < right)
{
int sum = numbers[left] + numbers[right];
if (sum == target)
{
return {left + 1, right + 1};
}
else if (sum < target)
{
left++;
}
else
{ // sum > target
right--;
}
}
// 虽然题目保证必有解,即上个循环中必定会返回值
// 但由于函数在各分支必须有返回,因此需要象征性地返回
return {};
}
};
注:
由于题目保证有解,因此在循环内必能找到答案并return
。但编译器要确保每条支路都要有返回,因此在main
函数最后要象征性地写一句return
。
复杂度分析
时间复杂度:O(n)
空间复杂度:O(1)。我们只需要常量空间存放若干变量。
参考结果
Accepted
19/19 cases passed (4 ms)
Your runtime beats 86.14 % of cpp submissions
Your memory usage beats 93.72 % of cpp submissions (9.2 MB)
Animation powered by ManimCommunity/manim