
忧郁兔斯基:个人主页****
个人专栏:《从零开始的C++学习之旅》** 《C语言》 《数据结构》 《Linux学习》《优选算法》**
励志名言:道阻且长,行则将至
个人介绍:
本文是博主开设的《优选算法》专栏的第一篇文章,本专栏会介绍一些常用的算法技巧,并利用这些技巧解决多个例题。一个专题会分成上下两文,一篇文章会
每个例题的讲解会分成三个部分:1、题目分析 2、算法原理 3、代码实现 最后希望各位观众佬爷看完能有所收获。
目录
[2.1 题目分析](#2.1 题目分析)
[2.2 算法原理](#2.2 算法原理)
[2.3 代码实现](#2.3 代码实现)
[3.1 题目分析](#3.1 题目分析)
[3.2 算法原理](#3.2 算法原理)
[3.3 代码实现](#3.3 代码实现)
[4.1 题目分析](#4.1 题目分析)
[4.2 算法原理](#4.2 算法原理)
[4.3 代码实现](#4.3 代码实现)
[5.1 题目分析](#5.1 题目分析)
[5.2 算法原理](#5.2 算法原理)
[5.3 代码实现](#5.3 代码实现)
一、双指针
误区:"双指针"中的指针并不是我们在C/C++中学到的指针,这里更多是指功能上的相似,都是用于定位元素的。像我们在数组中用下标来进行定位等思想。
常见的双指针有两种形式,⼀种是对撞指针 ,⼀种是左右指针。
对撞指针:⼀般用于顺序结构中,也称左右指针。
对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼 近。
对撞指针的终止条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是: left == right (两个指针指向同⼀个位置) 或者 left > right (两个指针错开)
快慢指针:又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。
这种方法对于处理环形链表或数组 非常有用。 其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现方式有很多种,最常用的⼀种就是:
在一次循环中,每次让慢的指针向后移动⼀位,而快的指针往后移动两位,实现⼀快⼀慢。
二、移动零
2.1 题目分析

题目要求:
1、将数组中非零元素在不改变相对顺序的情况下左移,零元素全部右移
2、不能额外创建数组
2.2 算法原理
这道题目问题在与不能额外创建数组,不然我们直接创建一个数组,然后对原数组进行遍历,将非零元素全部按顺序移到新数组中。
那么这种对数组进行原地操作,数组分区的问题,它的特点:
给定一个数组根据题目给出的规则,将其划分为多个区间,本题是将所有的非零的元素移到左边,零元素移到右边
针对这种问题,我们采用双指针算法,这里的指针是利用数组的下标来充当指针
我们设置的两个指针如下:
dest (destination):已处理的区间内,非零元素的最后一个位置(在刚开始,因为我们还没对数组进行操作,所以我们将其指向下标为-1的位置)
cur(current):从左往右扫描,遍历数组,会将整个数组划分成两个区间,左边是已处理区间,右边是待处理区间(设置为0,遍历整个数组)
根据题目要求,处理后的区间需要左边为非零元素右边为零元素,dest作为分割线,结构如下:
0,dest\] \[dest+1,cur-1\] \[cur,n-1
非零 零 待处理
对于该题目:
0 1 0 3 1 2
cur=0,dest=-1;
在cur从左往右扫描的过程中:如果碰到零元素,cur++;因为我们的目标是让 dest + 1, cur - 1 内的元素全都是零,因此当 cur 遇到 0 的时候,直接 ++ ,就可以让 0 在 cur - 1的位置上,从⽽在 dest + 1, cur - 1 内;
如果遇到非零元素,我们需要让dest++,然后交换两个指针对应的数,再让cur++ swap(arr++dest,arrcur;cur++;
因为 dest 指向的位置是非零元素区间的最后⼀个位置,如果扫描到⼀个新的⾮零元素,那么它的位置应该在 dest + 1 的位置上,因此 dest 先⾃增 1 ;
dest++ 之后,指向的元素就是 0 元素(因为非零元素区间末尾的后⼀个元素就是0 ),因此可以交换到 cur 所处的位置上,实现 0, dest 的元素全部都是非零元素, dest + 1, cur - 1 的元素全是零。
2.3 代码实现
cpp
class Solution
{
public:
void moveZeroes(vector<int>& nums)
{
int cur=0,dest=-1;
int size=nums.size();
while(size--)
{
if(nums[cur])
swap(nums[++dest],nums[cur]);
cur++;
}
}
};
通过截图:

三、复写零
3.1 题目分析

题目描述:
1、将数组中出现的零再复写一次,同时将其余元素右移
2、不创建数组,只能进行原地操作,不能越界
3.2 算法原理
我们上面已经说过,像是对数组进行原地操作的题目,我们优先考虑一下双指针。
本题我们先尝试创建一个数组,来模拟和熟悉一下复写零的操作:

此时,上一行的数组只遍历到4就结束了。
我们熟悉了操作后,设置两个指针:
cur:用于遍历数组,dest:指向已经处理的最后一个数字

我们发现,当我们从前向后完成复写操作时,dest在第二次就会超过cur,原数组的元素就被覆盖了,这必然会导致结果的错误。
那我们就需要从后向前完成复写操作,即我们第一次模拟的倒置操作。可以说是反向操作,但是我们需要让cur和dest指针指向最后一次复写的位置,所以我们需要先进行正向的变换,此次变换的目的是作位置变换,不需要考虑数值的变换,规则就是,dest<n-1的情况下,让cur遍历数组,当cur指向的是零,dest+=2,否则dest++;
但是需要注意的是:如果当dest==n-2,且cur此时指向的是0,那么就会导致dest的越界,所以我们在得到最终位置后,需要进行判断,如果dest=n,那么设置arrn-1=0,cur--,dest-=2;
得到最终位置后,我们只需要从后向前复写即可。
3.3 代码实现
cpp
class Solution
{
public:
void duplicateZeros(vector<int>& arr)
{
//进行位置变换,等到最后的位置
int cur=-1,dest=-1,n=arr.size();//dest是指向处理后的最后一个元素的位置
while(dest<n-1)//注意这里不能是等于,如果是等于就一定会越界的
{
if(!arr[++cur])//如果是0
dest+=2;
else
dest++;
}
//对越界的情况进行处理,即向前回推一步
if(dest==n)
{
arr[n-1]=0;
cur--,dest-=2;
}
//从后向前进行复写
while(cur>=0)
{
if(arr[cur])
{
arr[dest--]=arr[cur--];
}
else
{
arr[dest--]=0;
arr[dest--]=0;
cur--;
}
}
}
};
注意:cur以及dest需要保证是同步进行操作,所以我将cur设置为-1,如果是0,那么就会导致最后cur指向的是正确位置的下一个位置,那么结果就错误了
如果将第一步的代码替换成下面这样也可以:
cpp
// 1. 先找到最后⼀个数
int cur = 0, dest = -1, n = arr.size();
while(cur < n)
{
if(arr[cur]) dest++;
else dest += 2;
if(dest >= n - 1) break;
cur++;
}

四、快乐数
4.1 题目分析

题目分析:
将n替换成各个数位上的平方和,最终会有两个结果,1、变成1 2、无限循环 如果最终结果为1那么就是快乐数,否则就不是
重点:要么是最终变成1,要么就是无限循环
4.2 算法原理

我们把上面的结构画出来,很明显看出是一个带环的结构,我们说过,带环结构使用快慢指针有奇效,根据快慢指针相遇时指向的元素是否相等就可以确定是否是快乐数。
定义快慢指针 慢指针每次向后走1步,快指针每次向后走两步,判断相遇时候的值即可。
快慢指针相遇时,一定都在环上,如果此时指针的值为1,那么就是快乐数
为什么一定会循环呢?
经过⼀次变化之后的最大值 9^2 * 10 = 810 ( 2^31-1=2147483647 。选⼀个更大的最
⼤ 9999999999 ),也就是变化的区间在 1, 810 之间;
根据「鸽巢原理」,⼀个数变化 811 次之后,必然会形成⼀个循环;
其实就是说数的大小是有限的,必然会出现数的重复这样就会导致循环。
因此,变化的过程最终会⾛到⼀个圈⾥⾯,因此可以⽤「快慢指针」来解决。
4.3 代码实现
cpp
class Solution {
public:
int getNum(int n)
{
int sum=0;
while(n)
{
int tmp=n%10;//得n最后一个数位上的数
sum+=tmp*tmp;
n/=10;//最终n会变成0
}
return sum;
}
bool isHappy(int n)
{
int slow=n, fast=getNum(n);
while(slow!=fast)
{
slow=getNum(slow);
fast=getNum(getNum(fast));
}
return fast==1;
}
};

五、盛最多水的容器
5.1 题目分析
题目链接:11. 盛最多水的容器 - 力扣(LeetCode)

数组中的数字代表高度,数组中元素的间距代表宽度,求数组中存在的两个元素对应的宽度和高度的乘积最大值
5.2 算法原理
设两个指针 left , right 分别指向容器的左右两个端点,此时容器的容积 :
v = (right - left) * min( heightright, heightleft)
容器的左边界为 heightleft ,右边界为 heightright 。
为了方便叙述,我们假设「左边边界」小于「右边边界」。
如果此时我们固定⼀个边界,改变另⼀个边界,⽔的容积会有如下变化形式:
容器的宽度⼀定变⼩。
由于左边界较小,决定了水的⾼度。如果改变左边界,新的⽔⾯⾼度不确定,但是⼀定不会超
过右边的柱⼦⾼度,因此容器的容积可能会增⼤。
如果改变右边界,⽆论右边界移动到哪⾥,新的⽔⾯的⾼度⼀定不会超过左边界,也就是不会
超过现在的水面高度,但是由于容器的宽度减小,因此容器的容积⼀定会变⼩的。
由此可见,左边界和其余边界的组合情况都可以舍去。所以我们可以 left++ 跳过这个边界,继
续去判断下⼀个左右边界。
5.3 代码实现
cpp
class Solution {
public:
int maxArea(vector<int>& height)
{
int n=height.size();
int left=0,right=n-1;
int ret=0;
while(left<right)
{
int v=min(height[left],height[right])*(right-left);
ret=max(v,ret);
if(height[left]<height[right])
left++;
else right--;
}
return ret;
}
};

下一篇文章将会继续讲解双指针有关的四道例题