LeetCode 算法题解:双指针专题
目录
26. 删除有序数组中的重复项
题目描述
给你一个 非严格递增排列 的数组 nums,请你 原地 删除重复出现的元素,使每个元素 只出现一次,返回删除后数组的新长度。
解法:双指针法
class Solution {
public int removeDuplicates(int[] nums) {
int n = nums.length;
int j = 0; // 慢指针
for (int i = 0; i < n; i++) { // i 是快指针
if (nums[i] != nums[j]) {
nums[++j] = nums[i];
}
}
return j + 1;
}
}
复杂度: 时间 O(n),空间 O(1)
27. 移除元素 ⭐
题目描述
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
关键区别: 与第 26 题不同,本题 不需要保持元素的相对顺序 ,且数组 无序。
示例
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2,_,_]
解释:返回 k = 2,nums 前两个元素为 2
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3,_,_,_]
解释:返回 k = 5,前五个元素为 0,0,1,3,4(顺序可任意)
解法:双指针(快慢指针)
思路
使用快慢指针:
-
快指针 :遍历数组,寻找不等于
val的元素 -
慢指针
idx:指向当前可以写入的位置
当快指针遇到不等于 val 的元素时,将其复制到慢指针位置,然后慢指针前进。
代码实现
class Solution {
public int removeElement(int[] nums, int val) {
int idx = 0; // 慢指针,指向下一个写入位置
for (int num : nums) { // 快指针遍历数组
if (num != val) {
nums[idx] = num; // 保留不等于 val 的元素
idx++;
}
}
return idx; // 返回新数组长度
}
}
简化写法
class Solution {
public int removeElement(int[] nums, int val) {
int idx = 0;
for (int x : nums) {
if (x != val) nums[idx++] = x; // 先赋值,后自增
}
return idx;
}
}
执行流程图解
以 nums = [0,1,2,2,3,0,4,2], val = 2 为例:
初始: [0,1,2,2,3,0,4,2], idx=0
i=0, num=0 ≠ 2: nums[0]=0, idx=1 → [0,1,2,2,3,0,4,2]
i=1, num=1 ≠ 2: nums[1]=1, idx=2 → [0,1,2,2,3,0,4,2]
i=2, num=2 = 2: 跳过
i=3, num=2 = 2: 跳过
i=4, num=3 ≠ 2: nums[2]=3, idx=3 → [0,1,3,2,3,0,4,2]
i=5, num=0 ≠ 2: nums[3]=0, idx=4 → [0,1,3,0,3,0,4,2]
i=6, num=4 ≠ 2: nums[4]=4, idx=5 → [0,1,3,0,4,0,4,2]
i=7, num=2 = 2: 跳过
结果: 返回 5, nums = [0,1,3,0,4,_,_,_]
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 单次遍历数组 |
| 空间复杂度 | O(1) | 原地修改,常数额外空间 |
与 26 题的对比
| 特性 | 26. 删除重复项 | 27. 移除元素 |
|---|---|---|
| 数组状态 | 有序 | 无序 |
| 保留条件 | 与前一个元素不同 | 不等于给定值 |
| 顺序要求 | 保持相对顺序 | 不强制要求顺序 |
| 指针移动 | 比较 nums[i] 与 nums[j] |
直接与 val 比较 |
28. 找出字符串中第一个匹配项的下标
题目描述
在字符串 haystack 中找出 needle 的第一个匹配下标。
解法:暴力匹配
class Solution {
public int strStr(String haystack, String needle) {
int hs = haystack.length();
int nd = needle.length();
char[] hsArray = haystack.toCharArray();
char[] ndArray = needle.toCharArray();
for (int i = 0; i <= hs - nd; i++) {
int p = i, k = 0;
while (k < nd && hsArray[p] == ndArray[k]) {
p++;
k++;
}
if (k == nd) return i;
}
return -1;
}
}
80. 删除有序数组中的重复项 II ⭐
题目描述
给你一个 有序数组 nums,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素 只出现两次,返回删除后数组的新长度。
核心变化: 从「保留 1 次」变为「保留最多 2 次」
示例
示例 1:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
解释:元素 1 出现 3 次,只保留前 2 个
示例 2:
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
解释:元素 1 出现 4 次,只保留前 2 个
解法一:双指针(直接法)
思路
与第 26 题类似,但需要保留最多 2 个重复元素:
-
慢指针
slow:从索引 2 开始,指向下一个写入位置 -
快指针
fast:从索引 2 开始,遍历数组 -
保留条件 :
nums[fast]与nums[slow-2]不同(确保当前元素出现次数不超过 2 次)
代码实现
class Solution {
public int removeDuplicates(int[] nums) {
int n = nums.length;
// 数组长度小于等于 2 时,直接返回长度
if (n <= 2) {
return n;
}
int slow = 2; // 慢指针从 2 开始(前两个元素直接保留)
int fast = 2; // 快指针从 2 开始遍历
while (fast < n) {
// 关键:与 slow-2 位置比较,确保最多保留 2 个相同元素
if (nums[slow - 2] != nums[fast]) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
}
执行流程图解
以 nums = [1,1,1,2,2,3] 为例:
初始: [1,1,1,2,2,3], slow=2, fast=2
fast=2, nums[0]=1 == nums[2]=1: 跳过(已有 2 个 1)
fast=3, nums[0]=1 != nums[3]=2: nums[2]=2, slow=3 → [1,1,2,2,2,3]
fast=4, nums[1]=1 != nums[4]=2: nums[3]=2, slow=4 → [1,1,2,2,2,3]
fast=5, nums[2]=2 != nums[5]=3: nums[4]=3, slow=5 → [1,1,2,2,3,3]
结果: 返回 5, nums = [1,1,2,2,3,_]
解法二:通用解法(推荐)
利用之前提到的通用模板,将 k 设为 2:
class Solution {
public int removeDuplicates(int[] nums) {
return process(nums, 2); // 最多保留 2 个
}
/**
* 通用解法:保留最多 k 个相同元素
*/
int process(int[] nums, int k) {
int idx = 0;
for (int x : nums) {
// 前 k 个直接保留,或者与前面第 k 个元素不同时保留
if (idx < k || nums[idx - k] != x) {
nums[idx++] = x;
}
}
return idx;
}
}
解法三:官方变形通用写法
public static int process(int[] nums, int exist) {
int n = nums.length;
if (n <= exist) return n; // 小于等于 exist 直接返回
int slow = exist; // 慢指针从 exist 开始
int fast = exist; // 快指针从 exist 开始
while (fast < n) {
// 与前面第 exist 个元素比较
if (nums[slow - exist] != nums[fast]) {
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
当 exist = 2 时,就是本题的解法。
三题对比总结
| 题目 | 保留次数 | 关键判断条件 | 慢指针起始位置 |
|---|---|---|---|
| 26. 删除重复项 | 1 次 | nums[i] != nums[j] |
0 |
| 27. 移除元素 | 保留非 val | num != val |
0 |
| 80. 删除重复项 II | 2 次 | nums[slow-2] != nums[fast] |
2 |
通用模板总结
/**
* 通用模板:在有序数组中保留最多 k 个相同元素
* 适用于:26题(k=1), 80题(k=2) 等
*/
int removeDuplicatesGeneral(int[] nums, int k) {
int idx = 0;
for (int x : nums) {
if (idx < k || nums[idx - k] != x) {
nums[idx++] = x;
}
}
return idx;
}
完整代码汇总
// 26. 删除有序数组中的重复项(保留1个)
class Solution26 {
public int removeDuplicates(int[] nums) {
int j = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != nums[j]) {
nums[++j] = nums[i];
}
}
return j + 1;
}
}
// 27. 移除元素
class Solution27 {
public int removeElement(int[] nums, int val) {
int idx = 0;
for (int x : nums) {
if (x != val) nums[idx++] = x;
}
return idx;
}
}
// 28. 找出字符串中第一个匹配项的下标
class Solution28 {
public int strStr(String haystack, String needle) {
int hs = haystack.length(), nd = needle.length();
char[] h = haystack.toCharArray(), n = needle.toCharArray();
for (int i = 0; i <= hs - nd; i++) {
int p = i, k = 0;
while (k < nd && h[p] == n[k]) {
p++; k++;
}
if (k == nd) return i;
}
return -1;
}
}
// 80. 删除有序数组中的重复项 II(保留2个)
class Solution80 {
public int removeDuplicates(int[] nums) {
int n = nums.length;
if (n <= 2) return n;
int slow = 2, fast = 2;
while (fast < n) {
if (nums[slow - 2] != nums[fast]) {
nums[slow++] = nums[fast];
}
fast++;
}
return slow;
}
}
学习要点
-
双指针技巧 是解决数组原地修改问题的核心
-
通用模板
process(nums, k)可以应对「保留 k 个重复元素」的所有情况 -
边界处理 是小数组(长度 ≤ k)时需要特殊处理
-
比较对象 决定了解法的差异:
-
与 前一个元素 比较 → 保留 1 个
-
与 前 k 个元素 比较 → 保留 k 个
-
与 给定值 比较 → 移除元素
-