LeetCode 双雄:从「四数之和」到「每日温度」——掌握双指针与单调栈的精髓

0 阅读4分钟

在算法的世界里,有些问题需要我们向内探索数组的组合(如四数之和),而另一些则要求我们向前眺望序列的未来(如每日温度)。今天,我们就来深入剖析这两道经典题目,理解 双指针单调栈 这两种强大工具如何解决看似不同但又都极具挑战性的问题。


第一部分:四数之和(LeetCode 18)—— 组合的艺术

题目大意
给定一个整数数组 nums 和一个目标值 target,找出所有不重复的四元组 [a, b, c, d],使得 a + b + c + d = target

核心思想:排序 + 固定 + 双指针 + 去重

这道题是“N数之和”问题的典型代表。其核心在于将高维的暴力搜索问题,通过排序和双指针技巧降维处理。

解题步骤
  1. 排序: 对数组进行升序排序,这是使用双指针和去重的前提。
  2. 固定前两个数: 使用两层嵌套循环,固定第一个数 nums[i] 和第二个数 nums[j]
  3. 双指针找后两个数: 在子数组 [j+1, n-1] 中,用 leftright 指针寻找满足 nums[left] + nums[right] == target - nums[i] - nums[j] 的组合。
  4. 去重: 在每一层循环和找到答案后,都要跳过重复元素,确保结果唯一。
JavaScript 代码实现
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function(nums, target) {
    const result = [];
    const n = nums.length;
    if (n < 4) return result;

    // 1. 排序
    nums.sort((a, b) => a - b);

    for (let i = 0; i < n - 3; i++) {
        // 跳过重复的第一个数
        if (i > 0 && nums[i] === nums[i - 1]) continue;

        for (let j = i + 1; j < n - 2; j++) {
            // 跳过重复的第二个数
            if (j > i + 1 && nums[j] === nums[j - 1]) continue;

            let left = j + 1, right = n - 1;
            while (left < right) {
                const sum = nums[i] + nums[j] + nums[left] + nums[right];
                if (sum > target) {
                    right--; // 和太大,右指针左移
                } else if (sum < target) {
                    left++; // 和太小,左指针右移
                } else {
                    // 找到一个有效解
                    result.push([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--;
                    left++;
                    right--;
                }
            }
        }
    }
    return result;
};

第二部分:每日温度(LeetCode 739)—— 向未来看一眼

题目大意
给定一个整数数组 temperatures,表示每天的气温。请返回一个数组 answer,其中 answer[i] 是指对于第 i 天,你需要等待多少天才能遇到一个更暖和的气温。如果之后都不会有更暖和的天气,请在该位置用 0 代替。

核心思想:单调栈(Monotonic Stack)

这是一个典型的“下一个更大元素”问题。暴力解法需要对每个元素向后遍历,时间复杂度为 O(n²)。而 单调栈 可以将其优化到 O(n)。

为什么用栈?
  • 栈的 后进先出(LIFO) 特性非常适合处理“最近相关性”的问题。
  • 我们希望栈中存储的索引对应的温度是 递减 的。这样,当遇到一个更高的温度时,它就是栈中所有比它低的温度的“下一个更高温度”。
解题步骤
  1. 初始化: 创建一个结果数组 result(全部初始化为 0)和一个空栈 stack。栈中存储的是 天数的索引

  2. 遍历每一天:

    • 检查栈顶: 如果当前温度 T[i] 大于栈顶索引对应的温度 T[stack[stack.length-1]],说明找到了栈顶那天的“答案”。
    • 计算天数差: i - stack.pop() 就是需要等待的天数,存入 result
    • 重复检查: 继续检查新的栈顶,直到栈为空或当前温度不再大于栈顶温度。
    • 入栈: 将当前天的索引 i 压入栈中。
JavaScript 代码实现
/**
 * @param {number[]} temperatures
 * @return {number[]}
 */
var dailyTemperatures = function(temperatures) {
    const n = temperatures.length;
    const result = new Array(n).fill(0); // 初始化结果数组
    const stack = []; // 单调栈,存储索引

    for (let i = 0; i < n; i++) {
        // 当栈不为空,且当前温度高于栈顶索引对应的温度时
        while (stack.length > 0 && temperatures[i] > temperatures[stack[stack.length - 1]]) {
            const prevIndex = stack.pop(); // 弹出栈顶
            result[prevIndex] = i - prevIndex; // 计算等待天数
        }
        stack.push(i); // 将当前索引入栈
    }

    return result;
};

示例演示 (temperatures = [73, 74, 75, 71, 69, 72, 76, 73])

  • i=0 (73): 栈空,入栈 [0]
  • i=1 (74): 74 > 73,弹出 0result[0] = 1-0 = 1。入栈 [1]
  • i=2 (75): 75 > 74,弹出 1result[1] = 2-1 = 1。入栈 [2]
  • i=3 (71): 71 < 75,入栈 [2, 3]
  • i=4 (69): 69 < 71,入栈 [2, 3, 4]
  • i=5 (72): 72 > 69,弹出 4result[4]=5-4=172 > 71,弹出 3result[3]=5-3=2。入栈 [2, 5]
  • ... 以此类推。

总结与对比

特性四数之和 (LeetCode 18)每日温度 (LeetCode 739)
问题类型数组组合、查找序列、下一个更大元素
核心算法双指针单调栈
关键数据结构数组栈 (Stack)
时间复杂度O(n³)O(n)
空间复杂度O(1) (不计输出)O(n)
思维方式向内收缩 (固定两端,向中间找)向外扩展/回溯 (当前元素影响之前的元素)
  • 四数之和 教会我们如何利用 有序性 来高效地搜索组合。
  • 每日温度 则展示了 单调栈 如何优雅地解决“等待”或“下一个”这类具有方向性的问题。

掌握这两种模式,就能应对 LeetCode 中大量相关的高频面试题。