注:本文中均用下标指指针。
一.移动0核心算法
遇到0,cur++,des不动,严格划分三个区间。遇到非0,des+1与cur元素互换,然后
des++,cur++


class Solution {
public:
void moveZeroes(vector<int>& nums) {
for(int cur = 0,des = -1;cur < nums.size();cur++)
{
if(nums[cur])
{
swap(nums[cur],nums[++des]);//小巧思,既让des+1与cur互换了,也让des++了
}
}
}
};
二.复写0核心算法
https://leetcode.cn/problems/duplicate-zeros
https://leetcode.cn/problems/duplicate-zeros
算法一.异地操作

因为题目要求,算法一无法使用,我们优化一下,将"异地"改"就地"。
算法二.就地操作


会有des出界的特殊情况,因此判断条件是des >= n - 1,而非==n - 1。

在复写时由于从前往后会把数组元素覆盖掉,因此选择从后往前。
三.快慢指针
1.满足循环链表的条件
在解题前,我们来探讨一下,一个怎样的链表才能被称作循环链表?
此时,先引入这个板块的核心------快慢双指针。定义快指针为fast,慢指针为slow;快指针在链表上一次走两步,慢指针一次走一步;慢指针初始指向链表第一个元素,快指针指向第二个。
先举个跑步的例子:假如两个人在一条跑道上并排同向奔跑,一个快一些,一个慢一些,假如永远跑下去,都不会相遇,那么这条跑道就不是成环封闭的。如果相遇了,就代表快的人到后面套了慢的人一圈,则说明跑道是成环封闭的。
那么回到指针与链表上,两个指针不断在链表上移动,当指针相遇,也就是指向同一位置时,就说明这个链表是循环链表。
那这个时候就会有人问了,假如指针一前一后,永远保持这个状态,那么即便是循环链表,是不是也不会相遇,永远一前一后?虽然一前一后,但快慢两个指针移动速度不同,所以这种想法错了。
2.题解思路
①.大体分析
注意:下面提到的指针并非那个指向什么区域的ptr,我们创建一个int变量,将每次bitsum(实现取位,开平方,并求和的函数) 后的返回值赋值给该变量,就模拟出指针的效果。同样,提到的循环链表也并非实际存在的链表,是一种对数字每一次拆解,开平方,求和后的结果所串起的流程,进行逻辑上的模拟。

根据题设可知,所有情况都是建立在链表是循环的基础之上的,在此之上,分成两种情况:

总结一下就是:

循环体内所有数为1,则满足快乐数;非1则不是。
②.代码实现的角度分析
那么将以上思想转化为代码,该如何考虑呢?
首先定义一个bitsum 函数,用于取位,开平方,并求和。
既然题设情况全是循环链表,但满足条件的链表,循环体元素都为1,我们就创建快慢指针去模拟出的链表上遍历,快指针在链表上一次走两步,慢指针一次走一步;慢指针初始指向链表第一个元素(n),快指针指向第二个(如果快慢指针均为n,也就是均指向首元素的话,是进不了while循环的)。当slow = fast = 1时,就说明是快乐数。

class Solution {
public:
int bitsum(int n)
{//执行每位求平方,并求和的操作。
int sum = 0;
int bit = 0;
while(n)
{
bit = n % 10;
sum += bit * bit;
n /= 10;
}
return sum;
}
bool isHappy(int n)
{
int slow = n,fast = bitsum(n);
while(slow != fast)
{//slow挪一步,fast挪两步
slow = bitsum(slow);
fast = bitsum(bitsum(fast));//小巧思
}
//出循环则双指针相遇,判断下相遇值是否为1,为1就快乐
return slow == 1;
}
};
四.盛最多水的容器(根据单调性小巧思解题)
1.单调性小巧思
整个集合:

我们截取整个集合的一个区间来研究。

以高度小的那个边为基准,从大的那个边开始,去向内(向小这边 )枚举,高度一直是小高度(不变)或者小于小高度 ,宽度 在向内过程中不断减小 ,那么所得的体积也必然在不断减小。
所以区间内V最大的时候就是V初始的时候 (区间左边界(高为6)和右边界(高为4)围成容器的时候)
2.解题思路
①.大体思路
先从最左和最右边界开始,不断的定区间,同时求区间里的Vmax(左右边界宽度*小的高度,既区间中V的初始值 ),并舍掉区间中高度小(数组中数值小)的那一边。 毕竟区间里不断向小(高度)的那一边去枚举,求V,上面的规律告诉我们,这必定是个V减小的过程,那么V肯定比不过Vmax。既然比不过,干嘛去劳神费力的逐个枚举,干脆在初始时求个Vmax,然后干掉当下区间中高度小的那个边界,停滞枚举。在干掉小(高度)边界以后,继续找新区间,继续求区间的Vmax,继续干当下区间的小(高度)边界,就这么循环往复,求出各个区间的Vmax,再把这些个Vmax拿来比较,看谁是最大的。

②.代码角度分析
Ⅰ.复杂度
根据上面建区间的过程,我们知道需要两个指针去指向左右边界,并移动,所以仍旧要用到双指针。两个指针于两侧不断向中间遍历,在完成分所有区间,去小高度边界后,两个指针才完整的走了数组一遍 ,那么复杂度比起暴力枚举是很低的,才O(N)。
Ⅱ.实现思路
数组储存边界高度,规定左右指针谁数值小,谁移动(干小高度边界)。先计算区间最大容积,每计算完一个容积,就去与之前区间的最大容积比较一下,然后更新一下容积,再移动指针。反复求容积,移动,直至俩指针相遇。

注:要记得min,max接口,不然写容积更新的时候着实坐牢,还有求vector的大小要用size()接口,不要sizeof!!!!!!!!
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0;
int right = height.size() - 1;
int V_max = 0;
int V = 0;
while(left < right)
{
V = min(height[left],height[right]) * (right - left);
//更新容积
V_max = max(V_max,V);
//移动指针
if(height[left] <= height[right])
{
++left;
}
else
{
--right;
}
}
return V_max;
}
};
五.有效三角形个数
https://leetcode.cn/problems/valid-triangle-number
https://leetcode.cn/problems/valid-triangle-number
解法1.暴力枚举
满足三角形的条件不多说了吧:

根据这个来写个伪代码,看看开销:

可以看到,三层循环嵌套,效率也是低到了姥姥家:O(N^3)。
让我们优化一下:
解法2.利用单调性,使用双指针
①.排序小巧思
对三边排好序,再判断

这时候边c最大,那么任意一个边加它都必将大于第三条边 ,不管三边关系满不满足三角形,所以下面两个条件不需要去判断,只需判断a+b > c这个条件即可。
②.实现思路
先对整个边长序列进行排序(升序) ,然后挑出最右(最大)的数,固定,作为c。然后开始枚举剩下的a,b两数,left和right指向的数作a,b。当然,枚举仍旧不能胡乱枚举,不然开销又上去了。要将left指向剩下序列最左(最小),right指向最右(最大),此时就又能发现一个规律:

将b固定,a向右枚举,既然整个序列最小值2 + b都能大于c了,那么后面这几个数+b也必然大于c,那么left和right间(包含left)枚举的总次数就等于right - left,就可以用差值代替逐个枚举求总次数,节省开销。 得到这个总次数,9就没有利用价值了,就可以给它干掉,让right--

这时候right指向5,既然2倍的b(b为当下序列最大)都跟c一样大(没法形成三角形),那么a,b间(包括a)这几个比b小的数+b肯定比c小,就是说这一序列的所有枚举情况都没法形成三角形,就没必要一个个枚举,直接干掉2,让left++

总结一下就是:
注:a是指当下区间最左,b是最右

当left与right相遇时,一轮的遍历结束,然后让c--,固定,然后重复以上操作,直至c到整个数组最左。
class Solution {
public:
int triangleNumber(vector<int>& nums) {
//l.排序
sort(nums.begin(),nums.end());
//2.固定+统计枚举次数
int n = nums.size();
int ret = 0;
for(int i = n - 1;i >= 0;i--)
{
int left = 0;
int right = i - 1;
while(left < right)
{
if(nums[left] + nums[right] > nums[i])
{
ret += (right - left);
--right;
}
else
{
++left;
}
}
}
return ret;
}
};
3.开销对比

六.和为s的两个数字(七的pioneer)
由于题目被力扣删了,所以这题没链接

解法1.暴力枚举
固定一个数,让这个数和数组里剩余的数依次匹配,看是否等于target;枚举完一轮,固定的指针++,然后接着固定,接着向后匹配,反复操作,直至枚举完所有的数。

伪代码:

开销为**O(N^2)。**依旧慢到了姥姥家。
解法2.依旧利用单调性,使用双指针

这里利用了序列升序的特性,在最左(最小)和最右(最大)定义了指针left和right,left指向的数代表固定的数,right指向的数代表当下区间最大的数 ,先让left指向的数去+right指向的数,代表固定数去加当下区间最大数,如果和小于t,那么说明连最大数加固定数都搞不定,就没必要去浪费时间枚举了,就把这个固定数干掉,即left++。假如和>t,就说明当下区间中有枚举值可以满足,但right指向的数与left指向的数之和超过t了,我们需要找一个比right指向的值更小的值去使得和等于t,就把right干掉,即right--。
总结一下:

最坏情况left和right走到序列正中,开销为O(N)。
代码:

七.三数之和
https://leetcode.cn/problems/3sum
https://leetcode.cn/problems/3sum
注意:不包含重复三元组的意义:

解法一.暴力枚举
1.排序
先对整个序列进行升序排序

2.枚举
从左到右依次枚举出满足条件的三元组

3.去重
将重复的几个三元组放进set容器 里,就可以做到去重,详情参见这篇文章一.1节

解法二.对于有序的序列,要想起双指针
注:此处序列换了一个更具代表性的。
1.排序

2.双指针
①.算法讲解
先固定上一个数a,然后在除这个数以外的后面区间内,再找两个数,使得这两个数之和等于-a

在后面区间找两个数之和就是第六题的解法了。

找到所有元组后,进行去重操作,但解法一的去重操作会让你跟offer无缘,因此下面详细讲解更优质的操作。
②.细节把控
Ⅰ.不漏
由于这里的双指针要找到所有满足的元组 ,因此在找到一组以后,不能直接返回,需要找下一组,缩小区间(right++,left--) ,然后在缩小的区间里接着寻找满足条件的元组,但此时需要注意重复的问题。
Ⅱ.更优质的去重

i跳过重复:

++第二个-4所包含的所有情况必然是第一个-4情况的子集,所以可以无需枚举,直接跳过。++
Ⅲ.关于越界

当固定i后,l会一直向后移动,以达到去重,最后越界。
此处结合下面代码实现部分进行讲解。
Ⅳ.关于固定数a的优化
倘若a>0,那么a后面区间里的数必然全部大于0 ,因此后面区间里所有数,都不可能与a相加得0, 所以只需要考虑a<=0的情况 ,所以a <= 0。
3.基于双指针算法的代码实现
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
//装输出结果的容器
vector<vector<int>> ret;
//1.排序
sort(nums.begin(),nums.end());
//2.双指针
int n = nums.size();
for(int i = 0; i < n;)//固定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//满足和为0
{
ret.push_back({nums[i],nums[left],nums[right]});
left++,right--;
//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 - 1] == nums[i]) ++i;
}
return ret;
}
};
处理left和right越界
我们就添加一个限定条件:
++left < right++
因为区间是left到right,left向后移动,出了right就越界;同时right往前移动,也不能越过left。
left和right去重

i去重与防止越界
1.i要确保<n。
2.将for循环里的i++挪到循环内部,不然去重后再进for循环时,会多++一次,导致有个i被落下。

八.四数之和
解法一.暴力枚举
跟第七题的暴力枚举一样,依旧先排序,再暴力枚举,最后将满足条件的元组,装入set去重。
这里会出现四层循环,从而超时,因此还得用解法二双指针进行优化。
解法二.双指针(仍旧排序+双指针)
步骤1.排序
步骤2.双指针
①.算法讲解

可以先依次固定一个a,随后在后面区间里,利用第七题的三数之和,找到三个数,使得和为target - a。 三数之和就可以变成在区间里,再依次固定一个数b,再在区间里再找一个区间,利用第六题二数之和,使得和为 target - a - b。
②.细节把控
Ⅰ.不漏
仍旧找到目标元组后不停止,缩小区间,left++,right--,继续在新区间寻找符合条件的元组。
Ⅱ.不重
要保证三个不重,最内层区间的left和right的不重,次外层区间b的不重,以及最外层区间a的不重。

代码编写所遇问题

此处出现了数据溢出,也就是输入数据过大的问题,需要把数据类型转成longlong。

完整代码
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
//排序
sort(nums.begin(),nums.end());
//nums[i]为a,nums[i + 1]为b
int n = nums.size();
for(int i = 0;i < n;)//固定a
{
for(int j = i + 1;j < n;)//固定b
{//双指针
int left = j + 1,right = n - 1;
long long sum = (long long)target - (long long)nums[i] - (long long)nums[j];
while(left < right)
{
if(left < right && nums[left] + nums[right] > sum) right--;
else if(left < right && nums[left] + nums[right] < sum) left++;
else
{//找到了符合条件的元组,去重。
ret.push_back({nums[i],nums[j],nums[left],nums[right]});
left++,right--;
//去重left和right
while(left < right && nums[left] == nums[left - 1]) left++;
while(left < right && nums[right] == nums[right + 1]) right--;
}
}
//去重b(nums[j])
j++;
while(j < n && nums[j] == nums[j - 1]) j++;
}
//去重a(nums[i])
i++;
while(i < n && nums[i] == nums[i - 1]) i++;
}
return ret;
}
};