从今天开始,整个暑假期间。我将不定期给大家带来有关各种算法的题目,帮助大家攻克面试过程中可能会遇到的算法这一道难关。
目录
(一) 基本概念
(二)题目讲解
1、难度:easy
1️⃣移动零
2️⃣复写零
2、难度:medium
1️⃣快乐数
2️⃣盛⽔最多的容器
3、难度:difficult
2️⃣最大得分
总结
双指针算法是一种常用的算法技巧,它通常用于在数组或字符串中进行快速查找、匹配、排序或移动操作。双指针算法使用两个指针在数据结构上进行迭代,并根据问题的要求移动这些指针。
常⻅的双指针有两种形式:
对撞指针:⼀般⽤于顺序结构中,也称左右指针。
• 对撞指针从两端向中间移动。⼀个指针从最左端开始,另⼀个从最右端开始,然后逐渐往中间逼近。
• 对撞指针的终⽌条件⼀般是两个指针相遇或者错开(也可能在循环内部找到结果直接跳出循环),也就是:
◦ left == right (两个指针指向同⼀个位置)
◦ left > right (两个指针错开)
快慢指针:⼜称为⻳兔赛跑算法,其基本思想就是使⽤两个移动速度不同的指针在数组或链表等序列结构上移动。
这种⽅法对于处理环形链表或数组⾮常有⽤。
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使⽤快慢指针的思想。
快慢指针的实现⽅式有很多种,最常⽤的⼀种就是:
• 在⼀次循环中,每次让慢的指针向后移动⼀位,⽽快的指针往后移动两位,实现⼀快⼀慢
【优势】
在具体应用双指针算法时,需要根据问题的特点和要求选择合适的指针移动策略,确保算法的正确性和高效性。同时,注意处理边界条件和特殊情况,以避免错误和异常。
接下来,我们通过几道题目让大家具体的感受一下。(题目由易到难)
链接如下:283. 移动零
【题⽬描述】
【解法】(快排的思想:数组划分区间-数组分两块)
算法思路:
【算法流程】
a. 初始化 cur = 0 (⽤来遍历数组),dest = -1 (指向⾮零元素序列的最后⼀个位置。
因为刚开始我们不知道最后⼀个⾮零元素在什么位置,因此初始化为 -1 )
b. cur 依次往后遍历每个元素,遍历到的元素会有下⾯两种情况:
i. 遇到的元素是 0 , cur 直接 ++ 。因为我们的⽬标是让 [dest + 1, cur - 1] 内
的元素全都是零,因此当 cur 遇到 0 的时候,直接 ++ ,就可以让 0 在 cur - 1
的位置上,从⽽在 [dest + 1, cur - 1] 内;
ii. 遇到的元素不是 0 , dest++ ,并且交换 cur 位置和 dest 位置的元素,之后让cur++ ,扫描下⼀个元素。
- 因为 dest 指向的位置是⾮零元素区间的最后⼀个位置,如果扫描到⼀个新的⾮零元素,那么它的位置应该在 dest + 1 的位置上,因此 dest 先⾃增 1 ;
- dest++ 之后,指向的元素就是 0 元素(因为⾮零元素区间末尾的后⼀个元素就是0 ),因此可以交换到 cur 所处的位置上,实现 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零。
【算法实现】
class Solution {
public:
void moveZeroes(vector& nums) {
for(int cur = 0, dest = -1; cur < nums.size(); cur++)
if(nums[cur]) // 处理⾮零元素
swap(nums[++dest], nums[cur]);
}
};
【结果展示】
具体的性能分析如下:
该算法的性能较好,处理速度快,且空间开销较小。由于只进行一次遍历,并且只进行元素交换操作,因此在大多数情况下,时间复杂度为线性级别。然而,在某些特殊情况下,比如数组中几乎所有元素都是非零元素时,仍需要遍历整个数组,但交换操作的次数会减少。
链接如下:1089. 复写零
【题⽬描述】
【解法】(原地复写---双指针)
① 如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。
② 但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两步:
【算法流程】
a. 初始化两个指针 cur = 0 , dest = 0 ;
b. 找到最后⼀个复写的数:
i. 当 cur < n 的时候,⼀直执⾏下⾯循环:
• 判断 cur 位置的元素:
◦ 如果是 0 的话, dest 往后移动两位;
◦ 否则, dest 往后移动⼀位。
• 判断 dest 时候已经到结束位置,如果结束就终⽌循环;
• 如果没有结束, cur++ ,继续判断。
c. 判断 dest 是否越界到 n 的位置:
i. 如果越界,执⾏下⾯三步:
1. n - 1 位置的值修改成 0 ;
2. cur 向移动⼀步;
3. dest 向前移动两步。
d. 从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:
i. 判断 cur 位置的值:
1. 如果是 0 : dest 以及 dest - 1 位置修改成 0 , dest -= 2 ;
2. 如果⾮零: dest 位置修改成 0 , dest -= 1 ;
ii. cur-- ,复写下⼀个位置
【算法实现】
class Solution {
public:
void duplicateZeros(vector& arr) {
// 1. 先找到最后⼀个数
int cur = 0, dest = -1, n = arr.size();
while(cur < n)
{
if(arr[cur]) dest++;
else dest += 2;
if(dest >= n - 1) break;
cur++;
}
// 2. 处理⼀下边界情况
if(dest == n)
{
arr[n - 1] = 0;
cur--; dest -=2;
}
// 3. 从后向前完成复写操作
while(cur >= 0)
{
if(arr[cur]) arr[dest--] = arr[cur--];
else
{
arr[dest--] = 0;
arr[dest--] = 0;
cur--;
}
}
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
该算法的性能较好,时间复杂度为线性级别,空间开销较小。在最坏情况下,需要遍历整个数组进行复写操作,但仍保持了线性时间复杂度。
链接如下:202. 快乐数
【题⽬描述】
【解法】(快慢指针)
使用双指针的算法判断快乐数的基本思路如下:
【算法流程】
a. 初始化快慢指针为给定的正整数。
b. 在一个循环中,计算快慢指针指向的数字的各个位上的数字的平方和,并将结果赋值给快指针。
c. 同时,慢指针移动一步。
d. 检查快指针和慢指针是否指向同一个数字,如果是,则说明存在循环,退出循环。
e. 如果快指针指向的数字等于 1,则说明是快乐数,返回 true。
f. 否则,将慢指针指向的数字赋值给快指针,重复步骤 b-e。
g. 如果循环结束仍未找到快乐数,则返回 false。
【算法实现】
class Solution {
public:
int getNext(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
bool isHappy(int n) {
int slow = n;
int fast = getNext(n);
while (fast != 1 && slow != fast) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}
return fast == 1;
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
时间复杂度:
空间复杂度:只使用了常量级别的额外空间,不随输入规模变化,因此空间复杂度为 O(1)。
链接如下:11. 盛最多水的容器
【题⽬描述】
【解法】(对撞指针)
【算法流程】
a.初始化最大盛水量
maxArea
为 0,左指针left
指向数组起始位置,右指针right
指向数组结束位置。b.进入循环,判断条件为左指针小于右指针。
◦ 计算当前区间的宽度、最小高度以及当前盛水量。
◦ 更新最大盛水量,取当前盛水量与历史最大盛水量的较大值。
◦ 判断两个指针所指向元素的高度,将较小元素的指针向内移动一步。
c. 循环结束后,返回最大盛水量作为结果。
【算法实现】
class Solution {
public:
int maxArea(vector& height) {
int res = 0;
int left = 0;
int right = height.size() - 1;
while (left < right) {
int width = right - left;
int minHeight = min(height[left], height[right]);
int v = width * minHeight;
res = max(res, v);
if (height[left] < height[right]) {
left++;
}
else {
right--;
}
}
return res;
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
链接如下:1537. 最大得分
【题⽬描述】
【算法流程】
初始化双指针
i
和j
分别为 0,并初始化两个变量sum1
和sum2
用于记录当前路径的和,初始化 res 用于记录最大得分。在循环中,比较当前指针位置上
nums1[i]
和nums2[j]
的值:
- 如果
nums1[i] < nums2[j]
,则将nums1[i]
加到sum1
中,并将指针i
向后移动一位。- 如果
nums1[i] > nums2[j]
,则将nums2[j]
加到sum2
中,并将指针j
向后移动一位。- 如果
nums1[i]
和nums2[j]
相等,意味着遇到了相同的值,此时需要考虑路径的切换。比较sum1
和sum2
的大小,将较大的值加到 res 中,并将sum1
和sum2
清零。然后将nums1[i]
加到 res中,同时将指针i
和j
向后移动一位。循环结束后,可能还会存在剩余未遍历的元素。此时,需要将剩余路径的和加到 res中。
返回 res
%
num,即对10^9 + 7
取余后的最大得分。
【算法实现】
class Solution {
public:
const int num = 1e9 + 7;
int maxSum(vector& nums1, vector& nums2) {
int n1 = nums1.size(), n2 = nums2.size();
int i = 0, j = 0;
long long sum1 = 0, sum2 = 0; // 使用 long long 类型防止整数溢出
long long res = 0;
while (i < n1 && j < n2) {
if (nums1[i] < nums2[j]) {
sum1 += nums1[i++];
}
else if (nums1[i] > nums2[j]) {
sum2 += nums2[j++];
}
else { // 遇到相同值,取两个路径中的较大值
res += max(sum1, sum2) + nums1[i];
sum1 = 0, sum2 = 0;
i++;
j++;
}
}
// 处理剩余的元素
while (i < n1) {
sum1 += nums1[i++];
}
while (j < n2) {
sum2 += nums2[j++];
}
res += max(sum1, sum2); // 加上剩余路径的和
return res % num;
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
以上便是本期关于双指针算法的全部讲解内容。如果大家掌握了上述知识,再去勤加练习的话我相信以后在遇到此类问题都可迎刃而解。