数据结构算法学习:LeetCode热题100-双指针篇(移动零、盛水最多的容器、三数之和、接雨水)
文章目录
- 题一:283. 移动零
- 问题描述
- 解题方法
- 题二:11. 盛最多水的容器
- 问题描述
- 解题方法
- 题三:15. 三数之和
- 问题描述
- 解题方法
- 题四:42. 接雨水
- 问题描述
- 解题方法
简介:该类题型主要考察双指针的使用,去避免过多读取重复数据的操作
题一:283. 移动零
问题描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
标签提示: 数组、双指针
解题方法
1、暴力求解:遍历数组,每次遇到非零元素,就将其移动到数组前面,同时将后面的元素后移。但这样会导致时间复杂度为O(n^2)。
2、双指针求解:
核心思想:通过双指针实时交换非零元素与零元素
- i指针:指向下一个非零元素应放置的位置
- j指针:遍历数组寻找非零元素
- 交换条件:仅当i != j时交换(避免无意义交换)
双指针求解代码
class Solution {public void moveZeroes(int[] nums) {int i = 0, j = 0, tmp;while(j < nums.length){if (nums[j] != 0) {// 仅当指针位置不同时才交换if (i != j) {tmp = nums[i];nums[i] = nums[j];nums[j] = tmp;}i++; // 非零元素处理后i指针右移}j++; // j指针始终右移}System.out.println(Arrays.toString(nums));}
}
题二:11. 盛最多水的容器
问题描述
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器
示例
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
标签提示: 贪心、数组、双指针
解题方法
1、暴力解法
暴力解法的思路直观且简单:枚举所有可能的两条线组合,计算每对线之间的盛水量,并从中找出最大值。具体而言,对于数组中的每一对线 (i, j)(其中 i < j),盛水量由底边长度 (j - i) 和两条线中较短的高度 min(height[i], height[j]) 共同决定。通过两层循环遍历所有组合,即可得到最大盛水量。
缺点:时间复杂度为 O(n²),当数据规模较大时效率较低,无法通过大规模测试用例。
2、双指针求解
为了优化暴力解法,我们可以采用双指针法。该方法利用了贪心算法的思想,通过两个指针分别从数组的两端向中间移动,从而在 O(n) 的时间复杂度内解决问题。
算法思路:
盛水量由两个因素决定:底边长度(即两条线之间的距离)和两条线中较短的那条的高度。初始时,我们将左指针置于数组的起始位置(i=0),右指针置于数组的末尾(j=n-1)。此时,底边长度最大。然后,我们计算当前指针所指向的两条线之间的盛水量,并更新最大盛水量。
接下来,我们需要移动指针。关键在于:移动较短的那条线所对应的指针。因为盛水量受限于较短的那条线,如果移动较长的那条线,底边长度会减小,而高度可能不会增加(甚至可能减小),所以盛水量不会增加。相反,移动较短的那条线,我们有可能遇到更高的线,从而增加盛水量。
具体步骤如下:
- 初始化:左指针 i 指向数组开头,右指针 j 指向数组末尾,最大盛水量 ans 初始化为0。
- 循环条件:当左指针小于右指针时(即 i < j),执行以下操作:
a. 计算当前盛水量:m = (j - i) * min(height[i], height[j])。
b. 更新最大盛水量:如果 m 大于 ans,则将 ans 更新为 m。
c. 移动指针:比较 height[i] 和 height[j]:
-如果 height[i] 大于 height[j],则将右指针 j 向左移动一位(即 j–)。
-否则,将左指针 i 向右移动一位(即 i++)。 - 循环结束后,返回 ans。
双指正求解代码:
class Solution {public int maxArea(int[] height) {int ans = 0, n = height.length;int m, i = 0, j = n -1;while(i < j){m = (j - i) * Math.min(height[i], height[j]);if(ans < m){ans = m;}if(height[i] > height[j]){j --;}else{i ++;}}return ans;}
}
题三:15. 三数之和
问题描述
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
提示标签: 数组、双指针、排序
解题方法
1、暴力求解
暴力解法的思路是枚举所有可能的三元组组合,检查其和是否为0,并确保结果不重复。具体步骤如下:
- 使用三重循环遍历所有可能的三元组 (i, j, k),其中 i < j < k
- 对每个三元组计算和 sum = nums[i] + nums[j] + nums[k]
- 若 sum == 0,则将该三元组加入结果列表
- 通过排序或哈希表等方式对结果进行去重处理
缺点:
时间复杂度高达 O(n³)
去重逻辑复杂且容易出错
当数据规模较大时(如 n > 1000)会超时
2、双指针求解
核心思想
利用排序 + 双指针优化时间复杂度。首先对数组排序,然后固定一个数,使用双指针在剩余部分寻找另外两个数,使得三数之和为0。关键点:
- 排序:使数组有序,便于双指针移动和去重
- 固定一个数:遍历数组,每次固定一个数 nums[i]
- 双指针查找:在 i+1 到 n-1 范围内使用双指针 l 和 r 寻找两数,使其和等于 -nums[i]
- 去重处理:利用排序特性跳过重复元素
双指针求解代码:
class Solution {public List<List<Integer>> threeSum(int[] nums) {List<List<Integer>> arraylist = new ArrayList<>();int sum, n = nums.length;Arrays.sort(nums); // 步骤1:排序数组for(int i = 0; i < n - 2; i ++){ // 步骤2:固定第一个数// 步骤3:去重处理 - 跳过重复的nums[i]if(i > 0 && nums[i] == nums[i - 1]){continue;}// 步骤4:提前终止 - 若nums[i]>0则后面不可能有解if(nums[i] > 0){break;}// 步骤5:初始化双指针int l = i + 1, r = n - 1;while(l < r){ // 步骤6:双指针查找sum = nums[i] + nums[l] + nums[r];if(sum == 0){ // 找到解arraylist.add(Arrays.asList(nums[i], nums[l], nums[r]));// 步骤7:跳过重复元素while(l < r && nums[l] == nums[l + 1]) l ++;while(l < r && nums[r] == nums[r - 1]) r --;// 步骤8:移动指针继续查找l ++;r --;}else if(sum > 0){ // 和过大,右指针左移r --;}else{ // 和过小,左指针右移l ++;}}}return arraylist;}
}
复杂度分析
时间复杂度:O(n²)
排序:O(n log n)
主循环:O(n) × 双指针遍历:O(n) = O(n²)
空间复杂度:O(1) 或 O(n)
取决于排序算法实现(通常为 O(log n) 到 O(n))
题四:42. 接雨水
问题描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
标签提示: 栈、数组、双指针
解题方法
开始错误的思路:开始拿到这题的时候,我先想到上面的盛水量那个题,然后根据示例图片,将每个区域为整体(也就是如图三个雨水区域),这样需要找到左右柱以及区域内更矮的柱占的面积(需要去除),这样就导致我需要考虑的因素太多了(左右柱是哪个、中间更矮的柱有那些)
双指针求解
核心思想
接雨水问题的关键在于计算每个位置能接的雨水量(也就是以横坐标为准,去考虑当前坐标也就是柱上面能够有接的雨水量,只要左右两次都有比它高的柱就能接雨水),而每个位置的雨水量取决于其左右两侧的最大高度中的较小值。双指针方法通过维护左右两个指针以及两侧的最大高度,在一次遍历中高效计算雨水量。
核心思路:
-
初始化左右指针l和r,分别指向数组首尾
-
维护lmax和rmax,分别表示从左到当前指针位置的最大高度和从右到当前指针位置的最大高度
-
比较lmax和rmax:
-
当lmax <= rmax时,左侧最大高度是当前限制雨水量的关键因素:
若当前高度height[l] > lmax,更新lmax
否则,当前位置可接雨水量为lmax - height[l]
左指针l右移 -
当lmax > rmax时,右侧最大高度是当前限制雨水量的关键因素:
若当前高度height[r] > rmax,更新rmax
否则,当前位置可接雨水量为rmax - height[r]
右指针r左移
-
-
当左右指针相遇时,遍历结束,返回累计雨水量
双指针求解代码
class Solution {public int trap(int[] height) {int ans = 0, n = height.length;int lmax = 0, rmax = 0, l = 0, r = n - 1;while(l <= r){if(lmax <= rmax){if(height[l] > lmax){lmax = height[l];}else{ans += lmax - height[l];}l ++;}else{if(rmax < height[r]){rmax = height[r];}else{ans += rmax - height[r];}r --;}}return ans;}
}
个人总结
通过系统学习双指针类题目,我深刻体会到问题转化能力在算法解题中的核心地位。双指针技术不仅是一种高效的编程技巧,更是一种重要的思维范式——它通过巧妙设置指针移动规则,从根本上减少了对冗余数据的重复扫描,从而显著优化算法性能。
双指针优化的深层含义:
-
本质是"排除无效解"
双指针通过建立明确的移动条件(如比较左右最大值、根据和与目标值的关系),在每一步迭代中主动排除不可能构成最优解的元素,避免暴力解法中的无效计算。例如:
在"盛最多水的容器"中,移动较矮边指针是因为保留较高边才可能获得更大容量
在"三数之和"中,跳过重复元素和根据和的大小移动指针,直接跳过大量不可能的组合 -
问题转化的核心价值
双指针常将复杂问题转化为更简单的子问题:
三数之和 → 固定一数后转化为两数之和
接雨水 → 将全局问题分解为左右两侧的局部最大值问题
这种转化降低了问题维度,使O(n²)的复杂度降为O(n)或O(n log n) -
有序性的高效利用
双指针与排序结合时(如三数之和),能充分利用有序数组的特性:
通过指针移动方向性(左增右减)快速逼近目标值
自然实现去重(跳过连续相同元素)
将无序问题的组合爆炸转化为有序问题的线性扫描 -
空间-时间的权衡艺术
双指针通常以O(1)的空间复杂度(除排序外)实现时间复杂度的阶跃式优化:
盛水容器:O(n²) → O(n)
三数之和:O(n³) → O(n²)
接雨水:O(n)空间(动态规划)→ O(1)空间
未来学习方向:
在掌握基础双指针模式后,我将深入研究其高级变体(如滑动窗口、快慢指针)和与其他算法的结合(如二分查找、贪心),并探索其在更复杂场景(如二维矩阵、链表环检测)中的应用。同时,我会系统分析各题解法的时空权衡,培养更精准的算法选择直觉。