文章目录
一、复写零
给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。
注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。
示例 1:
输入:arr = [1,0,2,3,0,4,5,0]
输出:[1,0,0,2,3,0,0,4]
解释:调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]
解题思路
如果从前向后进⾏原地复写操作的话,由于0 的出现会复写两次,导致没有复写的数被覆
盖掉。因此我们选择使用双指针算法从后往前的复写策略。
但是从后向前复写的时候,我们需要找到复写操作所处理的最后一个数,因此我们的⼤体流程分两
步:
- 先找到最后⼀个复写的数;
- 然后从后向前进⾏复写操作。
代码实现及解析
java
class Solution {
public void duplicateZeros(int[] arr) {
//先找到复写结束时,两指针的位置
int cur=0,dest=-1;
//按照复写逻辑来一遍,但是不改动数据,只移动指针
for(;cur<arr.length;cur++){
if(arr[cur]==0){
dest+=2;
}else{
dest++;
}
if(dest>=arr.length-1){
break;//如果dest到了数组边界甚至越界,就break
}
}
//此时就可以直接以两指针现在的位置进行从后往前的复写
//但是要先判断一下dest是否越界,并处理
if(dest==arr.length){
arr[arr.length-1]=0;
dest-=2;
cur--;
}
//进行复写
while(cur>=0){
if(arr[cur]==0){
arr[dest--]=arr[cur];
arr[dest--]=arr[cur--];
}else{
arr[dest--]=arr[cur--];
}
}
}
}
总结
当使用双指针算法出现了数据被错误覆盖的情况时,可以换一种遍历方式。一般可能会使用从后往前、或者是从两边向中间,那就要思考在特定逻辑下两个指针这个时候的初始位置应该在哪里,不能随意定义指针位置
扩展:
数组分块也是⾮常常见的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成几个部分。这种类型的题,⼀般就是使⽤双指针来解决。比如,cur指针遍历处理数据,dest指针作为一个区块的边界,可以dest++来扩展边界放入数据。
二、快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
提示:
1 <= n <= 231 - 1
解题思路
- 本是一个逻辑比较简单的题目,但解法很值得学习
- 关键在于能否利用在一次一次推算每个计算值当中含有的规律
计算过程是无限循环但始终变不到1:
- 无限循环的原因:但是由于n的范围是固定的,那么按照题目的固定计算方法,所得出的计算结果一定是在一个范围内的。那么当计算次数达到一定程度之后就会出现重复的结果,从这个重复的结果开始,就是一个循环了。
- 而这个最终会形成循环的计算过程,它所计算得出的全部数据结果的集合所形成的结构与带环链表的非常相似的。
- 而且最终结果会循环为1的情况也可以看做环中数据都是1的类似带环链表,那我们就可以使用带环链表相关的思路开解决此问题。
解题方法:
- 在计算过程中使用快慢指针,当指针在环中相遇时判断此时指针指向的值是否为1就可以了。
代码实现及解析
java
class Solution {
//按照题目逻辑,求n的下一个计算值
public int bitSum(int n){
int sum=0;
int tmp=n%10;
while(n!=0){
sum+=tmp*tmp;
n/=10;
tmp=n%10;
}
return sum;
}
public boolean isHappy(int n) {
//定义快慢指针
int fast=bitSum(n);
int slow=n;
//两指针一直走直到它们相遇
while(fast!=slow){
fast=bitSum(bitSum(fast));
slow=bitSum(slow);
}
//判断一下相遇时的值是不是1,是就代表为快乐数
if(fast==1){
return true;
}
return false;
}
}
总结
计算结果的集合与带环链表相似,使用带环链表相关思路解题
三、 盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
-
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
-
返回容器可以储存的最大水量。
注意:你不能倾斜容器。
示例 :

输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
不要想着用暴力解法,会超时!
解题思路
- 影响容积的因素只有两个:高度(短板的长度)、宽度(长板和短板之间的距离)。
- 我们可以先控制宽度从大(左、右最远的边界)到小(边界收缩直到相遇),然后控制边界收缩的逻辑来筛选出每次收缩后容积的最优解。在这个过程中我们枚举出了一定量的容积值,返回最大值。
- 每次移动都将短板舍弃,向内收缩,计算出容积。
- 因为只有移动短板,
才有可能找到更高的板,从而在宽度减少的情况下,用高度增加来补偿。比如,宽度虽然减小,但是遇到了一个比当前长板1更长的板2,那么此时水面高度就变成了板1,高度就比之前增加了,就可能得到更大的容积。 - 但移动长板的话,水面高度压根不会增加(被短板限制),但宽度却一定减少,容积一定减小
这样做就有效地跳过了大量不可能成为最优解的容器组合
代码实现及解析
java
class Solution {
public int maxArea(int[] height) {
int left=0;//容器的左边界
int right=height.length-1;//容器的右边界
int maxV=0;//记录边界变化过程中最大的容积
while(left!=right){//循环调整容积的边界,直到边界相遇
int V=Math.min(height[left],height[right])*(right-left);//计算此次的容积
maxV=Math.max(maxV,V);
//哪个边界短就舍弃哪个边界,让边界往里收缩
if(height[left]<height[right]){
left++;
}else{
right--;
}
}
//返回每次边界收缩后最大的容积
return maxV;
}
}
总结
使用对撞指针作为容器的边界,通过控制宽度的递减以及高度的不断最优筛选,最终从少量的枚举容积值中得出最大值
四、 有效三角形的个数
解题思路
- 如果直接使用暴力枚举对 1.a+b>c 2.a+c>b 3.b+c>a 这三条表达式进行验证的话效率会非常的低下,时间复杂度高达O(n^3)!所以一定要对解题方法进行大幅度优化。
- 那么如果a、b、c 满足a<b<c 我们就只需验证 a+b>c 这一个表达式就够了,这个成立其他则两个恒成立。所以我们可以先对数据排序,再按大小取数据匹配。
- 之后还可以依据有序数据集合的单调性进行其他的算法优化,可以砍掉一大部分的不必要的比较。
代码实现及解析
java
class Solution {
public int triangleNumber(int[] nums) {
Arrays.sort(nums);//对数组进行排序,得到a<b<c....
int count=0;//外部定义count,记录有效三元组个数
for(int i=nums.length-1;i>=2;i--){//最外层循环从后往前遍历数组,固定三元组的最大值
int left=0;
int right=i-1;//[left,right]之间的元素来匹配与上述固定的三元组最大值比较
while(left!=right){
if(nums[left]+nums[right]>nums[i]){
count+=right-left;//如果a+b>c,那么[a+1,b)的数+b都大于c,就不用再比较了
right--;//直接进行下一趟(所以才有一层while循环,此时直接一整趟不用比较了)
}else{
left++;//继续让nums[right]和left后面的元素继续尝试匹配,
} //而且left之后每一趟都不用再更新了,因为之后的nums[right]只会越来越小
} //与这个left位置值加和也都不会>c
}
return count;
}
}
总结
有效三角形的验证要注意使用有序数据 a<b<c ,之后验证a+b>c就足够了,数据有序化不仅仅在这种问题有帮助,在很多问题上有序的数据都能够提供很多便利要学会利用 有序数据集合 的端点值来解题,也就是有序数组+双指针(通常是对撞指针),尤其在"比较"、"加和运算"等场景下。左、右端点分别是该数据集合的两个最值,这二者结合使用常常对算法有很大程度的优化,有时也会采取中间值+端点值的策略,要看具体问题具体分析
五、三数之和为零
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
解题思路
- 可以先对数据排个序,以便双指针算法的应用等等
- 三数加和问题,先固定一个数,再使用双指针算法匹配另外两个数,再利用有序数据集合的特性与双指针算法的结合对数据进行去重。
代码实现及解析
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list=new ArrayList<>();
Arrays.sort(nums);//先排序
for(int i=0;i<=nums.length-3;i++){//先在最外层循环固定i
if(nums[i]>0) break;//一个小优化,nums[i]>0,那么后面的数都>0,之后的三数之和都不可能==0
//再使用对撞指针来匹配另外两个数
int left=i+1;
int right=nums.length-1;
while(left!=right){
if((nums[left]+nums[right]+nums[i])>0){
right--;
}else if((nums[left]+nums[right]+nums[i])<0){
left++;
}else{//找到三数加和为零
List<Integer> threeNum=new ArrayList<>();
threeNum.add(nums[i]);
threeNum.add(nums[left]);
threeNum.add(nums[right]);
list.add(threeNum);
//left、right两个位置的组合数据去重
int repeatNum=nums[left];
while(nums[left]==repeatNum&&left!=right){//注意:left!=right 处理,防止越界
left++;
}
repeatNum=nums[right];
while(nums[right]==repeatNum&&left!=right){
right--;
}
}
}
//i位置数据的去重
int repeatNum=nums[i];
while(nums[i]==repeatNum&&i<=nums.length-3){
i++;
}
i--;//这里再将i退回一步,因为这层循环结束后,for循环语法快内会将i再++一次,
//导致i多走一步。不过由于while循环已经处理了i的移动,也可以选择直接将for循环内的i++撤掉
}
return list;
}
}
总结
本题算法经典、细节也有很多,非常值得着重学习
对数据进行排序不仅可以使用有序数据集合的单调性等等特性来解题,而且有利于二分算法、双指针算法的应用可以利用有序数据的单调性对算法进行多处的优化,比如本题不仅在双指针算法中应用该特性,而且还以此分析出正整数分界点之后数据加和均>0的优化此题利用有序数组中相同数据"捆绑"的特性推算出去重的操作,也就是当处理一个数据后往后移动时,若遇到相同的数据直接跳过,直到找到不相同的数据为止,多数据组合也是一样的道理(比如本题就 i、left、right 组合的问题,对它们依算法逻辑都进行了去重)
去重不能只知道用哈希表,依然要去想如何设计出算法来达到要求,不然面试要求其他方法就哑巴了(那就挂了)
扩展:
四数之和问题
四数之和问题和三数之和问题的解法是一模一样的,只是多了一个数这个时候就需要我们使用两层for循环在外面来固定两个数,内层循环同样使用双指针算法来匹配剩下的两个数,相当于是在最外层循环固定了一个数之后就又变成了三数之和问题了。同样需要注意每个数的去重问题,方法依然是利用有序数组相同数据的"捆绑"性,跳过重复的数据来达到要求