双指针算法解析
双指针是一种思想,而不是说真的就是定义了两个指针,它和语言没有关系,比如C++,Java,Python等都可以使用双指针算法解题,而且是一种非常常见的算法
本篇博客适合所有语言学者阅读,因为算法是思想,每个题目除超详细的算法解析外后面还附赠了Java代码来供参考
常见的双指针有两种形式,一种是左右指针,一种是快慢指针
左右指针
- 一般用于顺序结构中,也称对撞指针
- 左右指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近
- 左右指针的终止条件一般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
- left == right (两个指针指向同一个位置)
- left > right (两个指针错开)
快慢指针
- 又称为龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动
- 这种方法对于处理环形链表或数组非常有用
- 其实不单单是环形链表或者数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想
- 快慢指针的实现方式有很多种,最常用的一种就是:
- 在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢
1、力扣283.移动零
题目链接:283.移动零
题目描述:
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
**输入:** nums = `[0,1,0,3,12]` **输出:** `[1,3,12,0,0]` 示例 2:
**输入:** nums = `[0]` **输出:** `[0]` 提示:
1 <= nums.length <= 104-231 <= nums[i] <= 231 - 1
算法思路:
快排的思想:数组划分区间 - 数组分两块
在本题中,我们可以用一个 cur 指针来扫描整个数组,另一个 dest 指针用来记录非零数序列的最后一个位置。
根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
在cur遍历期间,使 [0, dest] 的元素全部都是非零元素, [dest + 1, cur - 1] 的元素全是零, 这两部分属于处理过的区间,存放处理过的元素,单论处理过的区间的话,是符合题中结果的,而 [cur, n-1] 是待处理的区间,里面存放还未处理的元素

算法流程:

Java算法代码:
class Solution { public void moveZeroes(int[] nums) { for(int cur = 0, dest = -1; cur < nums.length; cur++) if(nums[cur] != 0) // 仅需处理⾮零元素 { dest++; // dest 先向后移动⼀位 // 交换 int tmp = nums[cur]; nums[cur] = nums[dest]; nums[dest] = tmp; } } } 2、力扣1089复写零
题目链接:力扣1089复写零
题目描述:
给你一个长度固定的整数数组 arr ,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。
注意:请不要在超过该数组长度的位置写入元素。请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。
示例 1:
**输入:**arr = \[1,0,2,3,0,4,5,0\] **输出:**\[1,0,0,2,3,0,0,4\] **解释:**调用函数后,输入的数组将被修改为:\[1,0,0,2,3,0,0,4\] 示例 2:
**输入:**arr = \[1,2,3\] **输出:**\[1,2,3\] **解释:**调用函数后,输入的数组将被修改为:\[1,2,3\] 提示:
1 <= arr.length <= 1040 <= arr[i] <= 9
算法思路:
原地复写 + 双指针
如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆 盖掉」。因此我们选择「从后往前」的复写策略。
但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两 步:
i. 先找到最后⼀个复写的数; ii. 然后从后向前进⾏复写操作 值得注意如何找最后一个复写的数,咱可以这样想,就先按另外开辟数组的方法,一个指针原数组用,另一个指针新开辟的数组用,把新开辟的数组完成复写后,原数组的指向不就是咱需要的最后一个复写的数么,当然,此题不能另外开辟空间,咱可以用这种思想,不复写只移动来找到那个复写的数
算法流程:


Java算法代码:
class Solution { public void duplicateZeros(int[] arr) { int cur = 0, dest = -1, n = arr.length; // 1. 先找到最后⼀个需要复写的数 while(cur < n) { if(arr[cur] == 0) dest += 2; else dest += 1; if(dest >= n - 1) break; cur++; } // 2. 处理⼀下边界情况 if(dest == n) { arr[n - 1] = 0; cur--; dest -= 2; } // 3. 从后向前完成复写操作 while(cur >= 0) { if(arr[cur] != 0) arr[dest--] = arr[cur--]; else { arr[dest--] = 0; arr[dest--] = 0; cur--; } } } } 3、力扣202快乐数
题目链接:力扣202快乐数
题目描述:
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
**输入:**n = 19 **输出:**true **解释:** 12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1 示例 2:
**输入:**n = 2 **输出:**false 提示:
1 <= n <= 231 - 1
题目分析:

简单证明:

算法思路:

补充知识:如何求一个数n每个位置上的数字的平方和

当然,用递归求也行
Java算法代码:
class Solution { public int bitSum(int n) // 返回 n 这个数每⼀位上的平⽅和 { int sum = 0; while(n != 0) { int t = n % 10; sum += t * t; n /= 10; } return sum; } public boolean isHappy(int n) { int slow = n, fast = bitSum(n); while(slow != fast) { slow = bitSum(slow); fast = bitSum(bitSum(fast)); } return slow == 1; } } 4、力扣11盛水最多的容器
题目链接:力扣11盛水最多的容器
题目描述:
定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
说明:你不能倾斜容器。
示例 1:

**输入:**\[1,8,6,2,5,4,8,3,7\] **输出:**49 **解释:**图中垂直线代表输入数组 \[1,8,6,2,5,4,8,3,7\]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 示例 2:
**输入:**height = \[1,1\] **输出:**1 提示:
n == height.length2 <= n <= 1050 <= height[i] <= 104
算法思路:

Java算法代码:
class Solution { public int maxArea(int[] height) { int left = 0, right = height.length - 1, ret = 0; while(left < right) { int v = Math.min(height[left], height[right]) * (right - left); ret = Math.max(ret, v); if(height[left] < height[right]) left++; else right--; } return ret; } } 5、力扣611有效三角形的个数
题目链接:力扣611有效三角形的个数
题目描述:
给定一个包含非负整数的数组 nums ,返回其中可以组成三角形三条边的三元组个数。
示例 1:
**输入:** nums = \[2,2,3,4\] **输出:** 3 **解释:**有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3 示例 2:
**输入:** nums = \[4,2,3,4\] **输出:** 4 提示:
1 <= nums.length <= 10000 <= nums[i] <= 1000
算法思路:
排序+双指针

Java算法代码:
class Solution { public int triangleNumber(int[] nums) { // 1. 优化:排序 Arrays.sort(nums); // 2. 利⽤双指针解决问题 int ret = 0, n = nums.length; for(int i = n - 1; i >= 2; i--) // 先固定最⼤的数 { // 利⽤双指针快速统计出符合要求的三元组的个数 int left = 0, right = i - 1; while(left < right) { if(nums[left] + nums[right] > nums[i]) { ret += right - left; right--; } else { left++; } } } return ret; } } 6、力扣179 查找总价为目标值的两个商品/ 原剑指Offer和为s的两个数字
题目链接:力扣179查找总价值为目标值的两个商品
题目描述:
购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况,返回任一结果即可。
示例 1:
**输入:**price = \[3, 9, 12, 15\], target = 18 **输出:**\[3,15\] 或者 \[15,3\] 示例 2:
**输入:**price = \[8, 21, 27, 34, 52, 66\], target = 61 **输出:**\[27,34\] 或者 \[34,27\] 提示:
1 <= price.length <= 10^51 <= price[i] <= 10^61 <= target <= 2*10^6
算法思路:
注意到本题是升序的数组,因此可以⽤「对撞指针」优化时间复杂度。
算法流程:

Java算法代码:
class Solution { public int[] twoSum(int[] nums, int target) { int left = 0, right = nums.length - 1; while(left < right) { int sum = nums[left] + nums[right]; if(sum > target) right--; else if(sum < target) left++; else return new int[] {nums[left], nums[right]}; } // 照顾编译器 return new int[]{0}; } } 7、力扣15三数之和
题目链接:力扣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 。 提示:
3 <= nums.length <= 3000-105 <= nums[i] <= 105
算法思路:
每次固定一个位置,其余位置用双指针求双数之和的方法求,注意边界情况复杂

Java算法代码:
class Solution { public List<List<Integer>> threeSum(int[] nums) { List<List<Integer>> ret = new ArrayList<>(); // 1. 排序 Arrays.sort(nums); // 2. 利⽤双指针解决问题 int n = nums.length; for(int i = 0; i < n; ) // 固定数 a { if(nums[i] > 0) break; // ⼩优化 int left = i + 1, right = n - 1, target = -nums[i]; while(left < right) { int sum = nums[left] + nums[right]; if(sum > target) right--; else if(sum < target) left++; else { // nums[i] nums[left] num[right] ret.add(new ArrayList<Integer>(Arrays.asList(nums[i], nums[left], nums[right]))); left++; right--; // 缩⼩区间继续寻找 // 去重:left right while(left < right && nums[left] == nums[left - 1]) left++; while(left < right && nums[right] == nums[right + 1]) right--; } } // 去重:i i++; while(i < n && nums[i] == nums[i - 1]) i++; } return ret; } } 8、力扣18四数之和
题目链接:力扣18四数之和
题目描述:
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < na、b、c和d互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
**输入:**nums = \[1,0,-1,0,-2,2\], target = 0 **输出:**\[\[-2,-1,1,2\],\[-2,0,0,2\],\[-1,0,0,1\]\] 示例 2:
**输入:**nums = \[2,2,2,2,2\], target = 8 **输出:**\[\[2,2,2,2\]\] 提示:
1 <= nums.length <= 200-109 <= nums[i] <= 109-109 <= target <= 109
算法思路:
排序 + 双指针

Java算法代码:
class Solution { public List<List<Integer>> fourSum(int[] nums, int target) { List<List<Integer>> ret = new ArrayList<>(); // 1. 排序 Arrays.sort(nums); // 2. 利⽤双指针解决问题 int n = nums.length; for(int i = 0; i < n; ) // 固定数 a { // 三数之和 for(int j = i + 1; j < n; ) // 固定数 b { // 双指针 int left = j + 1, right = n - 1; long aim = (long)target - nums[i] - nums[j]; while(left < right) { int sum = nums[left] + nums[right]; if(sum > aim) right--; else if(sum < aim) left++; else { ret.add(Arrays.asList(nums[i], nums[j], nums[left++], nums[right--])); // 去重⼀ while(left < right && nums[left] == nums[left - 1]) left++; while(left < right && nums[right] == nums[right + 1]) right--; } } // 去重⼆ j++; while(j < n && nums[j] == nums[j - 1]) j++; } // 去重三 i++; while(i < n && nums[i] == nums[i - 1]) i++; } return ret; } } 🧸前路漫漫,愿星光与您相伴!