双指针的基本思想:
用两个指针在序列上移动,配合条件减少循环次数,把 O (n²) 暴力优化到 O (n),一般只用一趟或者两趟遍历。
两个指针通常叫左指针、右指针(慢指针、快指针),按移动方式分成两类:
- 快慢指针(同向双指针) 两个指针朝同一个方向走,一个走得快、一个慢。 常见场景:链表找环、链表中点、滑动窗口、移除数组元素。 核心:快指针负责探测,慢指针标记有效位置。
- 对撞指针(相向双指针) 左指针在开头,右指针在末尾,往中间靠拢。 前提:数组有序。 常见场景:两数之和、三数之和、反转数组、判断回文。 核心:根据当前和 / 大小关系,决定移左还是移右。
关键点
- 双指针本质:利用单调性,避免重复枚举
- 不一定同时移动,每次只动其中一个
- 适用:数组、字符串、链表;大多要求有序或者有单调性质
练习题:
移动零
「数组分两块」是⾮常常⻅的⼀种题型,主要就是根据⼀种划分⽅式,将数组的内容分成左右两部
分。这种类型的题,⼀般就是使⽤「双指针」来解决。
1. 题⽬链接:283. 移动零 - 力扣(LeetCode)
2. 题⽬描述:
给定⼀个数组 nums ,编写⼀个函数将所有 0 移动到数组的末尾,同时保持⾮零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进⾏操作。
⽰例 1:
输⼊: nums = 0,1,0,3,12
输出: 1,3,12,0,0
⽰例 2:
输⼊: nums = 0 输出: 0
3. 解法(快排的思想:数组划分区间 - 数组分两块):
底层逻辑:设置dest 和 cur 把,数组分为三个部分,dest 之前的数组为无0原顺序的数组,dest 和 cur 的都为储存0元素,cur 到 数组末的部分为还未检查部分,cur == nums.size(),检查完毕
dest 和 cur 运行逻辑:
cur 如果不为0的元素那么就与 dest 的位置进行 swap 调换,dest ++,如果是为0不做调换,cur每次进行++操作确保不会死循环。
代码展示:
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
size_t dest,cur;
dest = cur = 0;
while(cur!=nums.size())
{
if(nums[cur]!=0)
{
std::swap(nums[dest++],nums[cur]);
}
cur++;
}
}
};
复写零
给你一个长度固定的整数数组 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]
我的想法:
通过消耗空间,建一个目标数组,在进行拷贝给arr数组,达到目的。
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
vector<int> Marr;
for(size_t i = 0,j=0;j<arr.size(),i!=arr.size();i++)
{
if(arr[i]==0)
{
j+=2;
Marr.push_back(0);
Marr.push_back(0);
}
else
{
Marr.push_back(arr[i]);
j++;
}
}
for(size_t i = 0;i<arr.size();i++)
{
arr[i]=Marr[i];
}
}
};
最优解法:
如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆
盖掉」。因此我们选择「从后往前」的复写策略。
但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两
步:
i. 先找到最后⼀个复写的数;
ii. 然后从后向前进⾏复写操作。
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int dest = -1,cur = 0,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 && dest != cur)
{
if(arr[cur])arr[dest--]=arr[cur--];
else
{
arr[dest--]=0;
arr[dest--]=0;
cur--;
}
}
}
};
快乐数:
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
我的思路:
正常解题,一步一步解析分析出2为什么不是快乐数,进行计算。
发现:
2->4->16->37->58->89->145->42->20->4这个死循环里面,发现如果数据一旦到了4的时候就是死循环,就符合非快乐数,所以进行循环4的拦截。
cpp
class Solution {
public:
bool isHappy(int n) {
int count,_n;
while(n!=1 && n!=4)
{
count = 0;
while(n!=0)
{
_n = n%10;
count +=(_n*_n);
n/=10;
}
n = count;
}
return n == 1;
}
};
但缺点也很明显,需要计算,而却也不能真的确定真的就是4会导致死循环,有可能还存在其他死循环的可能性,不能真的解决问题。
快慢指针解题:
如果是非快乐数就会在一定的有序的数字之间跳动,类似循环,快乐数就会排到一定的数值都会变成1,1,1的循环,有快慢指针解决的话,在有循环的数值又快指针追到慢指针或者在1的循环快指针追到慢指针,也确实可以实现证明是否为快乐数。
cpp
class Solution
{
public:
int isfun(int n) // 返回 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 = isfun(n);
while(slow != fast)
{
slow = isfun(slow);
fast = isfun(isfun(fast));
}
return slow == 1;
}
};