【算法篇】1.双指针

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 原地修改)核心思路

  1. 核心目标

在不额外开辟数组的前提下,将原数组中每个0原地复制一次(后续元素右移,超出数组长度的部分舍弃)。

  1. 两步核心实现

步骤 1:统计需要复制的 0 的总数(cnt),处理边界越界

  • 遍历数组,用cnt记录遇到的0的个数(每个0会让后续元素多右移一位);
  • 遍历终止条件:i + cnt ≤ nn为数组最后一个下标,保证元素不越界);
  • 边界处理:若遍历中遇到i + cnt == n(当前0的复制会超出数组长度),则直接将数组最后一位设为0,并将n减 1(舍弃越界的复制),终止统计。

步骤 2:从后往前原地修改数组(避免覆盖未处理元素)

  • 确定起始修改位置:j = n - cnt(最后一个未被移位的原始元素下标);

  • j向前遍历:

    • 若当前元素是0:先将arr[i+cnt]设为0cnt减 1,再将arr[i+cnt]设为0(完成 0 的复制);
    • 若当前元素非 0:将arr[i+cnt]设为当前元素(完成元素右移)。

关键点回顾

  1. 核心技巧:先统计 0 的个数确定移位偏移量,再从后往前修改(避免正向遍历覆盖未处理元素);
  2. 边界处理:单独处理最后一个 0 的越界情况,保证数组长度不变;
  3. 原地修改:利用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 → 必然进入无限循环(因为数字的平方和取值范围有限)。

用「快慢指针」检测循环,逻辑和链表找环完全一致:

  1. 慢指针(slow):每次计算 1 次平方和(走 1 步);
  2. 快指针(fast):每次计算 2 次平方和(走 2 步);
  3. slow == fast:说明进入循环,此时若值为 1 则是快乐数,否则不是;
  4. 若快指针先到 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. 核心思路

利用三角形三边规则的简化条件,结合排序和双指针缩小搜索范围:

  1. 排序数组 :将数组升序排列,方便固定最大数 c,并在左侧找满足 a + b > ca、b
  2. 固定最大数 :遍历数组,将 i 作为最大数 c 的下标(从 2 开始,因为至少需要 3 个数);
  3. 双指针找有效对 :对于每个 i,左指针 left 初始为 0,右指针 right 初始为 i-1
    • nums[left] + nums[right] > nums[i]:说明 leftright-1 的所有数与 right 组合都满足条件(共 right-left 个),right--
    • nums[left] + nums[right] ≤ nums[i]:需要增大和,left++
  1. 累加所有有效组合数,即为答案。
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. 三数之和

核心思路

  1. 排序预处理:对数组升序排序,为双指针调整和、去重、提前终止提供基础;

  2. 固定单个数 :遍历数组固定第一个数 nums[i],将三数和为 0 转化为找「i右侧两数之和 = -nums[i]」的问题,同时跳过重复的nums[i]、遇到正数直接终止遍历(性能优化);

  3. 双指针找两数 :用左指针(i+1)、右指针(数组末尾)在i右侧区间遍历,通过移动指针调整两数之和:和小则左指针右移,和大则右指针左移,找到目标和时记录三元组并跳过指针重复值,避免重复结果。

  4. 去重相关(避免重复三元组,核心易错点)

  • 固定数去重 :必须跳过 i>0 && nums[i]==nums[i-1] 的情况,否则同一固定数会生成重复三元组(如连续两个 - 1 会重复找到 [-1,0,1]);
  • 双指针去重 :找到有效三元组后,需跳过 nums[l]==nums[l+1](左指针)和 nums[r]==nums[r-1](右指针)的重复值,且必须先判断 l<r 再检查重复(避免数组越界);
  • 去重时机:固定数的去重在遍历开始时,双指针去重在找到有效三元组后,顺序不可颠倒。
  1. 下标合法性(避免重复使用元素)
  • 左指针必须初始化为 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) 空间)

核心思路(三数之和的扩展)

四数之和本质是「固定两个数 + 两数之和」,复用三数之和的排序 + 双指针逻辑,核心步骤:

  1. 排序数组:解决重复问题,为双指针调整和提供基础;
  2. 双层循环固定前两个数
    • 外层循环固定第一个数 nums[i],跳过重复值;
    • 内层循环固定第二个数 nums[j]j > i),跳过重复值;
  1. 双指针找后两个数 :对每个 (i,j),左指针 l = j+1,右指针 r = n-1,找 nums[l] + nums[r] = target - nums[i] - nums[j]
  2. 去重 + 边界优化:每层循环都跳过重复值,添加合理的提前终止条件提升性能。
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 不用右移。

荷兰国旗(三指针-一次遍历)

  1. 核心思路

用三个指针划分三个区间,遍历过程中动态调整区间边界:

  • 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 种值的划分
本质 非比较排序,靠统计次数排序 指针操作,靠区间划分排序

总结

  1. 计数排序:是一种通用的非比较排序,核心是「统计次数 + 回填」,适合取值范围有限的整数数组,颜色分类中是简化版的原地实现;
  2. 荷兰国旗问题:是特定场景的算法问题,要求一次遍历将 3 值数组划分为 3 个连续区间,解法是三指针法,也是颜色分类的最优解;
  3. 面试技巧:被问到颜色分类时,可先讲计数排序(易理解),再讲荷兰国旗算法(最优解),体现思考的完整性。
相关推荐
倦王2 小时前
Dify的部署(详细步骤一步一步)
人工智能
你这个代码我看不懂2 小时前
Java软引用对象的创建以及对象回收
java·开发语言
qq_417695052 小时前
C++中的中介者模式
开发语言·c++·算法
wengqidaifeng2 小时前
备战蓝桥杯----C/C++组 (一)数据结构与STL讲解(上):顺序表、链表、栈与队列——从手写到调用,一文搞懂四种线性结构
c语言·数据结构·蓝桥杯
Rolei_zl2 小时前
AIGC(生成式AI)试用 48 -- AI与软件开发过程3
python·aigc
一水鉴天2 小时前
整体设计 设计文档修订与重构修改稿 (豆包助手)20260321
人工智能·重构
开开心心就好2 小时前
免费无广告的礼金记账本,安卓应用
java·前端·ubuntu·edge·pdf·负载均衡·语音识别
qq_416018722 小时前
持续集成/持续部署(CI/CD) for Python
jvm·数据库·python