Java双指针 - 附LeetCode 经典题解

Java 双指针 - 附LeetCode 经典题解

一、什么是双指针?

1.1 形象理解

想象你和朋友一起在操场跑步:

  • 对撞指针:你们从操场两端相向而跑,最终在中间相遇
  • 快慢指针:你跑得快,朋友跑得慢,你们从同一起点出发
  • 滑动窗口:你们保持一定距离,一起向前移动

在算法中,双指针就是:

  • 指针:数组或链表中的索引/引用
  • :同时使用两个指针
  • 目的:通过协同移动,在 O(n) 时间内解决问题

1.2 双指针的本质

双指针其实就是用两个变量追踪不同位置,避免嵌套循环:

tex 复制代码
数组:[1, 2, 3, 4, 5, 6, 7, 8]
      ↑                    ↑
     left                right

Java 特别注意: 在 Java 中,数组索引从 0 开始,链表使用 node.next 移动指针。


1.3 双指针的核心思想

核心: 通过两个指针的协同移动,将 O(n²) 的暴力解法优化为 O(n)。

关键点:

  1. 确定指针的初始位置
  2. 确定指针的移动规则
  3. 确定指针的终止条件

1.4 双指针的三大类型

1.4.1 对撞指针(相向双指针)

特点: 两个指针从两端向中间移动,直到相遇。

适用场景:

  • 有序数组的查找问题
  • 反转数组/字符串
  • 两数之和、三数之和
  • 判断回文串(如果是数字的话,建议用反转后半部分同前半部分比较来判断)

模板:

java 复制代码
public void collisionPointer(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    
    while (left < right) {
        // 根据条件移动指针
        if (满足某条件) {
            left++;
        } else {
            right--;
        }
    }
}

图解:

tex 复制代码
数组:[1, 2, 3, 4, 5, 6, 7, 8]

步骤1:left=0, right=7  →  [1] ... [8]
步骤2:left=1, right=6  →  [2] ... [7]
步骤3:left=2, right=5  →  [3] ... [6]
步骤4:left=3, right=4  →  [4] ... [5]
步骤5:left=4, right=3  →  相遇,结束

1.4.2 快慢指针(同向双指针)

特点: 两个指针从同一端出发,以不同速度移动。

适用场景:

  • 链表环检测(Floyd 判圈算法)
  • 链表中点查找
  • 数组去重
  • 删除元素

模板:

java 复制代码
public void fastSlowPointer(int[] nums) {
    int slow = 0;
    int fast = 0;
    
    while (fast < nums.length) {
        if (满足某条件) {
            nums[slow] = nums[fast];
            slow++;
        }
        fast++;
    }
}

图解:

tex 复制代码
数组:[1, 1, 2, 2, 3, 3, 4, 4]

步骤1:slow=0, fast=0  →  [1] ...
步骤2:slow=1, fast=1  →  [1] [1] ...(重复,slow不动)
步骤3:slow=1, fast=2  →  [1] [2] ...(不重复,slow移动)
步骤4:slow=2, fast=3  →  [1] [2] [2] ...(重复,slow不动)
...

1.4.3 滑动窗口(特殊的双指针)

博主的下一篇文章会详细解释滑动窗口,在这里仅作简单解释。

特点: 两个指针维护一个窗口,窗口大小可变或固定。

适用场景:

  • 子串/子数组问题
  • 最长/最短满足条件的序列
  • 字符串匹配

模板:

java 复制代码
public int slidingWindow(int[] nums) {
    int left = 0, right = 0;
    int result = 0;
    
    while (right < nums.length) {
        // 扩大窗口
        right++;
        
        // 收缩窗口
        while (窗口不满足条件) {
            left++;
        }
        
        // 更新结果
        result = Math.max(result, right - left);
    }
    
    return result;
}

1.4.4 对比总结
特性 对撞指针 快慢指针 滑动窗口
起点 两端 同一端 同一端
方向 相向 同向 同向
速度 相同/不同 不同 不同
典型题目 两数之和 链表环检测 最长子串
难度 简单/中等 中等 中等/困难

记忆技巧:

  • 对撞指针:像两辆车相向而行
  • 快慢指针:像龟兔赛跑
  • 滑动窗口:像火车车厢移动

二、LeetCode 经典题解

2.1 对撞指针:两数之和 II(LeetCode 167)

题目描述

给你一个下标从 1 开始的整数数组 numbers,该数组已按非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。

示例:

复制代码
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9。因此 index1 = 1, index2 = 2。

解题思路

这是典型的对撞指针问题。

核心思想:

  1. 左指针指向数组开头,右指针指向数组末尾
  2. 计算两数之和
  3. 如果和等于 target,返回结果
  4. 如果和小于 target,左指针右移(增大和)
  5. 如果和大于 target,右指针左移(减小和)

为什么可以用对撞指针?

  • 数组已排序,左边的数小,右边的数大
  • 通过移动指针,可以有序地调整和的大小

完整代码

java 复制代码
public class Solution {
    public int[] twoSum(int[] numbers, int target) {
        // 1. Java 中必须先判断 null
        if (numbers == null || numbers.length < 2) {
            return new int[0];
        }
        
        // 2. 初始化对撞指针
        int left = 0;
        int right = numbers.length - 1;
        
        // 3. 对撞指针移动
        while (left < right) {
            int sum = numbers[left] + numbers[right];
            
            if (sum == target) {
                // 题目要求下标从 1 开始
                return new int[]{left + 1, right + 1};
            } else if (sum < target) {
                left++;  // 和太小,左指针右移
            } else {
                right--;  // 和太大,右指针左移
            }
        }
        
        return new int[0];  // 未找到
    }
}

时间复杂度: O(n)
空间复杂度: O(1)


2.2 对撞指针:三数之和(LeetCode 15)

题目描述

给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k,同时还满足 nums[i] + nums[j] + nums[k] == 0

示例:

复制代码
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

解题思路

这是典型的对撞指针问题,是两数之和的进阶版,思路大致一致,但是难度稍大一点

核心思想:

  1. 先对数组排序
  2. 固定一个数 nums[i]
  3. 用对撞指针在剩余数组中找两数之和等于 -nums[i]
  4. 去重处理

完整代码

java 复制代码
import java.util.*;

public class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        
        if (nums == null || nums.length < 3) {
            return result;
        }
        
        // 1. 排序(Java 中使用 Arrays.sort())
        Arrays.sort(nums);
        
        // 2. 固定第一个数
        for (int i = 0; i < nums.length - 2; i++) {
            // 去重:跳过重复的第一个数
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            
            // 3. 对撞指针找另外两个数
            int left = i + 1;
            int right = nums.length - 1;
            int target = -nums[i];
            
            while (left < right) {
                int sum = nums[left] + nums[right];
                
                if (sum == target) {
                    result.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    
                    // 去重:跳过重复的第二个数
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }
                    // 去重:跳过重复的第三个数
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }
                    
                    left++;
                    right--;
                } else if (sum < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        
        return result;
    }
}

时间复杂度: O(n²)
空间复杂度: O(1)(不计结果数组)


2.3 快慢指针:删除有序数组中的重复项(LeetCode 26)

题目描述

给你一个非严格递增排列 的数组 nums,请你原地 删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。

示例:

tex 复制代码
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2,并且原数组 nums 的前两个元素被修改为 1, 2。

解题思路

这是典型的快慢指针问题。

核心思想:

  1. 慢指针指向不重复元素的末尾
  2. 快指针遍历数组
  3. 当快指针指向的元素与慢指针不同时,将快指针的元素复制到慢指针的下一个位置

完整代码

java 复制代码
public class Solution {
    public int removeDuplicates(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        // 1. 初始化快慢指针
        int slow = 0;  // 慢指针:不重复元素的末尾
        
        // 2. 快指针遍历数组
        for (int fast = 1; fast < nums.length; fast++) {
            // 3. 如果快指针指向的元素与慢指针不同
            if (nums[fast] != nums[slow]) {
                slow++;
                nums[slow] = nums[fast];
            }
        }
        
        // 4. 返回新长度
        return slow + 1;
    }
}

时间复杂度: O(n)
空间复杂度: O(1)


2.4 快慢指针:链表环检测(LeetCode 141)

** 题目描述**

给你一个链表的头节点 head,判断链表中是否有环。

示例:

tex 复制代码
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

解题思路

这是经典的 Floyd 判圈算法(龟兔赛跑算法)。

核心思想:

  1. 快指针每次走两步,慢指针每次走一步
  2. 如果有环,快指针最终会追上慢指针
  3. 如果无环,快指针会先到达链表末尾

完整代码

java 复制代码
public class Solution {
    public boolean hasCycle(ListNode head) {
        // 1. 边界判断
        if (head == null || head.next == null) {
            return false;
        }
        
        // 2. 初始化快慢指针
        ListNode slow = head;
        ListNode fast = head;
        
        // 3. 快慢指针移动
        while (fast != null && fast.next != null) {
            slow = slow.next;        // 慢指针走一步
            fast = fast.next.next;   // 快指针走两步
            
            // 4. 如果相遇,说明有环
            if (slow == fast) {
                return true;
            }
        }
        
        // 5. 快指针到达末尾,说明无环
        return false;
    }
}

// Java 链表节点定义
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) {
        val = x;
        next = null;
    }
}

时间复杂度: O(n)
空间复杂度: O(1)


2.5 快慢指针:链表的中间节点(LeetCode 876)

题目描述

给你单链表的头节点 head,请你找出并返回链表的中间节点。如果有两个中间节点,则返回第二个中间节点。

示例:

tex 复制代码
输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间节点,值为 3。

完整代码

java 复制代码
public class Solution {
    public ListNode middleNode(ListNode head) {
        if (head == null) {
            return null;
        }
        
        // 快慢指针
        ListNode slow = head;
        ListNode fast = head;
        
        // 快指针走两步,慢指针走一步
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        
        // 当快指针到达末尾时,慢指针正好在中间
        return slow;
    }
}

时间复杂度: O(n)
空间复杂度: O(1)


2.6 对撞指针:反转字符串(LeetCode 344)

题目描述

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

示例:

tex 复制代码
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

完整代码

java 复制代码
public class Solution {
    public void reverseString(char[] s) {
        if (s == null || s.length == 0) {
            return;
        }
        
        // 对撞指针
        int left = 0;
        int right = s.length - 1;
        
        while (left < right) {
            // 交换
            char temp = s[left];
            s[left] = s[right];
            s[right] = temp;
            
            left++;
            right--;
        }
    }
}

时间复杂度: O(n)
空间复杂度: O(1)


三、Java 易错点总结

3.1 错误一:链表空指针异常

java 复制代码
// 错误写法:未判断 null
public boolean hasCycle(ListNode head) {
    ListNode slow = head;
    ListNode fast = head.next.next;  // 如果 head.next 是 null,抛出 NullPointerException
}

// 正确写法:先判断 null
public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    
    ListNode slow = head;
    ListNode fast = head;
    
    while (fast != null && fast.next != null) {  // 必须同时判断
        slow = slow.next;
        fast = fast.next.next;
    }
}

3.2 错误二:数组越界

java 复制代码
// 错误写法:未判断边界
int left = 0;
int right = nums.length;  // 应该是 nums.length - 1
while (left < right) {
    int sum = nums[left] + nums[right];  // 当 right = nums.length 时,数组越界
}

// 正确写法
int left = 0;
int right = nums.length - 1;  // Java 数组下标从 0 到 length-1

3.3 错误三:忘记去重

java 复制代码
// 错误写法:未去重,导致结果包含重复的三元组
for (int i = 0; i < nums.length; i++) {
    // 未跳过重复的第一个数
}

// 正确写法:去重
for (int i = 0; i < nums.length; i++) {
    if (i > 0 && nums[i] == nums[i - 1]) {
        continue;  // 跳过重复的第一个数
    }
}

3.4 错误四:快慢指针初始化错误

java 复制代码
// 错误写法:快指针初始化错误
ListNode slow = head;
ListNode fast = head.next;  // 这样会导致判断逻辑复杂

// 正确写法:快慢指针都从 head 开始
ListNode slow = head;
ListNode fast = head;

3.5 错误五:交换元素时未使用临时变量

java 复制代码
// 错误写法:直接赋值会丢失数据
s[left] = s[right];
s[right] = s[left];  // 此时 s[left] 已经被修改,s[right] 也变成了原来的 s[right]

// 正确写法:使用临时变量
char temp = s[left];
s[left] = s[right];
s[right] = temp;

四、Java 双指针通用模板

4.1 对撞指针模板

java 复制代码
public class CollisionPointerTemplate {
    // Java 对撞指针通用模板
    public void collisionPointer(int[] nums) {
        if (nums == null || nums.length == 0) {
            return;
        }
        
        int left = 0;
        int right = nums.length - 1;
        
        while (left < right) {
            // 根据条件移动指针
            if (满足某条件) {
                // 处理逻辑
                left++;
            } else {
                right--;
            }
        }
    }
}

4.2 快慢指针模板(数组)

java 复制代码
public class FastSlowPointerTemplate {
    // Java 快慢指针通用模板(数组)
    public int fastSlowPointer(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int slow = 0;
        
        for (int fast = 0; fast < nums.length; fast++) {
            if (满足某条件) {
                nums[slow] = nums[fast];
                slow++;
            }
        }
        
        return slow;
    }
}

4.3 快慢指针模板(链表)

java 复制代码
public class FastSlowPointerListTemplate {
    // Java 快慢指针通用模板(链表)
    public ListNode fastSlowPointer(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            
            // 根据题目需求处理
            if (slow == fast) {
                return slow;
            }
        }
        
        return null;
    }
}

五、Java 刷题优化技巧

5.1 数组排序

java 复制代码
// Java 中对数组排序
Arrays.sort(nums);  // 升序排序,时间复杂度 O(n log n)

// 降序排序(需要转换为 Integer[])
Integer[] arr = Arrays.stream(nums).boxed().toArray(Integer[]::new);
Arrays.sort(arr, Collections.reverseOrder());

5.2 链表操作

java 复制代码
// 创建虚拟头节点(简化边界处理)
ListNode dummy = new ListNode(0);
dummy.next = head;

// 删除节点
prev.next = prev.next.next;

// 反转链表
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
    ListNode next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
}

5.3 避免整数溢出

java 复制代码
// 可能溢出
int sum = nums[left] + nums[right];
if (sum == target) { ... }

// 避免溢出(如果数值很大)
long sum = (long) nums[left] + nums[right];
if (sum == target) { ... }

六、总结

6.1 核心要点

  1. 双指针 = 两个变量协同移动
  2. 对撞指针:从两端向中间移动
  3. 快慢指针:从同一端以不同速度移动
  4. 滑动窗口:维护一个可变/固定大小的窗口
  5. 时间复杂度通常是 O(n)
  6. 空间复杂度通常是 O(1)

6.2 记忆口诀

tex 复制代码
对撞指针,两端相向
快慢指针,同向不同速
滑动窗口,维护区间
数组排序,链表判空
去重处理,避免重复
Java 中,先判 null

6.3 同类题推荐

题号 题目 难度 指针类型 核心技巧
167 两数之和 II 简单 对撞指针 有序数组
15 三数之和 中等 对撞指针 排序 + 去重
11 盛最多水的容器 中等 对撞指针 贪心
26 删除有序数组中的重复项 简单 快慢指针 原地修改
141 环形链表 简单 快慢指针 Floyd 判圈
142 环形链表 II 中等 快慢指针 找环入口
876 链表的中间节点 简单 快慢指针 找中点
344 反转字符串 简单 对撞指针 交换
125 验证回文串 简单 对撞指针 字符判断

希望这篇文章能帮你彻底掌握 Java 双指针算法!记住:对撞指针从两端,快慢指针同起点。多刷题,多总结,模板用熟了就能秒杀同类题。加油!

作者:[识君啊]

不要做API的搬运工,要做原理的探索者!

相关推荐
java1234_小锋1 小时前
分享一套优质的SpringBoot4+Vue3学生信息管理系统
java·vue.js·spring boot·学生信息
_F_y1 小时前
子序列系列动态规划
算法·动态规划
g***27991 小时前
knife4j+springboot3.4异常无法正确展示文档
java
田里的水稻1 小时前
FA_规划和控制(PC)-A*(规划01)
人工智能·算法·数学建模·机器人·自动驾驶
twilight_4691 小时前
机器学习与模式识别——Logistic算法
人工智能·算法·机器学习
weisian1512 小时前
JVM--10-JVM实战部署全指南:从`java -jar`到生产级高可用
java·jvm·jar·gc
人道领域2 小时前
Maven多模块开发:高效构建复杂项目
java·开发语言·spring boot·maven
ArturiaZ2 小时前
【day28】
开发语言·c++·算法
致Great2 小时前
使用 GRPO 算法训练多智能体系统:实现可靠的长期任务规划与执行
人工智能·算法·agent·智能体