在算法的世界里,有些问题需要我们向内探索数组的组合(如四数之和),而另一些则要求我们向前眺望序列的未来(如每日温度)。今天,我们就来深入剖析这两道经典题目,理解 双指针 和 单调栈 这两种强大工具如何解决看似不同但又都极具挑战性的问题。
第一部分:四数之和(LeetCode 18)—— 组合的艺术
题目大意
给定一个整数数组 nums 和一个目标值 target,找出所有不重复的四元组 [a, b, c, d],使得 a + b + c + d = target。
核心思想:排序 + 固定 + 双指针 + 去重
这道题是“N数之和”问题的典型代表。其核心在于将高维的暴力搜索问题,通过排序和双指针技巧降维处理。
解题步骤
- 排序: 对数组进行升序排序,这是使用双指针和去重的前提。
- 固定前两个数: 使用两层嵌套循环,固定第一个数
nums[i]和第二个数nums[j]。 - 双指针找后两个数: 在子数组
[j+1, n-1]中,用left和right指针寻找满足nums[left] + nums[right] == target - nums[i] - nums[j]的组合。 - 去重: 在每一层循环和找到答案后,都要跳过重复元素,确保结果唯一。
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) 特性非常适合处理“最近相关性”的问题。
- 我们希望栈中存储的索引对应的温度是 递减 的。这样,当遇到一个更高的温度时,它就是栈中所有比它低的温度的“下一个更高温度”。
解题步骤
-
初始化: 创建一个结果数组
result(全部初始化为 0)和一个空栈stack。栈中存储的是 天数的索引。 -
遍历每一天:
- 检查栈顶: 如果当前温度
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,弹出0,result[0] = 1-0 = 1。入栈[1]。i=2 (75):75 > 74,弹出1,result[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,弹出4,result[4]=5-4=1;72 > 71,弹出3,result[3]=5-3=2。入栈[2, 5]。- ... 以此类推。
总结与对比
| 特性 | 四数之和 (LeetCode 18) | 每日温度 (LeetCode 739) |
|---|---|---|
| 问题类型 | 数组组合、查找 | 序列、下一个更大元素 |
| 核心算法 | 双指针 | 单调栈 |
| 关键数据结构 | 数组 | 栈 (Stack) |
| 时间复杂度 | O(n³) | O(n) |
| 空间复杂度 | O(1) (不计输出) | O(n) |
| 思维方式 | 向内收缩 (固定两端,向中间找) | 向外扩展/回溯 (当前元素影响之前的元素) |
- 四数之和 教会我们如何利用 有序性 来高效地搜索组合。
- 每日温度 则展示了 单调栈 如何优雅地解决“等待”或“下一个”这类具有方向性的问题。
掌握这两种模式,就能应对 LeetCode 中大量相关的高频面试题。