283.移动零
这个解法的本质是:用两个指针「划分出数组的两个区间」,通过交换把非零元素「归位」到前半区,零自然被挤到后半区。
(慢指针 slow + 快指针 fast):
slow:指向「非零区的下一个位置」([0, slow)全是非零元素);fast:遍历数组,找到所有非零元素,将其赋值到slow位置,然后slow++;
bash
class Solution {
public void moveZeroes(int[] nums) {
int n=nums.length;
int slow=0,fast=0;
while(fast<n){
//不为0
if(nums[fast]!=0){
int tmp=nums[slow];
nums[slow]=nums[fast];
nums[fast]=tmp;
slow++;
}
fast++;
}
}
}
1089. 复写零
重复零(in-place 原地修改)核心思路
- 核心目标
在不额外开辟数组的前提下,将原数组中每个0原地复制一次(后续元素右移,超出数组长度的部分舍弃)。
- 两步核心实现
步骤 1:统计需要复制的 0 的总数(cnt),处理边界越界
- 遍历数组,用
cnt记录遇到的0的个数(每个0会让后续元素多右移一位); - 遍历终止条件:
i + cnt ≤ n(n为数组最后一个下标,保证元素不越界); - 边界处理:若遍历中遇到
i + cnt == n(当前0的复制会超出数组长度),则直接将数组最后一位设为0,并将n减 1(舍弃越界的复制),终止统计。
步骤 2:从后往前原地修改数组(避免覆盖未处理元素)
-
确定起始修改位置:
j = n - cnt(最后一个未被移位的原始元素下标); -
从
j向前遍历: -
- 若当前元素是
0:先将arr[i+cnt]设为0,cnt减 1,再将arr[i+cnt]设为0(完成 0 的复制); - 若当前元素非 0:将
arr[i+cnt]设为当前元素(完成元素右移)。
- 若当前元素是
关键点回顾
- 核心技巧:先统计 0 的个数确定移位偏移量,再从后往前修改(避免正向遍历覆盖未处理元素);
- 边界处理:单独处理最后一个 0 的越界情况,保证数组长度不变;
- 原地修改:利用
cnt作为移位偏移量,无需额外空间,时间复杂度 O (n)。
bash
class Solution {
public void duplicateZeros(int[] arr) {
//数组最后一个位置
int n=arr.length-1;
//情况1:
//1 0 1
//情况2:
//1 0 0 1
//2
int cnt=0;//当前位置需要重写0个数(当前需要加多少个0)
//统计需要重写0的个+处理最后一个(0元素越界问题)
for(int i=0;i+cnt<=n;i++){
//处理最后一个
if(arr[i]==0){
//最后一个重写的数是0
if(i+cnt==n){
arr[n]=0;// 最后一个要写的数是0
n--;
break;
}
cnt++;
}
}
int j=n-cnt; //最后一个要写的数的下标
for(int i=j;i>=0;i--){
if(arr[i]==0){
arr[i+cnt]=0;
cnt--;
arr[i+cnt]=0;
}else{
arr[i+cnt]=arr[i];
}
}
}
}
202. 快乐数(判环问题)
最优解法:快慢指针法(Floyd 判圈算法,O (logn) 时间 + O (1) 空间)
快乐数的关键矛盾是「是否进入循环」:
- 若最终能到 1 → 不会循环,是快乐数;
- 若到不了 1 → 必然进入无限循环(因为数字的平方和取值范围有限)。
用「快慢指针」检测循环,逻辑和链表找环完全一致:
- 慢指针(slow):每次计算 1 次平方和(走 1 步);
- 快指针(fast):每次计算 2 次平方和(走 2 步);
- 若
slow == fast:说明进入循环,此时若值为 1 则是快乐数,否则不是; - 若快指针先到 1 → 直接判定为快乐数。
java
class Solution {
public boolean isHappy(int n) {
//快先走一步,避免初始化相等,无限循环
int slow=n,fast=Sum(n);
//fast=1提前结束 而且一定相遇
while(fast!=1&&fast!=slow){
slow=Sum(slow); //走一步
fast=Sum(Sum(fast));//走两步
}
return fast==1;
}
//计算平方和
public int Sum(int n){
int sum=0;
while(n!=0){
int num=n%10;
sum+=num*num;
n=n/10;
}
return sum;
}
}
如果觉得快慢指针抽象,可先用哈希集合记录所有出现过的数,若重复出现则说明循环:
java
class Solution {
public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n); // 记录出现过的数
n = getSquareSum(n); // 计算平方和
}
return n == 1; // 若n=1则是快乐数,否则循环
}
private int getSquareSum(int num) {
int sum = 0;
while (num > 0) {
int digit = num % 10;
sum += digit * digit;
num /= 10;
}
return sum;
}
}
11. 盛最多水的容器
核心思路
贪心策略的核心逻辑:容量由「较短边」和「水平距离」共同决定,移动较短边的指针才有可能增大容量。
-
初始化左指针
left在数组开头(0),右指针right在数组末尾(n-1); -
计算当前容量,记录最大值;
-
移动指针:若
height[left] < height[right],则left++(移动较短边的指针 );否则right--; -
- 因为不管移动左还是右边,宽都得减1,不如尽可能让小的值更大一定,甚至大于右边的值
-
重复上述步骤,直到
left >= right,最终记录的最大值即为答案。
java
class Solution {
public int maxArea(int[] height) {
int l=0,r=height.length-1;
int ret=0;
while(l<r){
//高度由最小的边决定
int h=Math.min(height[l],height[r]);
int w=(r-l)*h;
ret=Math.max(ret,w);
//贪心:移动最小的边,因为后面/前面可能使得水更多
if(height[l]<height[r]) l++;
else r--;
}
return ret;
}
}
611. 有效三角形的个数
最优解法:排序 + 双指针法(O (n²) 时间 + O (logn) 空间)
1. 核心思路
利用三角形三边规则的简化条件,结合排序和双指针缩小搜索范围:
- 排序数组 :将数组升序排列,方便固定最大数
c,并在左侧找满足a + b > c的a、b; - 固定最大数 :遍历数组,将
i作为最大数c的下标(从 2 开始,因为至少需要 3 个数); - 双指针找有效对 :对于每个
i,左指针left初始为 0,右指针right初始为i-1:
-
- 若
nums[left] + nums[right] > nums[i]:说明left到right-1的所有数与right组合都满足条件(共right-left个),right--; - 若
nums[left] + nums[right] ≤ nums[i]:需要增大和,left++;
- 若
- 累加所有有效组合数,即为答案。
java
class Solution {
public int triangleNumber(int[] nums) {
//排序,找到数组最大的c
int n=nums.length;
if(n<3) return 0; //不可能是三角形
Arrays.sort(nums);
//找到两个数 a+b>c 就行
int cnt=0;
//下标i的数依次充当最大值
for(int i=2;i<n;i++){
//r必须从i-1开始,比i大,就不能保证下标为i的值是最大的
int l=0,r=i-1;
cnt+=f(nums,l,r,nums[i]); //统计个数
}
return cnt;
}
public int f(int[] nums,int l,int r,int c){
int ret=0; //统计个数
while(l<r){
//小于c 就往前移动l
if(nums[l]+nums[r]<=c){
l++;
}else{
//[l+1,r-1] 也一起统计了
ret+=(r-l);
r--;
}
}
return ret;
}
}
15. 三数之和
核心思路
-
排序预处理:对数组升序排序,为双指针调整和、去重、提前终止提供基础;
-
固定单个数 :遍历数组固定第一个数
nums[i],将三数和为 0 转化为找「i右侧两数之和 =-nums[i]」的问题,同时跳过重复的nums[i]、遇到正数直接终止遍历(性能优化); -
双指针找两数 :用左指针(
i+1)、右指针(数组末尾)在i右侧区间遍历,通过移动指针调整两数之和:和小则左指针右移,和大则右指针左移,找到目标和时记录三元组并跳过指针重复值,避免重复结果。 -
去重相关(避免重复三元组,核心易错点)
- 固定数去重 :必须跳过
i>0 && nums[i]==nums[i-1]的情况,否则同一固定数会生成重复三元组(如连续两个 - 1 会重复找到 [-1,0,1]); - 双指针去重 :找到有效三元组后,需跳过
nums[l]==nums[l+1](左指针)和nums[r]==nums[r-1](右指针)的重复值,且必须先判断l<r再检查重复(避免数组越界); - 去重时机:固定数的去重在遍历开始时,双指针去重在找到有效三元组后,顺序不可颠倒。
- 下标合法性(避免重复使用元素)
- 左指针必须初始化为
i+1,而非 0,保证i < l < r,三个下标互不重复; - 双指针循环条件必须是
l < r,而非l <= r,避免指针重合导致同一元素被使用两次。
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int n=nums.length;
Arrays.sort(nums); //排序
List<List<Integer>> list=new ArrayList<>();
//固定一个数cnt,只需要找两数之和=-nums[i]
for(int i=0;i<n;i++){
//正数,不用计算
if(nums[i]>0) break;
//重复元素不用计算了
if(i>0&&nums[i]==nums[i-1]) continue;
int target=-nums[i];
int l=i+1;
int r=n-1;
while(l<r){
int sum=nums[l]+nums[r];
//找到
if(sum==target){
list.add(Arrays.asList(nums[l],nums[r],nums[i])); //添加三元组
//忽略重复值(重复三元组)
while(l<r&&nums[l]==nums[l+1]) l++;
while(l<r&&nums[r]==nums[r-1]) r--;
//下一组
l++;
r--;
}else if(sum<target){
l++;
}else{
r--;
}
}
}
return list;
}
}
18. 四数之和
最优解法:排序 + 双层循环 + 双指针(O (n³) 时间 + O (logn) 空间)
核心思路(三数之和的扩展)
四数之和本质是「固定两个数 + 两数之和」,复用三数之和的排序 + 双指针逻辑,核心步骤:
- 排序数组:解决重复问题,为双指针调整和提供基础;
- 双层循环固定前两个数:
-
- 外层循环固定第一个数
nums[i],跳过重复值; - 内层循环固定第二个数
nums[j](j > i),跳过重复值;
- 外层循环固定第一个数
- 双指针找后两个数 :对每个
(i,j),左指针l = j+1,右指针r = n-1,找nums[l] + nums[r] = target - nums[i] - nums[j]; - 去重 + 边界优化:每层循环都跳过重复值,添加合理的提前终止条件提升性能。
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
int n = nums.length;
if (n < 4) return result; // 不足4个数,直接返回空
// 步骤1:排序数组(去重+双指针基础)
Arrays.sort(nums);
// 步骤2:外层循环固定第一个数nums[i]
for (int i = 0; i < n - 3; i++) {
// 优化1:重复的第一个数,跳过(避免重复四元组)
if (i > 0 && nums[i] == nums[i - 1]) continue;
// 优化2:当前最小四数和 > target,后续不可能满足,终止循环
long minSum1 = (long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3];
if (minSum1 > target) break;
// 优化3:当前最大四数和 < target,跳过当前i,继续下一个
long maxSum1 = (long) nums[i] + nums[n - 1] + nums[n - 2] + nums[n - 3];
if (maxSum1 < target) continue;
// 步骤3:内层循环固定第二个数nums[j]
for (int j = i + 1; j < n - 2; j++) {
// 优化1:重复的第二个数,跳过
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
// 优化2:当前最小四数和 > target,终止内层循环
long minSum2 = (long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2];
if (minSum2 > target) break;
// 优化3:当前最大四数和 < target,跳过当前j,继续下一个
long maxSum2 = (long) nums[i] + nums[j] + nums[n - 1] + nums[n - 2];
if (maxSum2 < target) continue;
// 步骤4:双指针找后两个数
int l = j + 1;
int r = n - 1;
long remain = (long) target - nums[i] - nums[j]; // 后两数需要的和
while (l < r) {
long sum = (long) nums[l] + nums[r];
if (sum == remain) {
// 找到有效四元组,加入结果集
result.add(Arrays.asList(nums[i], nums[j], nums[l], nums[r]));
// 跳过左指针重复值
while (l < r && nums[l] == nums[l + 1]) l++;
// 跳过右指针重复值
while (l < r && nums[r] == nums[r - 1]) r--;
// 移动指针找下一组
l++;
r--;
} else if (sum < remain) {
l++; // 和太小,左指针右移
} else {
r--; // 和太大,右指针左移
}
}
}
}
return result;
}
}
42. 接雨水(hard)
动态规划的思路(双指针初步版本)
1. 核心原理
每个位置能承接的雨水量由「左右两侧最高柱子的较小值」决定,
公式为: **当前位置接水量 = min(左侧最高高度, 右侧最高高度) - 当前柱子高度**(若结果为负则取 0,因代码中左右最高包含自身,差值天然≥0)。
2. 三步核心实现

步骤 1:预处理左最高数组(lMax)
- 定义:
lMax[i]表示从数组起始位置到下标i(包含i)的所有柱子的最大高度; - 初始化:
lMax[0] = height[0](第一个位置的左最高就是自身); - 遍历规则:从左到右遍历,
lMax[i] = Math.max(lMax[i-1], height[i])(当前左最高 = 前一位置左最高 和 当前柱子高度的较大值)。
步骤 2:预处理右最高数组(rMax)
- 定义:
rMax[i]表示从下标i(包含i)到数组末尾的所有柱子的最大高度; - 初始化:
rMax[n-1] = height[n-1](最后一个位置的右最高就是自身); - 遍历规则:从右到左遍历,
rMax[i] = Math.max(rMax[i+1], height[i])(当前右最高 = 后一位置右最高 和 当前柱子高度的较大值)。
步骤 3:计算总接水量
- 遍历每个位置
i,按核心公式计算该位置接水量,并累加至总和; - 因
lMax/iMax均包含当前位置自身,min(lMax[i], rMax[i]) ≥ height[i],差值无需额外判断正负,直接累加即可。
java
class Solution {
//核心思路方法
public int trap(int[] height) {
int n=height.length;
int[] lMax=new int[n]; //左最高数组
int[] rMax=new int[n]; //左最高数组
//统计左最高数组
lMax[0]=height[0];
for(int i=1;i<n;i++){
lMax[i]=Math.max(lMax[i-1],height[i]);
}
//统计右最高数组
rMax[n-1]=height[n-1];
for(int i=n-2;i>=0;i--){
rMax[i]=Math.max(rMax[i+1],height[i]);
}
//接雨水
//该格子接的水=min(左最高,右最高)-格子高度
int sum=0;
for(int i=0;i<n;i++){
sum+=Math.min(lMax[i],rMax[i])-height[i];//包含自己的话,最小都是0
}
return sum;
}
}
可以简单优化,
最优解法:双指针法(O (n) 时间 + O (1) 空间,面试首选)
暴力法需要提前预处理左右最高数组(O (n) 空间),双指针法通过**「贪心」在遍历过程中动态维护左右最高值,无需额外空间:**
-
初始化左指针
left=0、右指针right=len(height)-1; -
维护
leftMax(左指针左侧的最高高度)、rightMax(右指针右侧的最高高度); -
核心贪心规则:
-
- 若
height[left] < height[right]:当前位置的接水量由「左最高」决定(因为右侧必有更高的柱子),本质是取(左最大,右最大)两者最小值,目前右还走完,左都比比过人家;
- 若
-
-
- 若
height[left] >= leftMax:更新leftMax(当前柱子更高,无法接水); - 否则:累加
leftMax - height[left](当前位置能接的雨水量); - 左指针右移;
- 若
-
-
- 反之:当前位置的接水量由「右最高」决定;
-
-
- 若
height[right] >= rightMax:更新rightMax; - 否则:累加
rightMax - height[right]; - 右指针左移;
- 若
-
-
遍历至
left >= right结束,累加的总和即为总雨水量。
java
class Solution {
//双指针优化+贪心
public int trap(int[] height) {
int n=height.length;
int sum=0;
int l=0,r=n-1;
int lMax=0,rMax=0;
//
while(l<r){
//先处理小
if(height[l]<height[r]){
//更新左最大
if(height[l]>lMax) lMax=height[l];
//此时h[l]<h[r]) 当前<左最大(可以接雨水),积累差值
else sum+=lMax-height[l];
l++;
}else{
//更新右最大
if(height[r]>rMax) rMax=height[r];
//此时h[l]<h[r]) 当前<左最大(可以接雨水),积累差值
else sum+=rMax-height[r];
r--;
}
}
return sum;
}
}
75. 颜色分类(荷兰国旗)
思路:三指针法,left左侧永远都是0,right右侧永远都是2,左右侧都确定好了,那么中间的就自然全是1了(此问题是 荷兰国旗 问题:0 - 红,1 - 白,2 - 蓝 排序)。
具体步骤如下:
-
定义三指针
left, right, i := 0, len(nums) - 1, 0,遍历数组for i <= right。注意循环条件:原本是 i <= len(nums),但 right 指针右侧已经都是2了,没必要继续寻找。因此以越过右指针为终止条件,减少查找次数。 -
判断
nums[i]的值: -
- 若是 0,则移动到表头:
swap(nums[i], nums[left]),left++,i++注意:nums[left]已经在 i 向右遍历的过程中早就验证过了,所以i要加加右移 - 若是 1,则继续:
i++ - 若是 2,则移动到表尾:
swap(nums[i], nums[right]),right--。注意:这里不用i++,因为nums[right])交换到nums[i]上的数还没有验证(有可能是0或2),所以 i 不用右移。
- 若是 0,则移动到表头:
荷兰国旗(三指针-一次遍历)
- 核心思路
用三个指针划分三个区间,遍历过程中动态调整区间边界:
left:0 的右边界([0, left)全是 0);curr:当前遍历指针([left, curr)全是 1);right:2 的左边界((right, n-1]全是 2);- 未处理区间:
[curr, right],遍历完成时该区间为空。
注意:
交换 0 时,left 位置的数要么是 1(已担保的中间区间),要么是初始的 0(自己),都是「确定正确的数」,所以 curr 可以直接 ++;交换 2 时,right 位置的数是未检查的未知值,所以 curr 必须留在原地重新检查。
java
class Solution {
//计数排序法
public void sortColors(int[] nums) {
int n=nums.length;
//分三部分 0 1 2
// [l,cur) 全是0 [cur,r) 全是1 [right,n-1] 是2
int l=0,cur=0,r=n-1;
//[cur,right] 待处理区间
while(cur<=r){
if(nums[cur]==0){
//把0归位
swap(nums,cur,l);
l++; //l可能超过原来1在的左区间
cur++;
//如果交换来的是1(或初始0),无需再检查
}else if(nums[cur]==1){
cur++; //1在正确位置上
}else{
swap(nums,cur,r);
r--;
// 交换来的数可能是0/1/2,需重新检查curr,故curr不++
}
}
}
// 辅助交换函数
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
计数排序法(两次遍历)
若面试中先想到该解法,可先写出,再优化为荷兰国旗算法:
java
class Solution {
//计数排序法
public void sortColors(int[] nums) {
int c=0,c1=0,c2=0;
//统计数字个数
for(int num:nums){
if(num==0) c++;
else if(num==1) c1++;
else c2++;
}
//填充
int idx=0;
while(c-->0) nums[idx++]=0;
while(c1-->0) nums[idx++]=1;
while(c2-->0) nums[idx++]=2;
}
}
| 维度 | 计数排序 | 荷兰国旗算法(三指针) |
|---|---|---|
| 核心逻辑 | 统计次数 → 回填数组 | 指针划分区间 → 一次遍历归位 |
| 遍历次数 | 两次(统计 + 回填) | 一次 |
| 空间复杂度 | 通用版 O (n+k),简化版 O (1) | O (1)(仅指针变量) |
| 适用场景 | 取值范围小,允许两次遍历 | 要求一次遍历,仅 3 种值的划分 |
| 本质 | 非比较排序,靠统计次数排序 | 指针操作,靠区间划分排序 |
总结
- 计数排序:是一种通用的非比较排序,核心是「统计次数 + 回填」,适合取值范围有限的整数数组,颜色分类中是简化版的原地实现;
- 荷兰国旗问题:是特定场景的算法问题,要求一次遍历将 3 值数组划分为 3 个连续区间,解法是三指针法,也是颜色分类的最优解;
- 面试技巧:被问到颜色分类时,可先讲计数排序(易理解),再讲荷兰国旗算法(最优解),体现思考的完整性。