LeetCode100天Day4-盛最多水的容器与两数之和II:双指针算法的极致应用
摘要:本文详细解析了LeetCode中两道经典双指针算法题目------"盛最多水的容器"和"两数之和 II:输入有序数组"。通过对比暴力解法和双指针解法,深入分析算法的时间复杂度和空间复杂度,帮助读者掌握双指针算法的精髓。
目录
文章目录
- LeetCode100天Day4-盛最多水的容器与两数之和II:双指针算法的极致应用
-
- 目录
- [1. 盛最多水的容器(Container With Most Water)](#1. 盛最多水的容器(Container With Most Water))
- [2. 两数之和 II - 输入有序数组(Two Sum II - Input array is sorted)](#2. 两数之和 II - 输入有序数组(Two Sum II - Input array is sorted))
-
- [2.1 题目描述](#2.1 题目描述)
- [2.2 解题思路对比](#2.2 解题思路对比)
- [2.3 双指针解法数学证明](#2.3 双指针解法数学证明)
- [2.4 复杂度对比分析](#2.4 复杂度对比分析)
- [3. 双指针算法思想总结](#3. 双指针算法思想总结)
-
- [3.1 适用场景](#3.1 适用场景)
- [3.2 常见双指针模式](#3.2 常见双指针模式)
- [3.3 代码模板](#3.3 代码模板)
- [4. 实战技巧与注意事项](#4. 实战技巧与注意事项)
-
- [4.1 代码优化技巧](#4.1 代码优化技巧)
- [4.2 常见陷阱](#4.2 常见陷阱)
- [4.3 调试技巧](#4.3 调试技巧)
- [5. 扩展思考](#5. 扩展思考)
-
- [5.1 相关题目推荐](#5.1 相关题目推荐)
- [5.2 进阶优化](#5.2 进阶优化)
- [6. 总结与展望](#6. 总结与展望)
- 参考资源
- 文章标签
1. 盛最多水的容器(Container With Most Water)
1.1 题目描述
给你一个长度为 n 的整数数组 height。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
1.2 解题思路分析
暴力解法思路
首先想到的暴力解法是:对于每一对可能的线段组合,计算它们能容纳的水量,然后找出最大值。
暴力解法的时间复杂度为 O(n²),对于大型数据集会导致超时。
双指针解法核心思想
双指针解法的核心在于贪心策略:
- 指针初始化 :一个指针在数组开头(
left = 0),一个在数组末尾(right = len-1) - 移动策略 :每次移动较短的那根线段对应的指针
- 理论基础:移动较长的线段只会减少容器的宽度,而高度不会增加(因为较短的线段限制了高度)
关键洞察 :容器的容积由
min(height[left], height[right]) * (right - left)决定。为了获得更大的容积,我们需要在保持可能的高度的同时,增加宽度或者找到更高的线段。
1.3 代码实现与详细解析
java
class Solution {
public int maxArea(int[] height) {
int len = height.length;
int tail_index = len - 1; // 右指针
int head = height[0]; // 左指针指向的线段高度
int head_index = 0; // 左指针
int tail = height[tail_index]; // 右指针指向的线段高度
// 计算初始容积
int lower = head < tail ? head : tail;
int curtemp = (tail_index - head_index) * lower;
int max = curtemp; // 最大容积
// 双指针移动逻辑
while(head_index < tail_index){
if (head < tail) {
// 左边线段更短,移动左指针
head_index++;
head = height[head_index];
} else {
// 右边线段更短或相等,移动右指针
tail_index--;
tail = height[tail_index];
}
// 计算当前容积
lower = head < tail ? head : tail;
curtemp = (tail_index - head_index) * lower;
// 更新最大容积
if (curtemp > max) {
max = curtemp;
}
}
return max;
}
}
1.4 复杂度分析
| 指标 | 暴力解法 | 双指针解法 |
|---|---|---|
| 时间复杂度 | O(n²) | O(n) |
| 空间复杂度 | O(1) | O(1) |
| 实际执行时间 | 超时 | 2ms |
2. 两数之和 II - 输入有序数组(Two Sum II - Input array is sorted)
2.1 题目描述
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。
如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
2.2 解题思路对比
解法一:暴力搜索(利用有序性优化)
这是最直观的解法,利用了数组有序的特性:
java
class Solution {
public int[] twoSum(int[] numbers, int target) {
int[] ans = new int[2];
for(int i = 0; i < numbers.length-1; i++){
int complement = target - numbers[i];
// 内层循环从 i+1 开始
for(int j = i+1; j < numbers.length; j++){
// 利用有序性提前终止
if(numbers[j] > complement) {
break;
}
// 找到答案
if(numbers[i] + numbers[j] == target){
ans[0] = i+1; // 下标从1开始
ans[1] = j+1;
return ans;
}
}
}
return ans;
}
}
优化点分析:
- 利用数组有序的特性,当
numbers[j] > complement时可以提前终止内层循环 - 时间复杂度:最坏情况下仍为 O(n²),但平均情况有所改善
解法二:双指针(最优解)
双指针解法充分利用了数组有序的特性:
java
class Solution {
public int[] twoSum(int[] numbers, int target) {
// 边界条件检查
if(numbers == null || numbers.length < 2) return null;
int head = 0, tail = numbers.length - 1;
while(head < tail){
int sum = numbers[head] + numbers[tail];
if(sum > target) {
// 和大于目标值,需要减小
tail--;
}
else if(sum < target) {
// 和小于目标值,需要增大
head++;
}
else {
// 找到答案
return new int[]{head+1, tail+1};
}
}
return null;
}
}
2.3 双指针解法数学证明
设数组为 numbers,目标为 target,当前指针位置为 left 和 right:
情况1 :numbers[left] + numbers[right] > target
- 由于数组有序,对于任意
k > left,都有numbers[k] ≥ numbers[left] - 因此
numbers[k] + numbers[right] ≥ numbers[left] + numbers[right] > target - 结论:必须减小右指针才能找到解
情况2 :numbers[left] + numbers[right] < target
- 由于数组有序,对于任意
k < right,都有numbers[k] ≤ numbers[right] - 因此
numbers[left] + numbers[k] ≤ numbers[left] + numbers[right] < target - 结论:必须增大左指针才能找到解
2.4 复杂度对比分析
| 对比项 | 暴力搜索优化版 | 双指针解法 |
|---|---|---|
| 时间复杂度 | O(n²) | O(n) |
| 空间复杂度 | O(1) | O(1) |
| 代码简洁度 | 中等 | 优秀 |
| 执行效率 | 较慢 | 极快 |
| 内存使用 | 常量级 | 常量级 |
3. 双指针算法思想总结
3.1 适用场景
双指针算法适用于以下场景:
- 有序数组:数组已经排序(升序或降序)
- 查找特定条件:需要找到满足特定条件的元素对或子数组
- 需要优化时间复杂度:从 O(n²) 优化到 O(n)
3.2 常见双指针模式
| 模式类型 | 特点 | 典型题目 |
|---|---|---|
| 对撞指针 | 左右指针向中间移动 | 两数之和、盛水容器 |
| 快慢指针 | 不同速度的指针 | 环形链表检测 |
| 滑动窗口 | 窗口大小可变 | 最小覆盖子串 |
| 分离指针 | 两个独立目标 | 三数之和 |
3.3 代码模板
java
// 对撞指针模板
int left = 0, right = array.length - 1;
while (left < right) {
int sum = array[left] + array[right];
if (sum == target) {
// 找到解
return new int[]{left, right};
} else if (sum < target) {
left++; // 需要更大的和
} else {
right--; // 需要更小的和
}
}
4. 实战技巧与注意事项
4.1 代码优化技巧
- 边界检查:始终检查输入数组的边界条件
- 提前终止:利用有序性提前终止无效搜索
- 避免重复计算:缓存中间结果减少重复计算
4.2 常见陷阱
注意事项:
- 下标从0还是从1开始:注意题目要求,本题要求从1开始
- 整数溢出:虽然本题数据范围较小,但实际应用中要考虑
- 重复使用元素:题目明确要求不能重复使用同一元素
4.3 调试技巧
java
// 添加调试信息便于理解
System.out.println("left: " + left + ", right: " + right);
System.out.println("numbers[left]: " + numbers[left] + ", numbers[right]: " + numbers[right]);
System.out.println("current sum: " + (numbers[left] + numbers[right]));
5. 扩展思考
5.1 相关题目推荐
- 三数之和:LeetCode 第15题
- 四数之和:LeetCode 第18题
- 最接近的三数之和:LeetCode 第16题
- 三角形最小路径和:双指针思想应用
5.2 进阶优化
对于超大数据集,可以考虑:
- 并行化处理:将数组分割后并行处理
- 位运算优化:特定情况下的位运算技巧
- 缓存优化:利用CPU缓存提高访问速度
6. 总结与展望
今天我们深入学习了双指针算法在两个经典题目中的应用:
- 盛最多水的容器:通过贪心策略和双指针,将时间复杂度从 O(n²) 优化到 O(n)
- 两数之和 II:充分利用数组有序特性,实现线性时间复杂度的优雅解法
核心收获:
- 双指针算法的本质是用空间换时间,但实际上并不需要额外空间
- 有序性是双指针算法的重要前提
- 贪心策略在双指针算法中扮演关键角色
下一步学习计划:
- 练习更多双指针相关题目
- 学习滑动窗口等高级双指针技巧
- 探索双指针在其他数据结构中的应用
互动时间:你还在哪些题目中见过双指针算法的应用?欢迎在评论区分享你的解题经验!
参考资源
文章标签
#LeetCode #算法 #双指针 #Java #数据结构
喜欢这篇文章吗?别忘了点赞、收藏和分享!你的支持是我创作的最大动力!