双指针是一种在算法和数据结构中常用的技巧,尤其适用于数组、链表等线性数据结构。双指针通常有两个指针在数据结构中进行遍历,借助这两个指针来实现高效的算法。
1. 常见的双指针
1. 1快慢指针
快慢指针是两个指针以不同的速度在数据结构中移动的技巧。通常情况下,一个指针每次移动一步(慢指针),另一个指针每次移动两步(快指针)。常见的应用有:
- 判断链表是否有环:如果链表中有环,快指针最终会追上慢指针。
- 找到链表的中点:当快指针到达末尾时,慢指针正好在中点位置。
ListNode* detectCycle(ListNode* head)
{
ListNode *slow = head, *fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{ // 有环
slow = head;
while (slow != fast)
{
slow = slow->next;
fast = fast->next;
}
return slow; // 返回环的入口
}
}
return nullptr;
}
1.2 左右指针(对撞指针)
左右指针一般用于在数组上进行处理。一个指针从左边开始,另一个指针从右边开始,两者向中间靠拢,直到满足某种条件。常见的应用包括:
- 数组的元素对求和问题:如两数之和。
- 判断回文串:左右指针逐一比较元素,直到碰撞。
//判断回文串
bool isPalindrome(const string& s) {
int left = 0, right = s.size() - 1;
while (left < right) {
if (s[left] != s[right]) return false;
left++;
right--;
}
return true;
}
1.3 滑动窗口
滑动窗口是一种特殊的双指针技巧,用于处理连续子序列的问题。它通常应用在找到数组或字符串中的某个子集,使得子集满足某种要求(如长度、和等)。
- 常用于字符串或数组中的子串、子数组问题,如最大长度、最小和等。
- 通过窗口的左右边界调整窗口大小,从而实现高效搜索。
//数组大于target的最小区间
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, sum = 0, result = INT_MAX;
for (int right = 0; right < nums.size(); ++right) {
sum += nums[right];
while (sum >= target) {
result = min(result, right - left + 1);
sum -= nums[left++];
}
}
return result == INT_MAX ? 0 : result;
}
双指针方法的优势在于其高效性,特别是在涉及到子数组、子串问题时,可以避免使用嵌套循环,使时间复杂度降低到线性级别。
接下来我们结合各种运用双指针的的题来熟练掌握双指针的用法。我们接下来主要是使用双指针的思想来解题,其中的指针并不是真正的指针,通常是使用数组下标来充当指针。
2. 移动零
2.1 题目介绍
2.2 思路分析
(1)定义两个指针cur 和dest,cur遍历数组,dest指向已经处理区间的非零元素的最后一个位置,这样我们可以得到三个区间
(2)每当遇到不为零的数时,cur与dest所在位置的数进行交换
2.3 代码实现
class Solution {
public:
void moveZeroes(vector<int>& nums) {
for(int cur = 0, dest = -1; cur < nums.size(); cur++)
{
if(nums[cur])
{
swap(nums[cur], nums[++dest]);
}
}
}
};
3. 复写零
3.1 题目介绍
3.2 思路分析
如果题目没有要求只能对数组就地修改,我们可以通过创建另一个数组,然后遍历原数组,很容易实现代码。如果是就地修改,解题思路是:
(1)先找到最后一个“复写”的值:定义指针cur和dest,如果nums[cur]是零,dest移动两步,非零则移动一步。直到dest指向数组的最后一个元素或后面一个位置,此时cur指向最后一个复写的值。
(2)如果此时dest刚好指向数组的最后一个元素,可以直接从后往前完成复写操作
(3)如果dest指向数组最后一个元素后面一个位置,说明,dest移动了两步,cur此时指向的值是0,这时就要特殊处理一下,将数组最后一个元素的值置为0,dest向前走两步。(这种情况下复写的两个0一个是数组最后一个元素的,另一个是最后元素后面一个位置的,与数组倒数第二个元素无关)
3.3 代码实现
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int cur = 0, dest = -1, n = arr.size();
while (cur < n)
{
if(arr[cur]) dest++;
else dest += 2;
if (dest >= n - 1) break;
cur++;
}
if (dest == n)
{
arr[n - 1] = 0;
dest -= 2;
cur--;
}
while (cur >= 0)
{
if (arr[cur])
arr[dest--] = arr[cur--];
else
{
arr[dest--] = arr[cur--];
arr[dest--] = 0;
}
}
}
};
4. 快乐数
4.1 题目介绍
4.2 思路分析
由于这个数最终都会循环,所以我们可以使用快慢指针来解题,等到快指针追上慢指针是判断该数是否为1即可。
(1)定义一个函数用来返回n的替换后的数
(2)定义快慢指针,慢指针每次走一步,快指针每次走两步,当快慢指针相同时,判断此时的值是否为1
4.3 代码实现
class Solution {
public:
int Square(int n)
{
int sum = 0;
while(n)
{
int t = n % 10;
sum += t * t;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
int slow = n, fast = Square(n);
while (slow != fast)
{
slow = Square(slow);
fast = Square(Square(fast));
}
return slow == 1;
}
};
5. 盛水最多的容器
5.1 题目介绍
5.2 思路分析
所以我们定义左右指针从首尾开始遍历,计算此时区间可以盛纳的水,比较存储盛水最多的容器
每次让两个指针较小的那一个移动。
5.3 代码实现
class Solution {
public:
int maxArea(vector<int>& height) {
int left = 0, right = height.size() - 1, ret = 0;
while (left < right)
{
int m = min(height[left], height[right]) * (right - left);
ret = max(m, ret);
if (height[left] < height[right]) left++;
else right--;
}
return ret;
}
};
6. 有效三角形个数
6.1 题目介绍
6.2 思路分析
可以组成三角形三条边的条件是任意两边之和大于另一条边,因此我们让两条较小的边之和大于另一条边即可。
(1)首先将数组从小到大排序
(2)固定最大的一条边,定义左右指针left和right为另两条边,left从0开始向右遍历,right从小于最大边的位置开始想左遍历
(3)如果两边之和大于另一条边,那么,(left, right)之间的边与right之和也一定大于另一条边。
(4)如果两边之和小于最大边,则说明边太小,left向右移动
6.3 代码实现
class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
int n = nums.size(), ret = 0;
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;
}
};
7. 和为s的两个数字
7.1 题目介绍
7.2 思路分析
定义左右指针遍历数组,商品价格是单调递增的,因此左右指针之和小于target时,左指针移动,大于target,右指针移动。
7.3 代码实现
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target) {
int left = 0, right = price.size() - 1;
vector<int> ret(2);
while (left < right)
{
int sum = price[left] + price[right];
if(sum > target) right--;
else if (sum < target) left++;
else{
ret[0] = price[left];
ret[1] = price[right];
break;
}
}
return ret;
}
};
8. 三数之和
8.1 题目介绍
8.2 思路分析
这个题的思路与有效三角形的个数有相似之处。
(1)首先将数组进行排序
(2)固定一个i为数组的最后一个元素,随着程序的进行向前移动,定义左右指针指针遍历【0,i - 1】之间的数。
(3)当三数之和大于零时,right--来减小数,小于零则left++。
(4)因为答案中不能包含重复三元组,所以每当找到一组符合的数之后,left和right要跳过和符合组相同的数,并且将符合组存下来。
(5)当完成left<right的循环时,i 也要跳过重复数。
8.3 代码实现
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> ret;
int i = nums.size() - 1;
while (i >= 2)
{
int left = 0, right = i - 1;
while (left < right)
{
int sum = nums[left] + nums[right] + nums[i];
if (sum > 0) right--;
else if (sum < 0) left++;
else
{
ret.push_back({nums[left], nums[right], nums[i]});
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++, right--;
}
}
while (i >= 2 && nums[i] == nums[i - 1]) {i--;}
i--;
}
return ret;
}
};
9. 四数之和
9.1 题目介绍
9.2 思路分析
该题在上题的基础上在多加一层循环,将四数之和为零改为三数之和为另一个数的负数即可。
9.3 代码实现
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target)
{
sort(nums.begin(), nums.end());
vector<vector<int>> ret;
for (int i = nums.size() - 1; i >= 3; i--)
{
int j = i - 1;
while (j >= 2)
{
int left = 0, right = j - 1;
while (left < right)
{
long long newtarget = (long long)target - nums[i] - nums[j];
int sum = nums[left] + nums[right];
if (sum > newtarget) right--;
else if (sum < newtarget) left++;
else
{
ret.push_back({nums[left], nums[right], nums[j], nums[i]});
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++, right--;
}
}
while (j >= 2 && nums[j] == nums[j - 1]) j--;
j--;
}
while (i >= 3 && nums[i] == nums[i - 1]) i--;
}
return ret;
}
};