双指针算法(Two Pointer Algorithm)是一种常用于数组(或链表)操作的算法技巧。它的核心思想是通过维护两个指针,在数组中高效地解决一些问题,这里的指针不一定是真实的指针,是一种抽象的概念,比如数组的下标,C++的迭代器等等。这两个指针可以分别指向数组的不同位置,也可以分别指向数组的开始和结束。
常见的双指针算法有两种类型:快慢指针和左右指针。
下面我们通过实际算法题来进行讲解
题目链接:https://leetcode.cn/problems/move-zeroes/description/
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
示例 2:
输入: nums = [0] 输出: [0]
思路
核心思路是使用两个指针,一个指针(cur)用于遍历数组,另一个指针(dest)用于指示当前非零元素的插入位置。通过遍历数组,当发现非零元素时,将其与dest位置的元素进行交换,同时递增dest指针,确保非零元素的相对顺序保持不变。
下面是算法的详细流程:
- 初始化两个指针:cur和dest,初始值为0和-1,因为cur指针先走。
- 从左到右遍历数组,用cur指针指向当前元素。
- 如果当前元素不为零,将其与dest位置的元素进行交换,并递增dest指针。
- 继续遍历,重复步骤3,直到数组遍历完毕。
- 此时,所有非零元素已经移动到数组的前面,而dest指针的位置之后都是零。因为dest初始值为-1,所以dest + 1即为第一个零的位置。
- 遍历完成后,数组中所有零已经被移动到数组的末尾。
这个算法的优点在于它只需要遍历一次数组,并且在原地进行操作,不需要额外的空间。这使得它在处理大规模数组时非常高效,时间复杂度只有O(n)。
以下是对应的C++代码:
class Solution { public: void moveZeroes(vector& nums) { for(int cur=0,dest=-1;cur if(nums[cur]) swap(nums[++dest],nums[cur]); } } };
题目链接:https://leetcode.cn/problems/duplicate-zeros/
给你一个长度固定的整数数组 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]
思路
这里我们能够想到最简单的一种算法就是,我们再创建一个容器数组逐个遍历,进行零的重写,最后直接覆盖或交换原数组,但是这种写法花费的时间和特别是空间代价太大了,所以我们这种时候利用双指针的算法可以很好的解决这个问题。
算法的核心思想是通过两个步骤实现对数组中的零的复制:
下面是算法的详细步骤:
以下是对应的C++代码:
class Solution { public: void duplicateZeros(vector& arr) { int n=arr.size(),count=0,i=0; for(i=0;i if(arr[i]==0) count++; if(i+count+1>=n) break;//题目规定不能超过数组固定长度 } int r=n-1; if(i+count+1>n){ //处理边界情况,算出复写后位置大于n就说明最后那个位置一定是0 arr[r--]=0; i--; } //从后向前通过i和r的下标相对位置完成复写0 while(i>=0){ if(arr[i]) arr[r--]=arr[i--]; else{ arr[r--]=0; arr[r--]=0; i--; } } } };
题目链接:https://leetcode.cn/problems/happy-number/description/
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19 输出:true 解释: 12 + 92 = 82 82 + 22 = 68 62 + 82 = 100 12 + 02 + 02 = 1
示例 2:
输入:n = 2 输出:false
思路
这道题其实我们可以根据一个数学知识知道如果经过不断计算无线循环的话,其中是一定会有重复的数字的,其实这里最好的解法应该是使用哈希思想,但是在这里我们使用双指针也可以有很简单的写法,我们先来看看双指针的写法
解法一:快慢指针
以下是对应的C++代码:
class Solution { public: int nextSum(int n){ int sum=0; while(n){ int tmp=n%10; sum+=tmp*tmp; n/=10; } return sum; } bool isHappy(int n) { int slow=n,fast=nextSum(n); while(slow!=fast){ slow=nextSum(slow); fast=nextSum(nextSum(fast)); } return slow==1; } };
解法二:哈希
这个算法也是通过计算每位数字的平方和,然后不断迭代,检测是否能够最终得到1。不同之处在于,这个算法使用了一个哈希集合 unordered_set 来记录已经出现过的数字,以便检测循环。
以下是对这个算法的核心思想和流程的详细解释:
这个算法使用了哈希集合来检测循环,避免了在链表中进行双指针遍历的方式。这样的实现在空间上相对较高效,因为哈希集合的查找和插入操作都是平均 O(1) 的时间复杂度。算法的时间复杂度取决于是否存在循环,最坏情况下是 O(logN)。
以下是对应的C++代码:
class Solution { public: bool isHappy(int n) { // 使用一个集合来存储已经出现过的数字,以检测循环 unordered_setseen; while (n != 1 && seen.find(n) == seen.end()) { seen.insert(n); int sum = 0; while (n > 0) { int digit = n % 10; sum += digit * digit; n /= 10; } n = sum; } return n == 1; } };
题目链接:https://leetcode.cn/problems/container-with-most-water/
给定一个长度为 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
思路
首先看到这种题的第一种想法可能都是暴力枚举每一种情况,但是这样做的复杂度太大,是会超时的,其实这里我们可以通过对撞指针的方式不断改变一边的高度,进而更新最大容量,直至指针相遇。
核心思想和流程的详细解释:
这个算法的核心在于通过不断移动指针,选择较短的线段向内移动,以期望找到更高的线段来提高容器的高度。这样的策略保证了在寻找最大容器面积的过程中,不漏过可能的最优解。算法的时间复杂度是 O(n)。
以下是对应的C++代码:
class Solution { public: int maxArea(vector& height) { int left=0,right=height.size()-1,ret=0; while(left int v=min(height[left],height[right])*(right-left); ret=max(ret,v); if(height[left] 05.有效三角形的个数
题目链接:https://leetcode.cn/problems/valid-triangle-number/description/
给定一个包含非负整数的数组 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思路
这里使用暴力枚举同样会超时,所以这里同样可以使用双指针的方式来解决
核心思想是通过排序数组,然后使用双指针的方式,依次选择一个较大的元素作为三角形的最大边,然后在左边的子数组中使用双指针寻找满足三角形条件的组合。
以下是这个算法的详细解释:
- 排序数组: 首先对输入数组 nums 进行排序,以便于使用双指针的方式进行查找。
- 遍历最大边: 从数组的最后一个元素开始,即 i = n-1,向前遍历数组。这个元素作为三角形的最大边。
- 双指针查找: 使用两个指针 left 和 right 分别指向当前最大边的前两个元素,即 left = 0 和 right = i-1。
- 判断是否构成三角形: 在一个循环中,判断 nums[left] + nums[right] > nums[i] 是否成立。如果成立,说明当前的组合 (nums[left], nums[right], nums[i]) 可以构成一个三角形。此时,由于数组已经排序,那么左边的元素 nums[left] 和任何 nums[k](其中 left < k < right)都能够构成三角形。因此,ret += right - left 表示当前 left 对应的元素与 right 之间的元素均能够构成三角形,加到结果中。
- 更新指针: 根据当前判断的结果,更新指针的位置,即 right-- 或 left++。
- 循环继续: 继续上述步骤,直到 left >= right,然后将 i 减一,进入下一轮循环。
- 返回结果: 最终得到的 ret 即为满足条件的三角形组合的数量。
以下是对应的C++代码:
class Solution { public: int triangleNumber(vector& nums) { sort(nums.begin(),nums.end()); int ret=0; int n=nums.size(); for(int i=n-1;i>=2;i--){ int left=0; int right=i-1; while(left if(nums[left]+nums[right]>nums[i]){ ret+=right-left; right--; }else left++; } } return ret; } }; 06.和为s的两个数字
题目链接:https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/description/
购物车内的商品价格按照升序记录于数组 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]思路
同样暴力枚举会超时,因为这里是升序数组,我们使用双指针的解法可以非常轻松解决
- 初始化指针: 使用两个指针 left 和 right 分别指向数组的第一个元素和最后一个元素。
- 循环查找: 在一个循环中,计算当前 left 和 right 指向的两个元素的和 sum。如果 sum > target,说明当前和过大,需要将 right 向左移动一步;如果 sum < target,说明当前和过小,需要将 left 向右移动一步;如果 sum == target,则找到了满足条件的两个元素,返回结果 {nums[left], nums[right]}。
- 循环继续: 继续上述步骤,直到 left >= right,循环结束。
- 返回结果: 如果循环结束时仍未找到满足条件的两个元素,则返回一个空数组。
这个算法的核心思想是利用有序数组的特性,通过双指针在数组中夹逼的方式,不断调整指针的位置,直到找到满足条件的两个元素或者循环结束。
这个算法的时间复杂度是 O(n)。因为每次循环中,left 或 right 至少有一个向中间移动一步,所以总体的时间复杂度是线性的。
以下是对应的C++代码:
class Solution { public: vectortwoSum(vector & nums, int target) { vector ret; for(int left=0,right=nums.size()-1;left int sum=nums[left]+nums[right]; if(sum>target) right--; else if(sum nums[left],nums[right]}; } return {}; } }; 07.三数之和
给你一个整数数组 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 。思路
其实这道题和两数之和解法类似,还是使用双指针的方式,只不过这里需要我们先排序,具体思路如下
- 排序: 对输入数组 nums 进行升序排序,这样可以使得相同的元素相邻,方便后续的去重操作。
- 遍历: 使用一个外层循环,固定一个数 nums[i] 作为三元组的第一个元素。
- 双指针: 在内层循环中,使用两个指针 left 和 right,分别指向当前数 nums[i] 的下一个元素和数组的最后一个元素。计算当前三个数的和 sum。
- 判断和与目标值的关系:
- 如果 sum > 0,说明右侧的数太大,将 right 向左移动一步。
- 如果 sum < 0,说明左侧的数太小,将 left 向右移动一步。
- 如果 sum == 0,说明找到了一个满足条件的三元组,加入结果集中,并进行去重操作。
- 去重操作: 在找到一个满足条件的三元组后,需要对 left 和 right 进行去重操作,即跳过相同的元素,避免结果中出现重复的三元组。
- 去重 i: 在外层循环中,找到一个满足条件的三元组后,也需要对 i 进行去重操作,即跳过相同的数,避免结果中出现相同的三元组。
- 返回结果: 最终返回所有满足条件的三元组组成的结果集。
以下是对应的C++代码:
class Solution { public: vector> threeSum(vector & nums) { vector > ret; sort(nums.begin(), nums.end()); int n = nums.size(); for (int i = 0; i < n; ) { if (nums[i] > 0) break; //没有负数或等于0的情况和不可能为0故直接跳出 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 { ret.push_back({ 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; } }; 08.四数之和
题目链接:https://leetcode.cn/problems/4sum/description/
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
- 0 <= a, b, c, d < n
- a、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]]思路
看到这个题目时,我们不难发现,可以用双指针的算法进行求解,这道题目还是使用类似三数之和的操作,只不过这里多了一个操作数,我们只需再加一层循环嵌套,以及一些去重优化即可,具体思路如下:
- 排序: 对输入数组 nums 进行升序排序,这样可以使得相同的元素相邻,方便后续的去重操作。
- 遍历: 使用两个外层循环,分别固定两个数 nums[i] 和 nums[j] 作为四元组的前两个元素。
- 双指针: 在内层循环中,使用两个指针 left 和 right,分别指向当前数 nums[i] 和 nums[j] 的下一个元素。计算当前四个数的目标和 aim。
- 判断和与目标值的关系:
- 首先这里不管是aim值还是sum值都要注意一个溢出的问题,所以我们这里都要使用long long进行类型的提升
- 如果 sum > aim,说明右侧的数太大,将 right 向左移动一步。
- 如果 sum < aim,说明左侧的数太小,将 left 向右移动一步。
- 如果 sum == aim,说明找到了一个满足条件的四元组,加入结果集中,并进行去重操作。
- 去重操作: 在找到一个满足条件的四元组后,需要对 left、right、i、j 进行去重操作,即跳过相同的元素,避免结果中出现重复的四元组。
- 返回结果: 最终返回所有满足条件的四元组组成的结果集。
class Solution { public: vector> fourSum(vector & nums, int target) { sort(nums.begin(),nums.end()); vector > ret; int n = nums.size(); for(int i=0;i for(int j=i+1;j int left=j+1,right=n-1; long long aim=(long long)target-nums[i]-nums[j]; while(left long long sum=(long long)nums[left]+nums[right]; if(sum>aim) right--; else if(sum ret.push_back({nums[i],nums[j],nums[left++],nums[right--]}); while(left
上一篇:C语言【位段篇】