一、解题思维总结
1. 何时使用双指针?
| 场景 | 解法 |
|---|---|
| 数组元素移动/重排 | 快慢指针(移动零) |
| 字符串回文判断 | 左右指针向中间逼近 |
| 有序数组两数之和 | 左右指针向中间逼近 |
| 容器最大面积/接雨水 | 左右指针向中间逼近 |
| 多数之和(三数、四数) | 外层循环 + 内层双指针 |
| 字符串压缩 | 读写指针 |
2. 复杂度分析
- 快慢指针:O(n) 时间复杂度,O(1) 空间复杂度
- 左右指针:O(n) 时间复杂度,O(1) 空间复杂度
- 多指针组合:O(n²) 时间复杂度,O(1) 空间复杂度
3. 常用技巧
- 快慢指针交换:
[nums[fast], nums[slow]] = [nums[slow], nums[fast]] - 左右指针逼近:
while (left < right)循环条件 - 去重逻辑:
while (left < right && nums[left] === nums[left-1]) left++ - 面积计算:
Math.min(height[left], height[right]) * (right - left)
二、核心技术
技巧一:快慢指针
适用场景:数组元素重排、原地修改
核心要点:
- 快指针遍历所有元素,慢指针指向下一个非零元素位置
- 如果数组里无非0值,则low和fast一直相等
- 原地交换保持相对顺序
典型例题:移动零
var moveZeroes = function(nums) {
let low = 0;
for (let fast = 0; fast < nums.length; ++fast) {
if (nums[fast] !== 0) {
[nums[fast], nums[low]] = [nums[low], nums[fast]];
++low;
}
}
return nums;
};
技巧二:左右指针逼近
适用场景:有序数组、对称性问题
核心要点:
- 利用数组有序性,和小于目标时左指针右移,大于目标时右指针左移
- 时间复杂度从O(n²)优化到O(n)
- 注意题目要求下标从1开始
典型例题:两数之和 II - 输入有序数组
var twoSum = function(numbers, target) {
let left = 0, right = numbers.length - 1;
while (left < right) {
const sum = numbers[left] + numbers[right];
if (sum === target) {
return [left + 1, right + 1];
} else if (sum < target) {
left++;
} else {
right--;
}
}
};
技巧三:面积最大化
适用场景:容器盛水、最大面积问题
核心要点:
- 移动较低高度的指针,因为移动较高指针不会增加面积
- 使用Math.min计算多个数之间的最小值
- 每次移动都排除不可能成为最优解的组合
典型例题:盛最多水的容器
var maxArea = function(height) {
let max = 0;
let left = 0;
let right = height.length - 1;
while (left < right) {
const current = Math.min(height[left], height[right]) * (right - left);
max = Math.max(max, current);
if (height[left] < height[right]) {
++left;
} else {
--right;
}
}
return max;
};
技巧四:多指针组合
适用场景:三数之和、四数之和等
核心要点:
- 先排序,外层循环固定一个数,内层使用双指针
- 去重逻辑:跳过重复元素避免重复解
- 时间复杂度从O(n³)优化到O(n²)
典型例题:三数之和
var threeSum = function(nums) {
const sortedNums = [...nums].sort((a, b) => a - b);
const result = [];
for (let i = 0; i < sortedNums.length - 2; ++i) {
if (i > 0 && sortedNums[i] === sortedNums[i - 1]) continue;
let left = i + 1, right = sortedNums.length - 1;
while (left < right) {
const sum = sortedNums[i] + sortedNums[left] + sortedNums[right];
if (sum < 0) {
++left;
continue;
}
if (sum > 0) {
--right;
continue;
}
result.push([sortedNums[i], sortedNums[left], sortedNums[right]]);
++left;
--right;
while (left < right && sortedNums[left] === sortedNums[left - 1]) ++left;
while (left < right && sortedNums[right] === sortedNums[right + 1]) --right;
}
}
return result;
};
技巧五:字符串处理
适用场景:字符串压缩、反转单词
核心要点:
- 读写指针分离:read指针读取,write指针写入
- 处理多位数计数:逐个字符写入
- 原地修改数组,返回新长度
典型例题:压缩字符串
var compress = function(chars) {
let curCount = 1;
let left = 0;
let right = 0;
while (right < chars.length) {
if (right + 1 < chars.length && chars[right] === chars[right + 1]) {
++right;
++curCount;
continue;
}
chars[left] = chars[right];
++left;
if (curCount > 1) {
const str = curCount.toString();
for (let i = 0; i < str.length; ++i) {
chars[left] = str[i];
++left;
}
}
curCount = 1;
++right;
}
return left;
};
技巧六:字符串反转
适用场景:反转字符串中的单词
核心要点:
- 正则表达式处理多余空格:
/\s+/ - 从右向左遍历,遇到空格开始新单词
- 双指针法可以避免使用内置方法
典型例题:反转字符串中的单词
// 简洁解法
var reverseWords = function(s) {
return s.trim().split(/\s+/).reverse().join(' ');
};
// 双指针解法
var reverseWords = function(s) {
const result = [];
let isWord = true;
let right = s.length - 1;
while (right >= 0) {
if (s[right] === ' ') {
isWord = true;
} else {
if (isWord) {
result.push('');
}
result[result.length - 1] = s[right] + result[result.length - 1];
isWord = false;
}
--right;
}
return result.join(' ');
};
三、易错点提醒
- 下标处理:注意题目要求下标从0开始还是从1开始
- 去重时机:找到解后立即去重,避免重复解
- 边界条件:空数组、单元素数组、全相同元素数组
- 指针移动:移动较低指针(面积问题)或根据和的大小移动
- 原地修改:注意是否需要返回新长度还是修改原数组
- 多位数处理:字符串压缩时数字可能有多位,需要逐个字符写入
四、学习心得
双指针的优势
- 减少冗余操作:相比暴力枚举,跳过不必要的比较
- 确保覆盖所有可能:从两端向中间逼近,检查所有组合
- 易于去重:天然适合处理重复元素
- 灵活性:可应用于多种变体问题
解题思维模式
- 识别模式:有序数组、对称性、最优解问题
- 选择指针类型:快慢指针、左右指针、读写指针
- 确定移动策略:根据问题特性决定指针移动规则
- 处理边界和去重:完善细节,确保正确性