双指针
1、移动零

示例如下:

1.1、题目理解
很明显,题目要求我们将数组分成两部分:
- 前面一部分为非0数,并且顺序不可改变
- 后面一部分全为0
1.2、原理分析
像这样类似数组分块 的问题,我们可以采用双指针的方法。
我们定义:
- int dest = -1
- int cur = 0
dest,全称destination,有目的地、终点的意思。我们这里让dest指向下标-1,意在使dest一直指向已排好的非0数序列的末端。
cur,全称current,意为:当前的、现在的。cur的作用是遍历数组。
cur遍历过程中:
- 遇到下标对应值为0,什么也不做
- 遇到下标对应值非0,dest++,dest与cur对应值交换
由于我们用的是for循环,cur的前进就不需要另外写。
我们以数组[0, 1, 0, 3, 12]为例,演示一遍移动的过程:

1.3、代码演示
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
for (int dest = -1, cur = 0; cur < nums.size(); ++cur)
if (nums[cur] != 0)
swap(nums[cur], nums[++dest]);
}
};
2、复写零

示例如下:

2.1、题目理解
遇到0就添加一个0,所以数组[1, 0, 2, 3, 0, 4, 5, 0]应该变成:
1, 0, 0, 2, 3, 0, 0, 4, 5, 0, 0
去除越界的部分,就得到了:
1, 0, 0, 2, 3, 0, 0, 4
2.2、原理分析
我们依旧使用双指针。
以数组[1, 0, 2, 3, 0, 4, 5, 0]为例。
从前往后,行不行呢?

图示过程中,我们使dest = 0,cur = 0,然后cur遍历整个数组,遇到非0数,dest++;遇到0,dest位置置0,dest--,dest位置再置0,dest--。
接着我们就会发现,cur指向第二个"0",对dest操作完后,元素"2"被覆盖掉了。所以从前往后不可行。
那从后往前呢?
我们使dest指向数组末尾,cur指向复写后数组的最后一个元素。cur开始向前遍历,遇到非0,cur指向数赋值给dest指向位置;遇到0,destdest位置置0,dest--,dest位置再置0,dest--。

就可以了。
现在的问题是:我们如何找到正确的cur,也就是复写0后数组的最后一个元素,在原来元素的位置?
我们还是可以使用双指针。
我们定义dest = -1,cur = 0,此时cur遍历数组,dest指向的是:复写操作调整完 之后的部分数组的末位。
当cur指向非0,dest前进一位;当cur指向0,dest前进两位。
当dest指向原数组末位,结束。

但是还有一种情况:dest会越界。
比如数组[1, 0, 2, 3, 0, 4]:

越界时,dest一定等于arr.size(),此时cur一定指向了0。
这时,我们就要特殊处理:当dest == arr.size()时,数组末位置0,cur--,dest -= 2。
2.3、代码演示
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int dest = -1, cur = 0, n = arr.size();
// 1.找复写后数组的最后一个数
while (cur < n)
{
if (arr[cur] != 0) ++dest;
else dest += 2;
if (dest >= n - 1) break;
++cur;
}
// 2.特殊处理
if (dest == n)
{
arr[n - 1] = 0;
--cur;
dest -= 2;
}
// 3.复写数组
while (cur >= 0)
{
if (arr[cur] != 0) arr[dest--] = arr[cur];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
}
--cur;
}
}
};
3、快乐数


3.1、题目理解
以19为例,画出19是快乐数的推导过程:

以2为例,画出2不是快乐数的推导过程:

3.2、原理分析
假设对的数的操作(每一次将该数替换为它每个位置上的数字的平方和)记为 f (操作)。
乍一看,快乐数与非快乐数之间没有任何联系。但是我们从循环的角度看,就可以把快乐数的推导过程与非快乐数的推导过程统一起来了:
- 对于数a,当进行次若干f操作后,数a最终变为1,此时a无论经过多少次f操作,结果都是1。此时a为快乐数。
- 对于数b,当进行次若干f操作后,数a最终变为非1的数c,此时b无论经过多少次f操作,结果都是c。此时b不是快乐数。
这时,我们就可以借鉴快慢指针的做法。
3.3、代码演示
cpp
class Solution {
public:
//平方和操作另外封装
int Func(int n)
{
int happy = 0;
while (n)
{
happy += (n%10)*(n%10);
n /= 10;
}
return happy;
}
bool isHappy(int n) {
//如果slow和fast都为n,那么循环就进不去
int slow = n, fast = Func(Func(n));
while (slow != fast)
{
slow = Func(slow);
fast = Func(Func(fast));
}
return fast == 1;
}
};
4、盛最多水的容器


4.1、题目理解
我们以例一为例。
柱状图是以数组每一位数字的下标为横坐标,每一位上的数字值为纵坐标:

每两根柱子构成一个容器。由木桶效应,得较短的柱子决定了盛水的多少,而柱子的高度就是数组对应位置上的值。
设数组名为height,选取的数组两个位置上的值,左边的值下标为left,右边的下标为right。那么底L为:(right - left),高h就是较小数。
则最多盛水的计算公式:
最多盛水 = ( r i g h t − l e f t ) ∗ m i n ( h e i g h t [ l e f t ] , h e i g h t [ r i g h t ] ) 最多盛水=(right - left)*min(height[left], height[right]) 最多盛水=(right−left)∗min(height[left],height[right])
如上图,选取数组1位置下的值8,和8位置下的值7,则最多盛水:(8 - 1)*7 = 49。
4.2、原理分析
我们可以直接采取暴力解法,将数组能生成的所有容器都列举一遍,然后算出所有的容量,再选出最大的一个。但是此时的时间复杂度为O(N^2),提交时可能会超时。
我们可以这样思考:
我们取出部分数组:[6, 2, 5, 4],令left指向最左边的6,right指向最右边的4。此时底L为3,高h为4。
假设我们令left向前,也就是抛弃6,同时right不动。此时left指向2。那么这时left的移动过程中,L和H的变化情况为:
- 底L一直减小
- 高h在left的移动过程中,可能遇到大于4的值,此时h不变;也可能遇到小于4的值,此时h减小
由公式容量V = L * h,left舍弃了较大值6,接下来的left无论怎么与right组合,结果都减小。
4.3、代码演示
由此我们就可以设计代码:
定义双指针left指向数组首位、right指向数组末位。计算出当前容量。然后选出较小数,对应指针往里靠近,再计算容量。重复操作。
cpp
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1, V = 0;
while (left < right)
{
V = max(V, (right-left)*min(height[left], height[right]));
if (height[left] < height[right]) ++left;
else --right;
}
return V;
}
};
5、有效三角形的个数

5.1、题目理解
比如数组[2①, 2②, 3, 4],有以下组合:

其中不满足的,只有2, 2, 4,所以有效的三元组是3个。
比如数组[4, 2, 3, 4],有以下组合:

其中所有组合都是有效的,所以有效的三元组是4个。
5.2、原理分析
首先,我们对判断三个数是否构成三角形三条边的依据作一个优化:
我们之前判断三个数a, b, c是否构成三角形三条边,依据是:
a + b > c a+b>c a+b>c
a + c > b a+c>b a+c>b
b + c > a b+c>a b+c>a三个条件同时满足。
但其实,我们对a, b, c三个数进行排升序,得到新序列x, y, z(x <= y <= z)。
只要满足两个较小数x+y > z,那么a, b, c(x, y, z)三个数一定能构成三角形。
所以,我们先对给出序列,排成升序。
接着,使用双指针法。我们以序列[2, 2, 3, 4, 5, 9, 10]为例演示过程:
定义三个下标:
- i从数组末位开始(直到下标2位置结束,因为是三元数),遍历数组
- left起始指向0位置,right指向i的前一个位置

此时2 + 9 = 11 > 10。由于数组是递增的,此时left从0位置开始,一直到right的前一位数中,每一个数与right指向数相加,结果都大于10,都满足构成三角形的条件。
那么这时,我们统计到有效三角形个数为:right - left = 5 - 0 = 5。
然后,--right。

此时2 + 5 = 7 < 10,不构成有效三角形。我们就让left++,left不能与right相遇。此此时有两种情况:
- 如果arr[left] + arr[right]还是小于arr[i],只能再left++,看看有没有加和大于arr[i]的情况
- 如果arr[left] + arr[right]大于arr[i],就先统计有效三角形个数,再--right
此时,我们可以总结方案:
- 外层循环中,i遍历数组
- 内层循环中,left起始位置0,right起始位置i - 1,left不能与right相遇:
- 如果arr[left] + arr[right] > arr[i] ,统计个数:right - left,然后--right
- 如果arr[left] + arr[right] < arr[i] ,left++
5.3、代码演示
cpp
class Solution {
public:
int triangleNumber(vector<int>& nums) {
// 1.排序
sort(nums.begin(), nums.end());
// 2.统计
int ret = 0, n = nums.size();
for (int i = n - 1; i >= 2; --i)
{
int left = 0, right = i - 1;
while (left < right)
{
if (nums[left] + nums[right] <= nums[i]) ++left;
else ret += right - left, --right;//left后逗号不可改为分号,否则分支终止
}
}
return ret;
}
};
6、两数之和

cpp
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
int n = price.size(), left = 0, right = n - 1;
while (left < right)
{
int sum = price[left] + price[right];
if (sum < target) ++left;
else if (sum > target) --right;
else
{
return { price[left], price[right] };
}
}
return {-1, -1};
}
};
7、三数之和


7.1、题目理解
怎么理解不重复?
以数组[-1, 0, 1, 2, -1, -4]为例。
前一个-1组成的序列[-1, 0, 1]符合三数和为零,后一个-1组成的序列[-1, 0, 1]也符合三数和为零。
所以,我们有一个去重的需求。
7.2、原理分析
我们可以先排序。
那么这时,我们可以直接枚举出所有的三元组,然后使用库函数set去重。
但是,我们可以做更进一步的优化:
-
排完序之后,我们用下标i固定一个数a。
-
然后在剩下的序列[i+1, size-1]中,定义下标left, right。然后寻找left与right指向数的和是否等于a的相反数。此时问题就变为了两数之和的问题。
但是,有一些细节需要处理:
- 当i移动到a大于0之后,由于此时数组升序,之后就找不到两个数,和为a的相反数。此时的i就无需讨论。
- 不漏
- 之前两数之和只需要我们返回一种情况即可。而本题需要我们找到所有情况,所以找到一种情况后,left, right应继续向里靠近,继续寻找。
- 去重
- 找到一种情况后,我们假设此时 left 指向 x, right 指向 y 。当left, right向里靠近时,由于此时数组升序,可能指向相同的 x, y 。这时我们应该再让left, right向里靠近。
- 当前指向a的i如果找到了符合的情况,向前遍历时可能也会找到相同的 a ,这时i也要跳过。
- 防止越界
- 如果序列为[0, 0, 0],去重过程中left可能越界,应注意控制
去重处理的辅助思考:

7.3、代码演示
参考代码:
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ret;
//排序
sort(nums.begin(), nums.end());
int n = nums.size();
for (int i = 0; i < n; )//固定a
{
//升序,a>0,a之后就肯定没有两数之和为-a
if (nums[i] > 0) break;
int left = i+1, right = n-1, target = -nums[i];
while (left < right)
{
int sum = nums[left]+nums[right];
//两数之和的思路
if (sum > target) --right;
else if (sum < target) ++left;
else
{
ret.push_back({nums[left], nums[right], nums[i]});
++left, --right;
//去重
while (left < right && nums[left] == nums[left-1]) ++left;
// 防止越界
while (left < right && nums[right] == nums[right+1]) --right;
}
}
//另外对i操作
++i;// 防止越界
while (i < n &&nums[i] == nums[i-1]) ++i;
}
return ret;
}
};
//我自己思路的小问题:找到情况后,left, right是同时向内一步的
我想出的代码:
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 1.排序
// 2.固定一个数a
// 3.在该数后面的区间内,利用 "双指针算法" 快速找到两个和为 -a 的数
// 细节
// 1.不漏
// 找到一种结果之后,不要停,left与right继续靠近,继续寻找
// 2.去重
// 找到一种结果后,由于数组有序,相同的值紧靠在边上,left与right要跳过当前结果对应的相同的值
// 找到一种结果后,i也要跳过相同值
// 3.防止越界
//left,right容易越界,要确定范围
sort(nums.begin(), nums.end());// 1.排序
vector<vector<int>> vv;
int n = nums.size();
for (int i = 0; i < n-2; ++i)// 2.固定一个数a
{
if (nums[i] > 0) break;//a>0时的优化
if (i != 0 && nums[i] == nums[i-1]) continue;//对固定数a的去重
int left = i + 1, right = n-1;
while (left < right)
{
if(left != i + 1 && right != n-1 && nums[left] == nums[left-1] && nums[right] == nums[right+1])
{//对left和right指向数的去重
++left; --right;
continue;
}
int sum = nums[left] + nums[right], target = -nums[i];
if(sum > target) --right;
else if(sum < target) ++left;
else
{
vv.push_back({nums[left],nums[right],nums[i]});
++left; --right;//继续寻找
}
}
}
return vv;
}
};
8、四数之和

8.1、题目理解
与"三数之和"类似。
我们选出的每一个四元组,其中每一个数必须来自不同的位置;并且这四个数的不同组合,都算一个四元组。
8.2、原理分析
与"三数之和"类似。
- 首先排成升序数组
- 其次,从左往右,固定数a
- 接着,在数a之后的序列中,固定数b
- 在数b之后的序列中,起始时定义一首一尾下标left, right
- 找到四数之和小于target,left++
- 找到四数之和大于target,right--
- 等于
- 入序列
- ++left, --right,这是为了不漏
去重
- a要去重
- b要去重
- left, right要去重
8.3、代码演示
cpp
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
sort(nums.begin(), nums.end());// 排序
int n = nums.size();
//for (int i = 0; i < n - 3; )
for (int i = 0; i < n; )//其实这里不需要特殊处理,因为超过了,操作left, right的循环就进不去了
{// 固定a
for (int j = i+1; j < n; )//这里也一样
{// 固定b
long long sum_i_j = nums[i] + nums[j];
int left = j+1, right = n-1;
while (left < right)
{// long long防止数据溢出
long long sum_left_right = nums[left] + nums[right];
if (sum_i_j + sum_left_right < target) ++left;
else if (sum_i_j + sum_left_right > target) --right;
else
{// 优化
ret.push_back({nums[left--], nums[right--], nums[i], nums[j]});
// 对left, right去重
while (left < right && nums[left] == nums[left-1]) ++left;
while (left < right && nums[right] == nums[right+1]) --right;
}
}
++j;// 对b去重
while (j < n - 2 && nums[j] == nums[j-1]) ++j;
}
++i;// 对a去重
while (i < n - 3 && nums[i] == nums[i-1]) ++i;
}
return ret;
}
};