目录
本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典数组算法题,以后会更新“二刷”“三刷”等等。
LeetCode #27: Remove Element 移除元素
给你一个数组 nums
和一个值 val
,你需要原地(in-place)移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
换言之,我们只需要保证数组索引在
[
0
,
k
)
\ [0, k)
[0,k) 内的元素对应的值均不同于 val
即可。
所谓原地,就是我们不能 new
一个新的数组,必须在原有数组上进行修改。我们无法直接删除数组中的某一个元素,数组的元素在内存地址中是连续的,只能覆盖。
暴力解法
这种方法很容易想到,也是最为直观的,即使用两层嵌套循环,首先遍历数组,一旦发现需要移除的元素,就将剩余的数组整体前移一位,直接将需要移除的元素覆盖掉。
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) {
for (int j = i + 1; j < size; j++)
nums[j - 1] = nums[j];
i--; //下标i以后的数值都向前移动了一位,i也向前移动一位
size--; //此时数组的大小 - 1
}
}
return size;
}
};
对暴力算法进行复杂度分析,不难得出该算法的时间复杂度为
O
(
n
2
)
\ O(n^2)
O(n2),对于一般情况来说,这并不是一个理想的结果。程序提交结果如下:
完全可以 AC,但是存在优化的空间。在这种算法中,将数组剩余元素整体前移的行为使得时间复杂度急剧增大,这一行为并不是完成这道题目所必需的,我们完全可以将需要删除的元素替换为下一个不同于 val
的元素,返回值 k
则始终指向不需要移除的元素的下一个元素,那么经过一次遍历后,可以返回一个满足题意的 k
值。
快慢指针解法(Tow-Pointer Technique)
所谓快慢指针,就是两个指针 slow
和 fast
同向而行,一快一慢。在数组上并没有严格意义上的指针,但我们可以将数组中的索引比作“指针”,快指针 fast
指向当前要和 val
对比的元素,慢指针 slow
则指向将被赋值的位置。
我们让慢指针 slow
走在后面,快指针 fast
走在前面探路,找到一个需要移除的元素就赋值给 nums[slow]
并让 slow
前进一步。假设 val
= 2:
总体来看,思路大抵是这样的:
- 遍历数组。如果
fast
指针指向的元素nums[fast]
≠ \neq =val
,则nums[fast]
是输出数组的元素,将其赋值到nums[slow]
的位置,slow
和fast
同时向后移动一位。 - 如果
nums[fast]
=val
,证明当前nums[fast]
是要移除的元素,fast
向后移动一位。 fast
遍历完整个数组,循环终止,slow
的值就是剩余数组的长度。
以此类推,就可以保证从 nums[0]
到 nums[slow]
(不包含后者)均为不同于 val
的元素。在上述图中所给出例子中,最终函数返回 slow
,截取出数组
[
0
,
1
,
3
,
0
,
4
]
\ [0, 1, 3, 0, 4]
[0,1,3,0,4],符合题意。代码实现如下:
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++)
if (nums[fast] != val)
nums[slow++] = nums[fast];
return slow;
}
};
对此种算法进行复杂度分析,该算法的时间复杂度为
O
(
n
)
\ O(n)
O(n)。
更多例子
让我们原地修改数组的题目,一般均可使用快慢指针技巧秒杀。
给你一个非严格递增排列的数组 nums
,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致。然后返回 nums
中唯一元素的个数。
考虑 nums
的唯一元素的数量为 k
,你需要做以下事情确保你的题解可以被通过:
- 更改数组
nums
,使nums
的前k
个元素包含唯一元素,并按照它们最初在nums
中出现的顺序排列。 nums
的其余元素与nums
的大小不重要。- 返回
k
。
思路如下:
- 遍历数组。如果
fast
指针指向的元素nums[fast]
≠ \neq =nums[slow]
,则nums[fast]
是输出数组的元素,slow
向后移动一位,将其赋值到nums[slow]
的位置,fast
向后移动一位。 - 如果
nums[fast]
=nums[slow]
,证明当前nums[fast]
是重复的元素,fast
向后移动一位。 fast
遍历完整个数组,循环终止,slow
的值为不重复数组的最后一个元素的索引,应返回剩余数组的长度slow + 1
。
本题的思路与#27题之所以不太一样,主要是因为#27题引入了外部参数 val
进行比对,而本题是基于数组内元素进行去重。
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++)
if (nums[slow] != nums[fast])
nums[++slow] = nums[fast]; //易错点
return slow + 1; //注意返回值应为数组长度而非最后一位索引
}
};
✅ 拓展:一个有序单链表的去重,也可以使用快慢指针法解决。
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
其实和数组去重是一致的,唯一的区别是把数组赋值操作变成操作指针而已。
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if (head == nullptr) return nullptr;
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr)
{
if (slow->val != fast->val)
{
slow->next = fast; //当前元素的下一个元素设置为下一个不相等的元素
slow = slow->next; //当前元素向后移动
}
fast = fast->next; //下一个元素向后移动
}
slow->next = nullptr; //断开与后面重复元素的连接
return head;
}
};
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
对于这道题,我们完全可以复用#27题的函数 removeElement
,先删除所有的 0
,再在数组尾部补全所有的 0
。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int k = removeElement(nums, 0);
for (; k < nums.size(); k++)
nums[k] = 0;
}
int removeElement(vector<int>& nums, int val) { //#27题原函数
int slow = 0;
for (int fast = 0; fast < nums.size(); fast++)
if (nums[fast] != val)
nums[slow++] = nums[fast];
return slow;
}
};
LeetCode #59:Spiral Matrix II 螺旋矩阵 II
给你一个正整数 n
,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n
正方形矩阵 matrix
。
一道难度中等、面试出现概率极高的考察编程能力的题目,本身不涉及算法,但是容易出现 bug。
基本思路如下(上 ⇒ 右 ⇒ 下 ⇒ 左):
- 上:从左到右填充。
- 右:从上到下填充。
- 下:从右到左填充。
- 左:从下到上填充。
简单解法
非常经典的模拟法,直接用编程语言直白地表现出来即可;注意随时画图,避免出现错误。以四个边界作为每行每列遍历填充的起点与终点,每完成一步就缩小边界规模。
基本思路中的 4 步走完后,需要全部遍历并填充,直到 n2 的数全部填完。
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> matrix(n, vector<int>(n)); //初始化二维数组
int cnt = 1; //初始化指针
int upper_bound = 0, lower_bound = n - 1; //初始化四个矩阵边界
int left_bound = 0, right_bound = n - 1;
while (cnt <= n * n) //重复基本 4 步
{
//从左至右填充最上行
for (int i = left_bound; i <= right_bound; i++) matrix[upper_bound][i] = cnt++;
upper_bound++; //上边界向下移动
//从上到下填充最右列
for (int i = upper_bound; i <= lower_bound; i++) matrix[i][right_bound] = cnt++;
right_bound--; //右边界向左移动
//从右至左填充最下行
for (int i = right_bound; i >= left_bound; i--) matrix[lower_bound][i] = cnt++;
lower_bound--; //下边界向上移动
//从下到上填充最左列
for (int i = lower_bound; i >= upper_bound; i--) matrix[i][left_bound] = cnt++;
left_bound++; //左边界向右移动
}
return matrix;
}
};
显然,该算法的时间复杂度为 O ( n 2 ) \ O(n^2) O(n2),由于需要维护一个大小为 n 2 \ n^2 n2 的二维数组,则空间复杂度也为 O ( n 2 ) \ O(n^2) O(n2)。
@halfrost 解法
大神 @halfrost 的解法可能会略显繁琐(可能也是因为 Go 语言 C++ 化的原因),再这里先挖个坑,以后有时间再来填上相关的笔记与思路。可以 AC 通过,最快运行时间 0 ms
。
- 补充对于@halfrost 解法的笔记
- 继续核查 C++描述版本的准确性(是否还原大神的做法)
新版本:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
int num = 1;
int row = 0, col = 0;
int direction = 0; // 0: 向右, 1: 向下, 2: 向左, 3: 向上
vector<pair<int, int>> dirs = {
{0, 1}, // 向右
{1, 0}, // 向下
{0, -1}, // 向左
{-1, 0} // 向上
};
for (int i = 0; i < n * n; ++i) {
res[row][col] = num++;
// 检查下一个位置是否有效
int nextRow = row + dirs[direction].first;
int nextCol = col + dirs[direction].second;
// 如果下一个位置无效(越界或已访问),则改变方向
if (nextRow < 0 || nextRow >= n || nextCol < 0 || nextCol >= n || res[nextRow][nextCol] != 0) {
// 使用switch语句简化方向更新
switch (direction) {
case 0: // 当前向右,无效则尝试向下
if (row + 1 < n && res[row + 1][col] == 0) direction = 1;
else direction = 3; // 否则向上
break;
case 1: // 当前向下,无效则尝试向左
if (col - 1 >= 0 && res[row][col - 1] == 0) direction = 2;
else direction = 0; // 否则向右
break;
case 2: // 当前向左,无效则尝试向上
if (row - 1 >= 0 && res[row - 1][col] == 0) direction = 3;
else direction = 1; // 否则向下
break;
case 3: // 当前向上,无效则尝试向右
if (col + 1 < n && res[row][col + 1] == 0) direction = 0;
else direction = 2; // 否则向左
break;
}
// 不需要再单独更新 nextRow 和 nextCol,因为已经确定了新的方向
// 直接使用 dirs[direction]
row += dirs[direction].first;
col += dirs[direction].second;
} else {
// 如果下一个位置有效,则直接移动到该位置
row = nextRow;
col = nextCol;
}
}
return res;
}
};
老版本(未还原 switch
语句):
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0));
vector<vector<bool>> visited(n, vector<bool>(n, false));
int num = 1;
int row = 0, col = 0;
int direction = 0; // 0: right, 1: down, 2: left, 3: up
vector<pair<int, int>> dirs = {
{0, 1}, // right
{1, 0}, // down
{0, -1}, // left
{-1, 0} // up
};
for (int i = 0; i < n * n; ++i) {
res[row][col] = num++;
visited[row][col] = true;
int nextRow = row + dirs[direction].first;
int nextCol = col + dirs[direction].second;
// 避免越界
if (nextRow < 0 || nextRow >= n || nextCol < 0 || nextCol >= n || visited[nextRow][nextCol]) {
// 更改方向
direction = (direction + 1) % 4;
// 移动到新方向上的下一个有效位置
nextRow = row + dirs[direction].first;
nextCol = col + dirs[direction].second;
}
row = nextRow;
col = nextCol;
}
return res;
}
};
LeetCode #209:Minimum Size Subarray Sum 长度最小的子数组
给定一个含有 n
个正整数的数组和一个正整数 target
。找出该数组中满足其总和大于等于 target
的长度最小的子数组
[
n
u
m
s
l
,
n
u
m
s
l
+
1
,
.
.
.
,
n
u
m
s
r
−
1
,
n
u
m
s
r
]
\ [nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r]
[numsl,numsl+1,...,numsr−1,numsr],并返回其长度。如果不存在符合条件的子数组,返回 0
。
暴力查找
遍历数组 nums
,每次将一个下标的元素作为子数组的开始,接下来从下标 i
开始向后遍历,找到最小下标 j
,使得从 nums[i]
到 nums[j]
之和大于等于 target
,更新子数组的最小长度。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int l = nums.size(); //获取数组长度
int min_len = l + 1; //初始化最小长度为数组长度 + 1
for (int i = 0; i < l; i++) {
int cur_sum = 0; //遍历过的总和,该值要与target进行比较
for (int j = i; j < l; j++) {
cur_sum += nums[j]; //累加当前元素到和中
if (cur_sum >= target) { // 若和大于等于目标值
min_len = min(min_len, j - i + 1); // 更新最小长度
break; // 跳出内层循环
}
}
}
return min_len == l + 1 ? 0 : min_len; // 若最小长度未被修改,返回0,否则返回最小长度
}
};
该算法运用了双重嵌套循环,时间复杂度为 O ( n 2 ) \ O(n^2) O(n2),对于题目给出最高 1 0 5 \ 10^5 105 数量级的数据,显然过于缓慢,以至于超出 AC 时间限制。
滑动窗口算法
引入:左右指针技巧
在处理数组和链表相关问题时,双指针技巧是经常用到的。双指针技巧主要分为两类:一个是上述已然涉及的快慢指针,另一个则是左右指针。
所谓左右指针,就是两个指针相向而行或者相背而行。实际上,二分搜索算法就是对于左右指针的经典应用:
int binarySearch(vector<int>& nums, int target) {
// 一左一右两个指针相向而行
int left = 0, right = nums.size() - 1;
while(left <= right) {
int mid = (right + left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1;
}
return -1;
}
给你一个下标从 1 开始的整数数组 numbers
,该数组已按非递减顺序排列,请你从数组中找出满足相加之和等于目标数 target
的两个数。如果设这两个数分别是 numbers[index1]
和 numbers[index2]
,则 1
≤
\leq
≤ index1
<
\ <
< index2
≤
\leq
≤ numbers.length
。
以长度为 2 的整数数组 [ index1
, index2
] 的形式返回这两个整数的下标 index1
和 index2
。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
只要数组有序,就应该想到双指针技巧。这道题的解法有点类似于二分搜索,通过调节 left
和 right
就可以调整 sum
的大小:
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}; // 题目要求的索引是从 1 开始的
else if (sum < target) left++; // 让 sum 大一点
else if (sum > target) right--; // 让 sum 小一点
}
return {-1, -1};
}
};
一般编程语言都会提供 reverse
函数来反转字符串,其实这个函数的原理非常简单:
class Solution {
public:
void reverseString(vector<char>& s) {
// 一左一右两个指针相向而行
int left = 0, right = s.size() - 1;
while (left < right) {
// 交换 s[left] 和 s[right]
char temp = s[left];
s[left] = s[right];
s[right] = temp;
left++;
right--;
}
}
};
给你一个字符串 s
,找到 s
中最长的回文子串(正反序一致)。
找回文串的难点在于,回文串的长度可能是奇数也可能是偶数。如果回文串长度为奇数,则它有一个中心字符;如果回文串长度为偶数,我们可以认为它有两个中心字符。因此,解决问题的核心在于从中心向两端扩散的左右指针技巧,这样我们就可以十分便捷地表示回文串的本质:
// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
string palindrome(string s, int l, int r) {
// 防止索引越界
while (l >= 0 && r < s.length()
&& s[l] == s[r]) {
// 双指针,向两边展开
l--; r++;
}
// 返回以 s[l] 和 s[r] 为中心的最长回文串
return s.substr(l + 1, r - l - 1);
}
那么,根据我们以上达成有关于奇偶数的共识,如果输入相同的 l
和 r
,就相当于寻找长度为奇数的回文串;如果输入相邻的 l
和 r
,就相当于寻找长度为偶数的回文串。综上所述,总体思路如下:
- 找到以
s[i]
为中心的回文子串 - 找到以
s[i]
和s[i+1]
为中心的回文子串 - 更新答案
则整个题目的代码实现如下:
class Solution {
public:
string longestPalindrome(string s) {
string res = "";
for (int i = 0; i < s.length(); i++) {
// 以 s[i] 为中心的最长回文子串
string s1 = palindrome(s, i, i);
// 以 s[i] 和 s[i+1] 为中心的最长回文子串
string s2 = palindrome(s, i, i + 1);
res = res.length() > s1.length() ? res : s1;
res = res.length() > s2.length() ? res : s2;
}
eturn res;
}
string palindrome(string s, int l, int r) {
// 防止索引越界
while (l >= 0 && r < s.length()
&& s[l] == s[r]) {
// 双指针,向两边展开
l--; r++;
}
// 返回以 s[l] 和 s[r] 为中心的最长回文串
return s.substr(l + 1, r - l - 1);
}
};
问题的解决
滑动窗口算法主要用于解决子数组问题,比如寻找符合某个条件的最长或最短子数组。
所谓滑动窗口,其实是维护一个可以扩大和缩小的、囊括数据的“窗口”,不断滑动,从而更新答案,获得结果。利用这一特性,对于#209题的求给定数组中满足其总和大于等于 target
的长度最小的子数组的长度,我们可以得出以下解题思路:
- 使用左右指针
left
和right
,left
和right
之间的长度即为滑动窗口的大小(即连续数组的大小)。 - 如果滑动窗口内的值
sum
≥ \geq ≥target
, 维护连续数组的最短长度,left
向右移动,缩小滑动窗口。 - 如果滑动窗口内的值
sum
< \ < <target
,则right
向右移动,扩大滑动窗口。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, right = 0, sum = 0;
int min_length = INT_MAX;
for (; right < nums.size(); right++) { //也可使用 while 循环并自增 right
sum += nums[right];
while (sum >= target)
{
//取之前窗口长度与当前窗口长度最短的
min_length = min(min_length, right - left + 1);
//去掉左侧的数
sum -= nums[left];
//缩小窗口
left++;
}
}
return min_length == INT_MAX ? 0 : min_length;
}
};
深入“滑动窗口”:如何聪明地穷举
该算法的大致逻辑如下:
int left = 0, right = 0;
while (right < nums.size()) {
// 增大窗口
window.add(nums[right]);
right++;
while (/*window needs shrink*/) {
// 缩小窗口
window.remove(nums[left]);
left++;
}
}
基于此种逻辑,指针 left
和 right
不会回退(它们的值只增不减),字符串或数组中的每个元素都只会进入窗口一次,随后被移除窗口一次,不存在某些元素多次进出窗口,故算法的时间复杂度就和数据长度成正比,为
O
(
n
)
\ O(n)
O(n)。值得注意的是,滑动窗口并不能穷举出所有子串(要想如此,只能使用嵌套 for 循环),但是对于该算法所适用的情景下,不需要进行穷举就能得出答案。滑动窗口就是这样一个算法模板,对枚举过程剪枝优化,避免冗杂的计算。
@labuladong 大佬提供了一份实用的、可替换的代码模板:
/* 滑动窗口算法框架 */
void slidingWindow(string s) {
// 用合适的数据结构记录窗口中的数据,根据具体场景变通
// 比如说,我想记录窗口中元素出现的次数,就用 map
// 我想记录窗口中的元素和,就用 int
unordered_map<char, int> window;
int left = 0, right = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
window.add(c);
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
// 注意在最终的解法代码中不要 print
// 因为 IO 操作很耗时,可能导致超时
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (left < right && window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
window.remove(d)
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
框架中两处
...
表示的是更新窗口的地方,分别是扩大和缩小窗口的更新数据操作,在具体题目中则体现为特定的代码逻辑。
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
对此题应用滑动窗口算法,思路如下:
- 在字符串
s
中使用左右指针技巧,初始化left = right = 0
,把索引左闭右开区间(便于边界处理)[left, right)
称为一个窗口。 - 不断增加
right
指针扩大[left, right)
窗口,直到窗口内的字符串包含了字符串t
的全部字符。 - 停止增加
right
,转而不断增加left
以缩小窗口[left, right)
,直到不再符合要求;同时,每次增加left
,都要更新一轮结果。 - 重复第 2 步和第 3 步,直到
right
指针到达字符串s
的尽头。
简而言之,这一思路的第 2 步在寻找一个可行解,而第 3 步则在优化这一可行解,二者相辅相成,最终拟合出最优解。
现在我们来运用先前介绍的代码框架:
首先,初始化两个哈希表 window
和 need
,分别记录窗口中的字符以及需要凑齐的字符:
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
其次,进行左右指针初始化:
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// 开始滑动
}
其中 valid
表示窗口中满足 need
条件的字符个数,一旦 valid == need.size
成立,则说明窗口已完全覆盖字符串 t
。
根据上述思路,如果一个字符进入或移出窗口,应该增加或减少 window
计数器;当 valid
满足 need
时应该收缩窗口,同时更新结果。对照着模板,总的代码实现如下:
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> window, need;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
int start = 0, len = INT_MAX;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
//扩大窗口
right++;
//进行窗口内数据的一系列更新
if (need.count(c))
{
window[c]++;
if (window[c] == need[c]) valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
//更新最小覆盖子串
if (right - left < len)
{
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s[left];
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
if (need.count(d))
{
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
};
利用这个模板,许多相似的题目都可以做出来。
LeetCode #977:Squares of a Sorted Array 有序数组的平方
这也是左右指针技巧的简单应用。我们可以利用原数组的有序性来减少时间复杂度,平方后的极值必然出现在数组两端,同时考虑负数的情况,绝对值可以更准确地反映平方后两端的大小。因此,可以用 2 个指针分别指向原数组的首尾,分别计算平方值,比较两者大小,较大的放在新数组尾端,随后较大的数所对应的指针移动,直至两个指针相撞,平方后的排序就完成了。
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
vector<int> squared(nums.size());
int site = nums.size() - 1;
while (left <= right)
{
if (nums[left] * nums[left] < nums[right] * nums[right])
{
squared[site] = nums[right] * nums[right];
right--;
}
else {
squared[site] = nums[left] * nums[left];
left++;
}
site--;
}
return squared;
}
};
@halfrost 大神也提供了其他解法,利用 sort()
内置函数:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
vector<int> squared(nums.size());
for (int i = 0; i < nums.size(); ++i) {
squared[i] = nums[i] * nums[i]; // 计算平方
}
sort(squared.begin(), squared.end()); // 对平方后的结果进行排序
return squared;
}
};