剑指Offer(数据结构与算法面试题精讲)C++版——day3
- 题目一:数组中和为0的3个数字
- 题目二:和大于或等于k的最短子数组
- 题目三:乘积小于k的子数组
题目一:数组中和为0的3个数字
前面我们提到,在一个排序之后的数组中快速找到何为0的两个数,采用左右双指针向中间收拢的方式,一次遍历即可完成。因此,对于这里的3个数和为0,可以先对整个数组排序,然后取出其中一个数,这样问题就变成了在剩下数中查找两个数,使得两个数的和为0。这种方式对应的时间复杂度为,对数组排序的时间,和对元素遍历查找3个数和为0的时间复杂度,如果使用快速排序,时间即为O(nlogn)和O(n^2), 综合下来的时间复杂度为O(n^2)。
# include <iostream>
# include <algorithm>
using namespace std;
void printTriple(int arr[],int len) {int before=0,sum=0;for(int i=0; i<len; ++i) {if(i!=0&&arr[i]==before) {//防止重复遍历第一个数 continue;}for(int p=i+1,q=len-1; p<len&&p<q;) {//选完第一个数之后只遍历之后的数 while(p+1<len&&arr[p+1]==arr[p])p++;//过滤重复 while(q-1>=0&&arr[q-1]==arr[q])q--;sum=arr[i]+arr[p]+arr[q];if(!sum) {//拿到结果之后接着找其他可能组合 cout<<"["<<arr[i]<<","<<arr[p]<<","<<arr[q]<<"]"<<endl;p++;q--;} else if(sum>0) {//移动指针 q--;} else {p++;}}before=arr[i];}}
int main() {int arr[]= {-1,0,1,2,-1,-4,-1,2};int len=sizeof(arr)/sizeof(arr[0]);sort(arr,arr+len);for(int i=0; i<len; ++i) {cout<<arr[i]<<" ";}cout<<endl;printTriple(arr,len);return 0;
}
注意,还有一点,题目中要求不能够重复,这里需要对每次取出的第一个进行标记before
,记录是否访问过。这样能够保证三个数中第一个选择的数据不会重复,另外,为了保证后续选择的两个数不会重复,这里只取顺序排列之后的数(即排序之后右边的数)来检测,比如这里数组为[-1,0,1,2,-1,-4],排序之后得到[-4,-1,-1,0,1,2],由于before的过滤,第一个被选择的数依次为[-4,-1,0,1,2],我们以第一个数为-1举例,对于查找剩下的两个数,我们从-1之后,即[-1,0,1,2]中使用左右指针向中间逼进的方式遍历,可以找到三元组[-1,-1,2],找到之后两个指针同时向中间逼进,最终可以找到余下的[-1,0,1]。再来分析为什么这样找不会漏掉,因为如果选中一个数之后,它可以和它左边的数构成目标三元组,那么在遍历左边的数的时候就会已经找到了。
题目二:和大于或等于k的最短子数组
这道题采用经典的双指针法来解决十分合适。我们设置两个指针,不妨分别设为 p
和 q
。这两个指针在数组上滑动,它们之间所涵盖的数构成了用于求和的子数组。算法开始时,我们先让指针 p
和 q
都指向数组的起始位置。然后进入迭代过程:当 p
和 q
之间子数组的和小于目标值 k
时,意味着当前子数组的元素总和还不够大,我们就将右侧的指针 q
向右移动一位,把 q
所指向的新元素纳入子数组,以此增大子数组的和。而当 p
和 q
之间子数组的和大于或等于 k
时,说明子数组可能包含了过多的元素,此时将指针 p
向右移动一位,把 p
原来指向的元素从子数组中剔除,来尝试缩短子数组的长度,进而减小子数组的和。不断重复上述操作,在这个过程中持续调整子数组的范围和元素组成,直到满足特定的条件或者遍历完整个数组。基于这样的思路,我们就可以得到如下代码来实现这一算法。
# include <iostream>
# include <algorithm>
using namespace std;
int findMinLength(int arr[],int len,int k) {int p=0,q=0,min=INT_MAX,sum=arr[0];while(p<=q) {if(sum<k) {//和不够,尝试右移右指针 if(q+1<len) {sum+=arr[++q];} else {//如果不能右移,结束检测 break;}} else {//够了,尝试缩小长度 if(min>q-p+1) {min=q-p+1;}sum-=arr[p++];}}return min;
}
int main() {int arr[]= {5,1,4,3};int len=sizeof(arr)/sizeof(arr[0]);int k=7;cout<<"输出最小长度:"<<findMinLength(arr,len,k);return 0;
}
这样,只需要在时间复杂度为O(n)的情况下就能够找到最小长度了。我们对这种方法进行提炼,通过移动双指针,能够帮助我们在时间复杂度为O(n)下遍历到所有可能的子数组。你可能说这不是两个数组下标吗,为什么要称为双指针呢,其实这是和链表那块做统一,之前在前端面试的时候,面试官问我字符串逆序怎么处理,我当时也是第一次听说双指针这个概念,我当时还在想JavaScript也没指针这个概念,后来才接受了这个概念,当提到算法的时候我们这么表达可能面试官也能更快的理解你所说的方法或思想。
题目三:乘积小于k的子数组
这道题和前面的方法很相似,整体上还是利用双指针来进行遍历。思路为,通过左指针p和右指针q来遍历,对于两个指针中间的子数组,分为两种情况,如果子数组乘积小于k,那么说明此时保持右指针不变,左指针向右移动得到的所有子数组都满足要求,通过这种方式边能够最大化查找子数组,在得到左指针超过右指针后,恢复指向,同时向原数组中新增一个元素,再次遍历,如果没有新元素可以加入,那么说明所有的子数组都被遍历完成,算法终止。
对于时间复杂度,最坏情况下,数组中的所有子数组都是成立的。由于右指针q需要遍历所有的元素,那么也就是子数组长度为1,2,3,…,n,其中每一个子数组中,都可以固定右指针q,不断移动左指针p,那么对应的时间复杂度为O(n^2)。
# include <iostream>
# include <algorithm>
using namespace std;
int findChildArr(int arr[],int len,int k) {int p=0,q=0,product=arr[0],count=0;while(p<=q) {if(product<k) {//当前子数组满足条件,则保持右指针不变,移动左指针可以拿到所有子数组 int tmp=p;while(p<=q) {count++;++p;}p=tmp;//恢复指针指向,同时在子数组中加入新元素 if(q<len-1) {product*=arr[++q];} else {break;}} else {product/=arr[p++];}}return count;
}
int main() {int arr[]= {10,5,2,6};int len=sizeof(arr)/sizeof(arr[0]);int k=100;cout<<"这样的数组个数为:"<<findChildArr(arr,len,k);return 0;
}
最后,在编写双指针算法时,我发现了一个问题,如果使用++q或者q++这样的表达,一定要细致。比如第一次编写算法三的时候,将product/=arr[p++];
写成了product/=arr[++p];
查了半天才发现问题。
我是【Jerry说前后端】,本系列精心挑选的算法题目全部基于经典的《剑指 Offer(数据结构与算法面试题精讲)》。在如今竞争激烈的技术求职环境下,算法能力已成为前端开发岗位笔试考核的关键要点。通过深入钻研这一系列算法题,大家能够系统地积累算法知识和解题经验。每一道题目的分析与解答过程,都像是一把钥匙,为大家打开一扇通往高效编程思维的大门,帮助大家逐步提升自己在数据结构运用、算法设计与优化等方面的能力。
无论是即将踏入职场的应届毕业生,还是想要进一步提升自己技术水平的在职开发者,掌握扎实的算法知识都是提升竞争力的有力武器。希望大家能跟随我的步伐,在这个系列中不断学习、不断进步,为即将到来的前端笔试做好充分准备,顺利拿下心仪的工作机会!快来订阅吧,让我们一起开启这段算法学习之旅!