双指针算法详解:从入门到精通(Java版)

目录

一、什么是双指针?

[1.1 对撞指针(左右指针)](#1.1 对撞指针(左右指针))

[1.2 快慢指针(龟兔赛跑算法)](#1.2 快慢指针(龟兔赛跑算法))

二、经典题目实战

[2.1 移动零(LeetCode 283)](#2.1 移动零(LeetCode 283))

[2.2 复写零(LeetCode 1089)](#2.2 复写零(LeetCode 1089))

[2.3 快乐数(LeetCode 202)](#2.3 快乐数(LeetCode 202))

[2.4 盛水最多的容器(LeetCode 11)](#2.4 盛水最多的容器(LeetCode 11))

[2.5 有效三角形的个数(LeetCode 611)](#2.5 有效三角形的个数(LeetCode 611))

[2.6 和为s的两个数字(剑指Offer 57)](#2.6 和为s的两个数字(剑指Offer 57))

[2.7 三数之和(LeetCode 15)](#2.7 三数之和(LeetCode 15))

[2.8 四数之和(LeetCode 18)](#2.8 四数之和(LeetCode 18))

三、双指针算法总结


一、什么是双指针?

双指针是一种常用的算法技巧,通过使用两个指针在数组或链表等数据结构上进行移动,来解决特定问题。根据指针移动方式的不同,双指针主要分为两种形式:

1.1 对撞指针(左右指针)

对撞指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近。

终止条件

  • left == right:两个指针指向同一个位置

  • left > right:两个指针错开

1.2 快慢指针(龟兔赛跑算法)

快慢指针使用两个移动速度不同的指针在序列结构上移动。通常在一次循环中,慢指针每次移动一位,快指针每次移动两位。

适用场景

  • 处理环形链表或数组

  • 研究出现循环往复的问题


二、经典题目实战

2.1 移动零(LeetCode 283)

题目描述 :给定一个数组 nums,将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。

算法思路

使用两个指针 curdestcur用于遍历数组,dest指向非零元素序列的最后一个位置。

java 复制代码
class Solution {
    public void moveZeroes(int[] nums) {
        for (int cur = 0, dest = -1; cur < nums.length; cur++) {
            if (nums[cur] != 0) {
                // 处理非零元素
                dest++;
                int temp = nums[dest];
                nums[dest] = nums[cur];
                nums[cur] = temp;
            }
        }
    }
}

核心思想 :遍历过程中,使 [0, dest]区间全为非零元素,[dest+1, cur-1]区间全为零。


2.2 复写零(LeetCode 1089)

题目描述:将数组中每个零都复写一遍,其余元素向右平移,不能超过数组长度。

算法思路

采用"从后向前"的复写策略,避免从前向后复写时数据被覆盖。

java 复制代码
class Solution {
    public void duplicateZeros(int[] arr) {
        int cur = 0, dest = -1, n = arr.length;
        
        // 1. 先找到最后一个需要复写的数
        while (cur < n) {
            if (arr[cur] == 0) {
                dest += 2;
            } else {
                dest += 1;
            }
            if (dest >= n - 1) break;
            cur++;
        }
        
        // 2. 处理边界情况
        if (dest == n) {
            arr[n - 1] = 0;
            cur--;
            dest -= 2;
        }
        
        // 3. 从后向前完成复写操作
        while (cur >= 0) {
            if (arr[cur] != 0) {
                arr[dest--] = arr[cur--];
            } else {
                arr[dest--] = 0;
                arr[dest--] = 0;
                cur--;
            }
        }
    }
}

2.3 快乐数(LeetCode 202)

题目描述:判断一个数是否为快乐数。快乐数定义为:每次将该数替换为它每个位置上的数字的平方和,重复这个过程直到变为1,或进入无限循环。

算法思路

使用快慢指针检测循环。如果相遇位置的值是1,则是快乐数;否则不是。

java 复制代码
class Solution {
    public boolean isHappy(int n) {
        int slow = n, fast = bitSum(n);
        while (slow != fast) {
            slow = bitSum(slow);
            fast = bitSum(bitSum(fast));
        }
        return slow == 1;
    }
    
    private int bitSum(int n) {
        int sum = 0;
        while (n != 0) {
            int t = n % 10;
            sum += t * t;
            n /= 10;
        }
        return sum;
    }
}

2.4 盛水最多的容器(LeetCode 11)

题目描述:找出两条垂线,使它们与x轴构成的容器能容纳最多的水。

算法思路

使用对撞指针,每次移动较短的边界,因为移动较长边界只会使容积变小。

java 复制代码
class Solution {
    public int maxArea(int[] height) {
        int left = 0, right = height.length - 1, ret = 0;
        while (left < right) {
            int v = Math.min(height[left], height[right]) * (right - left);
            ret = Math.max(ret, v);
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        return ret;
    }
}

2.5 有效三角形的个数(LeetCode 611)

题目描述:返回数组中可以组成三角形三条边的三元组个数。

算法思路

先排序,固定最长边,使用对撞指针在剩余区间中寻找满足两边之和大于第三边的组合。

java 复制代码
class Solution {
    public int triangleNumber(int[] nums) {
        // 1. 排序
        Arrays.sort(nums);
        
        // 2. 利用双指针解决问题
        int ret = 0, n = nums.length;
        for (int i = n - 1; i >= 2; i--) {  // 固定最大的数
            int left = 0, right = i - 1;
            while (left < right) {
                if (nums[left] + nums[right] > nums[i]) {
                    ret += right - left;
                    right--;
                } else {
                    left++;
                }
            }
        }
        return ret;
    }
}

2.6 和为s的两个数字(剑指Offer 57)

题目描述:在递增排序的数组中查找两个数,使它们的和等于目标值。

算法思路

利用数组有序的特性,使用对撞指针,根据当前和与目标值的大小关系移动指针。

java 复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left < right) {
            int sum = nums[left] + nums[right];
            if (sum > target) {
                right--;
            } else if (sum < target) {
                left++;
            } else {
                return new int[]{nums[left], nums[right]};
            }
        }
        return new int[]{-1, -1};
    }
}

2.7 三数之和(LeetCode 15)

题目描述:找出数组中所有和为0且不重复的三元组。

算法思路

先排序,固定一个数,在其后的区间中使用双指针找两数之和等于目标值,注意去重。

java 复制代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ret = new ArrayList<>();
        
        // 1. 排序
        Arrays.sort(nums);
        
        // 2. 利用双指针解决问题
        int n = nums.length;
        for (int i = 0; i < n; ) {  // 固定数 a
            if (nums[i] > 0) break;  // 小优化
            
            int left = i + 1, right = n - 1, target = -nums[i];
            while (left < right) {
                int sum = nums[left] + nums[right];
                if (sum > target) {
                    right--;
                } else if (sum < target) {
                    left++;
                } else {
                    ret.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    left++;
                    right--;
                    
                    // 去重操作
                    while (left < right && nums[left] == nums[left - 1]) left++;
                    while (left < right && nums[right] == nums[right + 1]) right--;
                }
            }
            
            // 去重
            i++;
            while (i < n && nums[i] == nums[i - 1]) i++;
        }
        return ret;
    }
}

2.8 四数之和(LeetCode 18)

题目描述:找出数组中所有和为target且不重复的四元组。

算法思路

在三数之和的基础上再套一层循环,固定两个数,使用双指针找剩余两数。

java 复制代码
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]));
                        left++;
                        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;
    }
}

三、双指针算法总结

题目 指针类型 关键技巧
移动零 快慢指针 数组分块,维护非零区间
复写零 前后指针 从后向前避免覆盖
快乐数 快慢指针 检测循环
盛水容器 对撞指针 移动较短边界
三角形个数 对撞指针 排序后固定最长边
两数之和 对撞指针 利用有序性
三数之和 对撞指针 固定一个数+去重
四数之和 对撞指针 固定两个数+去重

双指针的核心思想:通过分析问题的二段性,利用指针移动来减少不必要的枚举,将时间复杂度从 O(n²) 优化到 O(n)。

使用技巧

  1. 对撞指针常用于有序数组或字符串

  2. 快慢指针常用于环形结构或需要检测循环的场景

  3. 注意边界条件和指针移动的时机

  4. 涉及去重问题时,注意跳过重复元素

相关推荐
土司大王6 小时前
LeetCode hot100——两两交换链表中的节点
算法·leetcode·职场和发展
大熊背7 小时前
树莓派IspPipeline LSC模块原理详解
算法·lsc·isppipeline·mesh lsc
测试19987 小时前
Selenium 无法定位元素的几种解决方案
自动化测试·软件测试·python·selenium·测试工具·职场和发展·测试用例
zander2588 小时前
LeetCode 300. 最长递增子序列
算法·leetcode·深度优先
欧叶冲冲冲8 小时前
Python常见数据结构的CRUD(LeetCode高频版速查)
数据结构·python·leetcode
祖力558 小时前
Linux应用软件编程:目录IO与framebuffer
linux·运维·算法·framebuffer·目录io
名字还没想好☜8 小时前
Go 用 slices/maps 标准库泛型函数:告别手写 Contains、Sort、去重(Go 1.21)
开发语言·后端·算法·golang·go
地平线开发者8 小时前
【模型轻量化专题】深度学习模型为什么需要轻量化
算法
圣保罗的大教堂9 小时前
leetcode 3622. 判断整除性 简单
leetcode
_Narcissus_9 小时前
链表算法题和静态链表
数据结构·c++·笔记·算法·链表·ai·力扣