双指针算法题的技巧总结

1 阅读4分钟

一、解题思维总结

1. 何时使用双指针?

场景解法
数组元素移动/重排快慢指针(移动零)
字符串回文判断左右指针向中间逼近
有序数组两数之和左右指针向中间逼近
容器最大面积/接雨水左右指针向中间逼近
多数之和(三数、四数)外层循环 + 内层双指针
字符串压缩读写指针

2. 复杂度分析

  • 快慢指针:O(n) 时间复杂度,O(1) 空间复杂度
  • 左右指针:O(n) 时间复杂度,O(1) 空间复杂度
  • 多指针组合:O(n²) 时间复杂度,O(1) 空间复杂度

3. 常用技巧

  1. 快慢指针交换[nums[fast], nums[slow]] = [nums[slow], nums[fast]]
  2. 左右指针逼近while (left < right) 循环条件
  3. 去重逻辑while (left < right && nums[left] === nums[left-1]) left++
  4. 面积计算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(' ');
};

三、易错点提醒

  1. 下标处理:注意题目要求下标从0开始还是从1开始
  2. 去重时机:找到解后立即去重,避免重复解
  3. 边界条件:空数组、单元素数组、全相同元素数组
  4. 指针移动:移动较低指针(面积问题)或根据和的大小移动
  5. 原地修改:注意是否需要返回新长度还是修改原数组
  6. 多位数处理:字符串压缩时数字可能有多位,需要逐个字符写入

四、学习心得

双指针的优势

  1. 减少冗余操作:相比暴力枚举,跳过不必要的比较
  2. 确保覆盖所有可能:从两端向中间逼近,检查所有组合
  3. 易于去重:天然适合处理重复元素
  4. 灵活性:可应用于多种变体问题

解题思维模式

  1. 识别模式:有序数组、对称性、最优解问题
  2. 选择指针类型:快慢指针、左右指针、读写指针
  3. 确定移动策略:根据问题特性决定指针移动规则
  4. 处理边界和去重:完善细节,确保正确性