双指针算法(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<int>& nums) {
for(int cur=0,dest=-1;cur<nums.size();++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]
思路
这里我们能够想到最简单的一种算法就是,我们再创建一个容器数组逐个遍历,进行零的重写,最后直接覆盖或交换原数组,但是这种写法花费的时间和特别是空间代价太大了,所以我们这种时候利用双指针的算法可以很好的解决这个问题。
算法的核心思想是通过两个步骤实现对数组中的零的复制:
下面是算法的详细步骤:
count
表示数组中零的个数,变量 i
表示当前遍历的位置。i
。r
表示从后向前复制元素的位置,如果复制零后的数组长度超过了原数组长度,补充一个零到最后。i
开始,从后向前遍历数组,根据数组中的元素值进行复制操作,确保零被复制两次,非零元素直接复制。arr
中的零已经被复制一份,实现了题目的要求。以下是对应的C++代码:
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int n=arr.size(),count=0,i=0;
for(i=0;i<n;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
思路
这道题其实我们可以根据一个数学知识知道如果经过不断计算无线循环的话,其中是一定会有重复的数字的,其实这里最好的解法应该是使用哈希思想,但是在这里我们使用双指针也可以有很简单的写法,我们先来看看双指针的写法
解法一:快慢指针
nextSum
,该函数用于计算一个数每位数字的平方和。通过对输入数字每位取模得到个位数,然后累加每位数字的平方。slow
和 fast
,初始时都指向输入的数 n
。在循环中,slow
每次计算一次 nextSum
,而 fast
每次计算两次 nextSum
,这就相当于在一个链表中 slow
每次走一步,fast
每次走两步。slow
和 fast
最终都会进入这个循环,并相遇。如果 slow
最终等于1,则这个数是快乐数;如果 slow
和 fast
相遇但不等于1,说明存在循环,这个数不是快乐数。以下是对应的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
来记录已经出现过的数字,以便检测循环。
以下是对这个算法的核心思想和流程的详细解释:
seen
,用于记录已经出现过的数字。seen
集合中出现过。如果是,则说明这个数是快乐数,直接返回 true
。seen
中,那么将当前数字加入到 seen
中,并计算下一个数字的平方和。这个过程通过迭代计算每位数字的平方和,然后将结果赋值给 n
。n
等于1或者 n
已经在 seen
中出现过。n
是否等于1,如果是则返回 true
,否则返回 false
。这个算法使用了哈希集合来检测循环,避免了在链表中进行双指针遍历的方式。这样的实现在空间上相对较高效,因为哈希集合的查找和插入操作都是平均 O(1) 的时间复杂度。算法的时间复杂度取决于是否存在循环,最坏情况下是 O(logN)。
以下是对应的C++代码:
class Solution {
public:
bool isHappy(int n) {
// 使用一个集合来存储已经出现过的数字,以检测循环
unordered_set<int> seen;
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,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:height = [1,1]
输出:1
思路
首先看到这种题的第一种想法可能都是暴力枚举每一种情况,但是这样做的复杂度太大,是会超时的,其实这里我们可以通过对撞指针的方式不断改变一边的高度,进而更新最大容量,直至指针相遇。
核心思想和流程的详细解释:
left
和 right
分别指向数组的起始和结束位置,同时初始化变量 ret
为0,用来记录最大的容器面积。left
和 right
指向的两个线段之间的容器面积,并将其与 ret
中记录的最大面积进行比较。然后,将 left
或 right
中较小的那个指针向内移动一步。ret
的值为当前面积与 ret
中记录的最大面积的较大者。left
和 right
指针相遇。ret
,即找到的最大容器面积。这个算法的核心在于通过不断移动指针,选择较短的线段向内移动,以期望找到更高的线段来提高容器的高度。这样的策略保证了在寻找最大容器面积的过程中,不漏过可能的最优解。算法的时间复杂度是 O(n)。
以下是对应的C++代码:
class Solution {
public:
int maxArea(vector<int>& height) {
int left=0,right=height.size()-1,ret=0;
while(left<right){
int v=min(height[left],height[right])*(right-left);
ret=max(ret,v);
if(height[left]<height[right]) left++;
else right--;
}
return ret;
}
};
题目链接: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<int>& 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<right){
if(nums[left]+nums[right]>nums[i]){
ret+=right-left;
right--;
}else left++;
}
}
return ret;
}
};
题目链接: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:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> ret;
for(int left=0,right=nums.size()-1;left<right;){
int sum=nums[left]+nums[right];
if(sum>target) right--;
else if(sum<target) left++;
else return {nums[left],nums[right]};
}
return {};
}
};
给你一个整数数组 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
进行去重操作,即跳过相同的数,避免结果中出现相同的三元组。以下是对应的C++代码:
class Solution
{
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
vector<vector<int>> 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;
}
};
题目链接: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
。long long
进行类型的提升sum > aim
,说明右侧的数太大,将 right
向左移动一步。sum < aim
,说明左侧的数太小,将 left
向右移动一步。sum == aim
,说明找到了一个满足条件的四元组,加入结果集中,并进行去重操作。left
、right
、i
、j
进行去重操作,即跳过相同的元素,避免结果中出现重复的四元组。class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
vector<vector<int>> ret;
int n = nums.size();
for(int i=0;i<n;){
for(int j=i+1;j<n;){
int left=j+1,right=n-1;
long long aim=(long long)target-nums[i]-nums[j];
while(left<right){
long long sum=(long long)nums[left]+nums[right];
if(sum>aim) right--;
else if(sum<aim) left++;
else{
ret.push_back({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;
}
};