题目一:
算法原理:
首先我们可以对这道题目进行题目分类,像这种对数组以某种标准而进行一定的划分的题目,我们统称为数组分块问题,其中使用到的算法就是双指针算法,这里的指针并非真正int*这种,而是用数组下标来代替指针。
其中我们有两个指针,分别为cur和dest(cur是current的缩写,表示当前的,dest是destination的缩写,表示目的地),这两个指针的作用如下:
cur:从左到右扫描数组,遍历数组
dest:已处理的区间内,非零元素的最后一个位置
由上图我们可以看到,通过这两个指针,可以将该数组划分成三个区间,由cur指针划分已遍历和未遍历两个大区间,而在已遍历这个区间里,我们通过dest又细分了两个区间,左边的区间是符合某一条件的元素,右边的区间是不符合某一条件的元素,就如我们这道题,左边区间就是符合非零这一条件的元素,右边区间就是值为0的元素
而当我们cur遍历完整个数组,那么未遍历的区间就消失了,因此三个区间只剩下两个区间,这时我们也就完成了对这个数组的分块
那我们这道题,dest和cur分别初始值为-1和0,因为我们不确定是否需要划分,有可能该数组本身就符合分块标准,无需修改,所以dest设置为-1,而遍历数组是从下标0开始,所以cur设置为0
当cur去遍历数组时,一共有两种情况,非0和0,当0的时候,我们就直接cur++,不用其他操作,而当非0时,则将dest++,表示非零区间的元素个数加1,然后将dest和cur对应的值进行交换(此时dest对应的值是0这个区间的第一个元素)
直观来说如上图就是将dest+1和cur进行了交换,此时三个区间为[0,dest+1] [dest+2,cur] [cur+1,n-1]
最后当cur遍历完数组后,待处理的区间也就消失了,这时数组就只有两个区间,左边是非零,右边是0,完成了题目要求
代码1:
class Solution {
public void moveZeroes(int[] nums) {
for(int cur=0,dest=-1;cur<nums.length;cur++){
if(nums[cur]!=0){
//非零区间加1
dest++;
//交换
int tmp=nums[cur];
nums[cur]=nums[dest];
nums[dest]=tmp;
}
}
}
}
注:也可以有其他方法,比如这道题也可以通过i=0,j=0两个下标,i遍历数组,当i遍历到非零元素时,直接将j对应的元素覆盖,然后j++,最后遍历完后,又用j进行往后遍历数组,直接全部赋值0,这样也可以,但本质仍然是双指针,只不过一个是交换,一个是覆盖,因为这道题已经告诉其中一个区间全是0,有点特殊,就不需要保留原值,最后直接全部赋值0即可
代码2:
class Solution {
public void moveZeroes(int[] nums) {
int j=0;
//遍历数组
for(int i=0;i<nums.length;i++){
//将非零的值全部赋值到数组前面
if(nums[i]!=0){
nums[j++]=nums[i];
}
}
//用j继续往后遍历数组,全部赋值0
while(j<nums.length){
nums[j++]=0;
}
}
}
题目二:
算法原理:
像对数组的值进行移动的题目,也可以用到双指针算法。我们可以先创建一个新的数组来实现本题,因为使用两个数组这种比较暴力的方法通常都比在原数组上进行操作要简单许多,然后借此发散更多思路
也是用cur和dest两个指针,不过这回的双指针并不在同一个数组上,而是cur指向原数组0下标,dest指向新数组0下标,然后cur有两种情况:
一种是当cur指向的值不为0时,直接将该值复制给新数组,然后cur++,dest++;
另一种是当cur指向的值为0时,则直接将新数组dest和dest+1的位置赋值为0,然后cur++,dest++;
最后将新数组复制给原数组即可
只是这中间会有一个小细节的错误,就是当dest指向新数组的最后一个时,如果此时cur指向的是0,那么dest赋值为0后,dest+1再赋值为0就越界了,这样就会报错,所以要在dest+1赋值0之前判断一下此时dest是否处于最后一个的位置
代码1:
class Solution {
public void duplicateZeros(int[] arr) {
//创建一个长度跟arr一样的新数组arr2
int len=arr.length;
int[] arr2=new int[len];
//cur指向原数组,dest指向新数组
int cur=0;
int dest=0;
//当用dest复写时,还没有复写完数组长度
while(dest<arr.length){
//第一种情况,cur指向非0
if(arr[cur]!=0){
arr2[dest]=arr[cur];
cur++;
dest++;
}else{ //第二种情况,cur指向0
//dest位置赋值0
arr2[dest]=0;
dest++;
//判断一下是否越界
if(dest==arr.length){
break;
}
//将dest+1的位置赋值0
arr2[dest]=0;
cur++;
dest++;
}
}
//将新数组复制给原数组
for(int i=0;i<arr.length;i++){
arr[i]=arr2[i];
}
}
}
现在我们大致有了这么样的思路,就是非0时,直接复写,cur和dest都++,当为0时,dest要复写2次0,那么如果我们不创建新数组,直接在原数组上进行操作,该怎么做呢?
我们将cur和dest都指向原数组的0下标,按照我们刚刚的思路,非0时cur和dest都++,当0时cur++,dest+=2,可是当cur指向0时就会发生覆盖的情况,即dest修改了原来cur即将遍历的原数组的值,被覆盖为0,那么此时按照刚刚思路的代码,就会将后面全部复写为0,明显是不正确的
这时我们就要思考怎么样才能不被覆盖,如果从前往后进行复写,cur和dest都初始化为0,dest至少等于快于cur,那么就有可能发生覆盖的情况,但如果从后往前进行复写,当cur刚好为某个位置时,dest为原数组的最后一个位置,进行往前复写,那么就从原来的慢指针追快指针,变成了快指针追慢指针,当复写到第一个元素时,刚好同时到达终点,就不存在覆盖的问题
例如例题1,0,2,3,0,4,5,0
我们知道复写后是1,0,0,2,3,0,0,4
所以当cur指向原数组4时,dest指向0时
往前复写的步骤为(红色为cur位置,蓝色为dest位置,)
1,0,2,3,0,4,5,4
1,0,2,3,0,0,0,4
1,0,2,3,3,0,0,4
1,0,2,2,3,0,0,4
1,0,0,2,3,0,0,4(cur和dest都刚好到达终点1,指向第一个元素)
那么现在最关键的问题来了,怎么找到cur的初始值呢
当然还是从前往后进行我们刚刚的那个思路,非0则cur和dest++,0时则cur++,dest+=2,只不过我们并不进行复写修改值的操作,仅仅移动指针找到cur的位置,只不过稍微需要修改一点,因为是在原数组上进行修改,所以cur初始化为0,而dest初始化为-1(默认模板)
循环步骤如下:
1.判断cur指向的值
2.为非0则dest++,为0则dest+=2
3.判断此时dest是否已经超过数组长度,如果是,则break
4.cur++
同时,也会发生我们前面所说的细节小错误,即当cur为0时,dest已经来到最后一个位置了,这时再进行dest+=2操作就会越界,所以我们要处理一下边界问题
即让最后一个位置赋值为0,然后dest-=2,cur--即可
代码2:
class Solution {
public void duplicateZeros(int[] arr) {
int cur=0;
int dest=-1;
//找到cur的位置
while(cur<arr.length){
//对cur指向的值进行判断,dest进行移动
if(arr[cur]!=0){
dest++;
}else{
dest+=2;
}
//判断dest是否越界
if(dest>=arr.length-1){
break;
}
//最后cur++
cur++;
}
//处理边界问题
if(dest==arr.length){
dest--;
arr[dest--]=0;
cur--;
}
//正常往前复写
while(cur>=0){
if(arr[cur]!=0){
arr[dest--]=arr[cur--];
}
else{
arr[dest--]=0;
arr[dest--]=0;
cur--;
}
}
}
}
题目三:
如果直接按照暴力的判断,也可以直接写出代码
比如我们先写一个函数用来计算出当前n的长度,然后在判断是否是快乐数的方法中使用循环计算出各个位置上数字的平方和,如果不等于1就递归该平方和,如果等于1则return true
但是由于可能出现无限循环的情况,导致无限递归后栈溢出,所以我们可以建立一个全局遍历count,记录此时递归的次数,我们假设一个比较大的值比如100,当递归次数大于100时,我们就默认为是无限循环,就return false
代码1:
class Solution {
//设置一个全局变量记录递归次数
public int count=0;
public boolean isHappy(int n) {
int l=len(n);
int sum=0;
//循环求各个位置上数字的平方和
for(int i=0;i<l;i++){
sum=sum+(n%10)*(n%10);
n/=10;
}
//判断是否是快乐数
if(sum!=1){
if(count<100){ //觉得还不是无限循环,继续递归
count++;
return isHappy(sum);
}else{ //觉得应该是无限循环,停止递归
return false;
}
}
return true;
}
//计算位数
public int len(int n){
int len=0;
while(n!=0){
n/=10;
len++;
}
return len;
}
}
虽然这道题能通过,并且在短时间也不难想到,但是这并不一定正确,万一有一个数刚好要递归比100次要多,但最后的平方和为1,那么就产生了误判,你可能会说,那我就将门槛值往大的调,1000次,10000次,那这样子就效率太低下了,空间消耗和时间消耗也很大,属于坏的算法,而且给人的感觉也比较low,体现不了算法的魅力
那么接下来就要讲一种比较巧妙的方法了
算法原理:
由题目可知,一共有两种情况,一种是经过多次题目操作,最后平方和为1,另一种是会无限循环,我们可以简单一笔画出第二种的演化流程,就是一条线然后画一个闭合环,其实,第一种情况也可以这样表示,只不过这个环里都是1罢了
学过数据结构中的链表知识的话,我们很容易想起这个问题:如何判断一个链表是否有环
(如果没学过或者不太熟悉的话可以见链表面试练习习题(Java)-CSDN博客里面的第四题,有比较详细的讲解)
那么这样子就能套用那道题目的方法:快慢指针,正如我们第一题中所说,不要被指针给迷惑困住,以为真的要创建这个*,或者那个*,这只是一个思想,既可以用数组下标作为指针,这道题也可以用平方和的值作为指针
那么接下来就很容易了,通过快慢指针的方法,让快慢指针都从起点一起出发,慢指针走一步,快指针走两步,当快指针再次相遇慢指针的时候,那么此时的位置就一定在环内,判断此时位置的值是否为1即可
代码2:
class Solution {
public boolean isHappy(int n) {
//快慢指针都从起点开始走
int fast=n,slow=n;
int sum1=func(n);
int sum2=func(n);
//找到相遇结点
while(true){
slow=func(sum1);
fast=func(func(sum2));
sum1=slow;
sum2=fast;
if(slow==fast){
break;
}
}
//如果相遇结点为1
if(slow==1){
return true;
}else{ //如果相遇结点不为1
return false;
}
}
//求平方和
public int func(int n){
int len=length(n);
int sum=0;
for(int i=0;i<len;i++){
sum=sum+(n%10)*(n%10);
n/=10;
}
return sum;
}
//求位数
public int length(int n){
int len=0;
while(n!=0){
len++;
n/=10;
}
return len;
}
}
但是我们稍微补充一下,为什么一定会出现有环的情况呢,难道不存在像无理数一样一条线一直走这样第三种情况呢?
其实这里要证明需要用到一个简单的数学原理——鸽巢定理,即如果有n个鸽巢,n+1个鸽子,那么至少会有一个鸽巢里面至少有两只鸽子
而我们这道题的最大值为2的31次方-1,也就是int的最大数
2^31=2,147,483,648
一共有10位数,那么按照题目的操作,将每位数的平方加起来,那么10位数最大的9,999,999,999全部加起来也才等于(9^2)*10=810
也就是说每次求的平方和的区间都在[1,810]
那么经历811次题目操作之后,一定会出现一个重复的平方和的值,那么此时就成环了
所以不存在不成环的情况
稍微扩展一下,虽然题目已经说明只有两种情况,但是我们也要知道所谓的第三种情况是不存在的
题目四:
算法原理:
这道题就是想找到容量最大的情况,第一反应不需要思考的话,就是将所有情况都暴力枚举出来,用两层for循环将所有容量的值都计算出来,取最大的那一个,第一层for的i从第一根到倒数第二根,第二层for的j从i+1开始,到最后一根柱子
代码很简单,就是将庞大的计算量交给计算机处理了,虽然结果一定是对的,但是算法思路不好,而且在这道中等难度题目中会超时,因为时间复杂度是O(n^2)
这时会有一个很好的算法,我们都知道控制变量这个方法,体积V=height*weight,如果weight在增加,height在减小,那么V是无法确定增加还是减小的,同理,weight在减少,height在增加,V也是无法确定的
所以确定要让V更大,只有三种情况,h和w都增加或者h增加,w不变或者h不变,w增加
如果确定让V更小,也只有三种情况,h和w都减小或者h减小,w不变或者h不变,w减小
这时我们明白了这个简单的规律之后,代码就很好写了
找到整体区间,不断内缩,即weight在固定减小,每一次根据左右两边界的值选择左缩还是右缩,如果左边界的值比右边界的值更小,那么左缩(因为weight在缩小,height如果变小,那么V一定也在变小),相反,则右缩
我们只需要计算无法确定的情况,直接不计算V更小的情况,然后选出所有V中的最大值
我们定义两个指针和一个变量max,一个指向最左边,另一个指向最右边,进行循环,循环条件为两个指针不相遇时,计算此时的容量,如果比max大,那么就成为新的max,然后我们比较哪个height更矮就舍弃哪个,因为我们知道如果weight在减小,height也在减小,以及weight在减小,height不变,那么V一定是减小的,就不需要计算了,所以每次移动一次指针,计算一次V
那么这个时间复杂度只有O(n)
代码1:
class Solution {
public int maxArea(int[] height) {
//定义左右指针,变量max
int left=0;
int right=height.length-1;
int max=0;
//当两个指针还没有相遇时
while(left!=right){
//根据木桶理论选出值更小的作为高,并计算出此时体积
int h=height[left]>height[right]?height[right]:height[left];
int v=h*(right-left);
//让更大的V变为max
if(v>max){
max=v;
}
//weight是固定在减小的,那么height肯定不能再减小,要排除较小的height
if(height[left]>height[right]){
right--;
}else{
left++;
}
}
return max;
}
}
题目五:
算法原理:
首先要知道如何判断三个数是否构成一个三角形,很简单的数学知识,那就是任意两边之和要大于第三边,转化为代码也就是:a+b>c&&a+c>b&&b+c>a,也就是说要判断三次,其实这里可以进行一个优化,那就是如果明确了三个数的大小顺序,比如a<=b<=c,那么只需要判断一次即可,即a+b>c如果成立,则可以构成三角形,如果不成立则构不成三角形
首先第一种方法就是暴力枚举,用三层for循环,i,j,k作为三边,然后进行三次边长判断,如果我们不进行刚才的优化的话,那么时间复杂度就是O(3N^3),如果进行刚才的优化的话,先排序再判断一次边长条件的话,那么时间复杂度就是O(NlogN+N^3),虽然说常数系数可以忽略,但是N^3这个基数毕竟很大,3N^3肯定要远远大于NlogN+N^3,所以这个优化是质的飞跃
当然,这种方法也是容易想到,但不算最优解,这时我们可以先对数组进行排序,形成单调性,然后使用双指针的算法(甚至有点像三指针)
第一步:
通过max指针确定最大数c,即数组最后一个,以及定义一个统计情况的变量count
第二步:
通过left和right指针确定较小的两边a,b,即c的左区间的左边界(数组最小值)和右边界(数组倒数第二个)
第三步:
right指针固定,如果a+b>c,那么区间内left指针所有后面的值都能与b构成三角形,个数直接就得出count=count+(right-left),然后right--
如果a+b<=c,那么就构不成三角形,这时left++即可,因为如果left固定,right--,那么right对应的值只会越来越小,更不可能能构成三角形
判断完后继续循环第三步,直至left==right
第四步:
那么固定的最大边c的情况已经全部统计出来,接下来就继续确定新的最大边,max--即可,然后left指针又重置为0,right指针重置为max-1,再循环第三步的操作,直至max>=2(因为小于下标2的话,就凑不出三个数了)
第五步:返回count值即可
这种算法的时间复杂度只有两层循环,第一层循环找最大边c,第二层循环统计a,b能与c构成三角形的情况,所以时间复杂度只有O(N^2),比暴力枚举效率高不少
同样也是代码并不难,但是思路不好想,只能通过多积累多练习,积攒经验
代码1:
class Solution {
public int triangleNumber(int[] nums) {
//先排序,构成单调性
Arrays.sort(nums);
//找到最大边c和定义变量count
int max=nums.length-1;
int count=0;
//这层循环来控制最大边c的切换
while(max>=2){
//初始化left和right指针
int left=0;
int right=max-1;
//计算在固定最大边c的条件下,能构成三角形的情况
while(left!=right){
//如果能构成三角形
if(nums[left]+nums[right]>nums[max]){
count=count+(right-left);
right--;
}else{//构不成三角形
left++;
}
}
//最大边切换
max--;
}
//返回统计情况的总数
return count;
}
}
题目六:
算法原理:
简单来说题目就是在升序数组中,找到两个数之和等于target,返回这两个数即可
第一反应也是暴力枚举,将所有两个数之和都枚举出来,这时找到等于target的值,返回这两个数,也就是用两层for循环即可,时间复杂度为O(N^2)
但是暴力枚举并不是最优解,大部分算法都是在暴力枚举的基础上,进行优化,所以以后第一反应都是要看看暴力能不能做,能做的话,再思考怎么样进行优化
题目说明了数组是升序的,也就是具有单调性,看到具有单调性的时候,要反应过来有两种常见的算法:二分和双指针,二分现在还没有具体讲解,所以这道题就不讲二分了,而且这道题的最优解也不是二分,双指针要比二分更优在这道题里,当然如果没有单调性,也不要完全排除二分和双指针,因为我们只需要对数组进行排序就具有单调性了
步骤如下:
这道题我们只需要定义两个指针left和right,初始化为0和length-1
如果此时对应的两个数比target大,那么就说明要让一个数更小,那么right--
如果此时对应的两个数比target小,那么就说明要让一个数更大,那么left++
如果此时对应的两个数等于target,那么就说明此时这两个数符合要求,返回这两个数即可
有一点小注意,在写代码的过程后,会发现编译器会报错,因为编译器会认为并不是所有情况都有返回值,但是我们这道题没有说不存在的情况下会怎么样,所以默认至少存在两个数是符合题目要求的(事实上也是如此),那么我们此时要照顾一下编译器,只需要在最后任意return一个符合返回值类型的即可,这样就不会报错了
代码1:
class Solution {
public int[] twoSum(int[] price, int target) {
//定义双指针
int left=0,right=price.length-1;
//进行区间查找
while(left!=right){
//大了,就让大的那个数变小
if(price[left]+price[right]>target){
right--;
}else if(price[left]+price[right]<target){//小了,就让小的那个数变大
left++;
}else{//找到了就返回
return new int[]{price[left],price[right]};
}
}
//照顾编译器,任意返回一个符合返回值类型的都行
return new int[]{price[left],price[right]};
}
}
题目七:
算法原理:
与我们刚刚两数之和的套路几乎一样,这道题是求三数之和为0的情况
如果第一反应采用暴力枚举的方法的话,那么就需要三层循环,枚举出所有的三元组,最后找到符合条件的三元组,但是这里就有一个难点,怎么样去重呢?如果学过HashSet的话,直接扔进去就好了,当然如果面试的时候,面试官问还有其他解法时,还要自己思考其他方法,不能只会用Java现成的方法
同样,暴力枚举的方法不算高效,时间复杂度达到了O(N^3),更优秀的解法是双指针
这时要记得我们上道题目说的,不要看到题目的数组不是有序单调的,就完全不考虑二分,双指针算法,因为我们只需要给数组排个序就可以使用了
那么使用双指针算法的步骤如下:
先排序,再创建要返回的顺序表
然后第一层循环固定第一个数,循环里面则直接使用上道题找两个数之和的思路,只不过这两个数之和的target值为固定第一个数的负数(这样三个数相加才为0)
然后要有三个地方注意:不漏,去重以及数组下标不越界
不漏:因为两数之和的那道题是任意找到一组即可,而这道题要求全部找到,所以在找到一组之和,不能直接返回,还要继续找
去重:其实不用HashSet的话,我们只需用循环来判断一下该数的前一个数或者后一个数是否与当前值相等即可,每次++或者--,最后再++一次或者--一次,这时就跳过了所有一样的数
注意有两次去重,第一次是两数之和的时候,左右指针要去重,第二次是固定的第一个数要去重
数组下标不越界:因为我们在去重的时候,要用当前的前一个数或者后一个数进行比较,所以当此时的位置为首和尾时,那么用下标去访问首的前一个数或者尾的后一个数,那么就会数组下标就会越界,所以这时要记得添加一下判断条件
还有一个小优化,就是在固定第一个数的最后位置时,不需要固定到最后,因为当比0大时,即是正数时,那么后面的区间也全是正数,在这个区间内是不可能存在两个数之和等于负数(因为正数取反就是负数),所以循环条件只需要到0即可
需要注意的点很多,很考验基本功,虽然有思路,但是代码能力有可能不能够将思路转化为代码,所以这就比较考验之前学习语言时的基础和理解
同样,通过使用双指针算法,时间复杂度变为了O(N^2),由此可见,双指针算法都优化能力几乎是一个指数量级,N^3能优化至N^2,N^2能优化至N,N能优化至O(1),是一种很优秀的算法
代码1:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
//先排序
Arrays.sort(nums);
//创建要返回的顺序表
List<List<Integer>> list=new ArrayList<List<Integer>>();
//固定第一个数,并且截至在0,正数后面不可能存在
for(int i=0;i<nums.length&&nums[i]<=0;i++){
//创建左右指针
int left=i+1,right=nums.length-1;
//找到所有的两个数之和等于第一个数的负数
while(left<right){
//如果小了,把小的数换成大的
if(nums[left]+nums[right]<-nums[i]){
//如果后面几个数跟当前的数一样,跳过(去重)
while(left<nums.length-1&&nums[left]==nums[left+1]){
left++;
}
//找到后一个不一样的数
left++;
}else if(nums[left]+nums[right]>-nums[i]){//如果大了,把大的数换成小的
//如果前面几个数跟当前的数一样,跳过(去重)
while(right>0&&nums[right]==nums[right-1]){
right--;
}
//找到前一个不一样的数
right--;
}else{//找到了一组的两个数满足要求
//添加这一组的三个数
List<Integer> list1=new ArrayList<Integer>();
list1.add(nums[i]);
list1.add(nums[left]);
list1.add(nums[right]);
list.add(list1);
//继续找其他的与这一组不一样的两个数(去重)
while(left<nums.length-1&&nums[left]==nums[left+1]){
left++;
}
left++;
while(right>0&&nums[right]==nums[right-1]){
right--;
}
right--;
}
}
//去重并跳过
while(i<nums.length-1&&nums[i]==nums[i+1]){
i++;
}
}
//返回该顺序表
return list;
}
}
题目八:
算法原理:
几乎就是套娃了,跟之前思路一样,第一层循环先固定第一个数,然后在循环里面套用刚刚解三数之和的方法,让这三数之和为target-第一个数即可
同样可以使用暴力枚举,用四层循环,时间复杂度达到O(N^4)
需要注意的点跟上道题几乎一样,不漏,去重和数组下标越界,这里去重记得有三次,比上道多了一次,每多一个数就多一次去重
如果正确写完代码后,大致如下:
代码1(有一个小错误):
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ret = new ArrayList<>();
// 1. 排序
Arrays.sort(nums);
// 2. 利⽤双指针解决问题
int n = nums.length;
for (int i = 0; i < n;) // 固定数 a
{
// 三数之和
for (int j = i + 1; j < n;) // 固定数 b
{
// 双指针
int left = j + 1, right = n - 1;
int aim = target - nums[i] - nums[j];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum > aim)
right--;
else if (sum < aim)
left++;
else {
ret.add(Arrays.asList(nums[i], nums[j], nums[left++],
nums[right--]));
// 去重⼀
while (left < right && nums[left] == nums[left - 1])
left++;
while (left < right && nums[right] == nums[right + 1])
right--;
}
}
// 去重⼆
j++;
while (j < n && nums[j] == nums[j - 1])
j++;
}
// 去重三
i++;
while (i < n && nums[i] == nums[i - 1])
i++;
}
return ret;
}
}
这回代码里面使用了Arrays.asList()的方法,具体功能就是将括号里的按照加到数组里,然后再以List类型返回,注意()里面只能是string,integer这种包装类型,不能是int,long这些基本数据类型,我也是在学习的过程中才知道这个方法的,听说是一种比较常用的方法
会发现力扣有两个示例显示不通过,如下图显示
可以看到这里的数据都极大或极小,这时我们代码中三数之和算的aim就会越界int范围的最小值,所以要改用long接收一下
代码2(修改后正确的):
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ret = new ArrayList<>();
// 1. 排序
Arrays.sort(nums);
// 2. 利⽤双指针解决问题
int n = nums.length;
for (int i = 0; i < n;) // 固定数 a
{
// 三数之和
for (int j = i + 1; j < n;) // 固定数 b
{
// 双指针
int left = j + 1, right = n - 1;
long aim = (long) target - nums[i] - nums[j];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum > aim)
right--;
else if (sum < aim)
left++;
else {
ret.add(Arrays.asList(nums[i], nums[j], nums[left++],
nums[right--]));
// 去重⼀
while (left < right && nums[left] == nums[left - 1])
left++;
while (left < right && nums[right] == nums[right + 1])
right--;
}
}
// 去重⼆
j++;
while (j < n && nums[j] == nums[j - 1])
j++;
}
// 去重三
i++;
while (i < n && nums[i] == nums[i - 1])
i++;
}
return ret;
}
}
所以在算法比赛中,也有这么一种习惯:尽量不用int,几乎没事就用long。因为算法比赛中没有错误样例给你看,万一样例有这种比较恶心的数据,你这道题就拿不到满分了,而原因竟然仅仅是因为没有用long,而用的int,很亏
同样使用双指针的算法,在暴力枚举的基础上进行优化,时间复杂度降了一个次方,来到了O(N^3)
要有举一反三的能力,比如求五个数之和,六个数之和甚至更多,但根本思路是不变的,只不过是循环嵌套的层数在增加
总结:
至此,双指针算法就大致学习完毕了,我们来稍微总结一下
什么是双指针?
双指针只是一种思路,并不是真正的指针,既可以用数组下标作为指针,也可以用值作为指针
什么时候用双指针?
在看到题目有出现具有单调性区间的时候,要联想到双指针和二分,当然也不要看到没有单调性就完全排除,因为我们可以自己排序使其有序
双指针的优化能力?
时间复杂度能降一个次方,是一种非常高效的优化算法
那么接下来我们将继续学习更多常用算法,持续努力,一定会有收获!