从今天开始,整个暑假期间。我将不定期给大家带来有关各种算法的题目,帮助大家攻克面试过程中可能会遇到的算法这一道难关。
目录
(一) 基本概念
双指针算法是一种常用的算法技巧,它通常用于在数组或字符串中进行快速查找、匹配、排序或移动操作。双指针算法使用两个指针在数据结构上进行迭代,并根据问题的要求移动这些指针。
常⻅的双指针有两种形式:
- ⼀种是对撞指针;
- ⼀种是左右指针。
【优势】
- 双指针算法的优势在于它们能够在一次迭代中完成操作,时间复杂度通常比较低,并且不需要额外的空间;
- 通过合理地移动指针,可以有效地减少不必要的计算和比较,提高算法的效率。
在具体应用双指针算法时,需要根据问题的特点和要求选择合适的指针移动策略,确保算法的正确性和高效性。同时,注意处理边界条件和特殊情况,以避免错误和异常。
(二)题目讲解
接下来,我们通过几道题目让大家具体的感受一下。(题目由易到难)
1、难度:easy
1️⃣移动零
链接如下:283. 移动零
【题⽬描述】
【解法】(快排的思想:数组划分区间-数组分两块)
算法思路:
- 在本题中,我们可以⽤⼀个 cur 指针来扫描整个数组,另⼀个 dest 指针⽤来记录⾮零数序列的最后⼀个位置。根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
- 在 cur 遍历期间,使 [0, dest] 的元素全部都是⾮零元素, [dest + 1, cur - 1] 的元素全是零
【算法流程】
【算法实现】
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]);
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
- 时间复杂度:代码中使用了一个循环来遍历数组,因此时间复杂度为 O(n),其中 n 是数组的长度。
- 空间复杂度:代码中没有使用额外的空间,只是通过交换数组元素的方式来实现移动,所以空间复杂度为 O(1)。
该算法的性能较好,处理速度快,且空间开销较小。由于只进行一次遍历,并且只进行元素交换操作,因此在大多数情况下,时间复杂度为线性级别。然而,在某些特殊情况下,比如数组中几乎所有元素都是非零元素时,仍需要遍历整个数组,但交换操作的次数会减少。
2️⃣复写零
链接如下:1089. 复写零
【题⽬描述】
【解法】(原地复写---双指针)
- 算法思路:
① 如果「从前向后」进⾏原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数「被覆盖掉」。因此我们选择「从后往前」的复写策略。
② 但是「从后向前」复写的时候,我们需要找到「最后⼀个复写的数」,因此我们的⼤体流程分两步:
- 先找到最后⼀个复写的数;
- 然后从后向前进⾏复写操作
【算法流程】
【算法实现】
class Solution {
public:
void duplicateZeros(vector<int>& 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--;
}
}
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
- 时间复杂度:代码中使用了两个循环,第一个循环用于找到最后一个数、处理边界情况,第二个循环用于从后向前完成复写操作。由于第二个循环是针对数组长度的常数倍进行的操作,因此时间复杂度为 O(n),其中 n 是数组的长度。
- 空间复杂度:代码中没有使用额外的空间,只是通过修改原数组实现复写操作,所以空间复杂度为 O(1)。
该算法的性能较好,时间复杂度为线性级别,空间开销较小。在最坏情况下,需要遍历整个数组进行复写操作,但仍保持了线性时间复杂度。
2、难度:medium
1️⃣快乐数
链接如下:202. 快乐数
【题⽬描述】
【解法】(快慢指针)
使用双指针的算法判断快乐数的基本思路如下:
- 定义两个指针,一个指针快指针(fast)每次向前移动两步,一个慢指针(slow)每次向前移动一步。
- 将给定的正整数转换为字符串形式。
- 在一个循环中,不断计算当前数字的各个位上的数字的平方和。
- 将得到的平方和作为下一个数字,继续计算平方和,直到平方和等于 1,或者出现循环。
- 判断循环是否发生,如果发生循环并且平方和不等于 1,说明该数字不是快乐数。
【算法流程】
【算法实现】
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(logn),其中 n 是给定的数字。
- 快慢指针在循环中遍历数字,直到找到结果或者出现循环。最坏情况下,循环次数是一个数字中各个位数的总和,也就是 O(logn)。
- 因此,总体时间复杂度为 O(logn)。
-
空间复杂度:只使用了常量级别的额外空间,不随输入规模变化,因此空间复杂度为 O(1)。
2️⃣盛⽔最多的容器
链接如下:11. 盛最多水的容器
【题⽬描述】
【解法】(对撞指针)
- 首先,初始时左指针指向数组的起始位置,右指针指向数组的结束位置。计算当前区间内的容器的盛水量,并更新最大盛水量。
- 接着,判断两个指针所指向的元素的高度,将较小的元素的指针向内移动一步。这样可以保证每次移动都在选择更高的边界线,以获得可能的更大盛水量。
- 重复上述步骤,直到两个指针相遇,即左指针大于等于右指针。此时遍历完成,返回最大盛水量。
【算法流程】
【算法实现】
class Solution {
public:
int maxArea(vector<int>& 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;
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
- 时间复杂度分析:对撞指针移动的过程中,每次都排除了一部分区域,因此时间复杂度为 O(n),其中 n 是数组的长度。
- 空间复杂度分析:该算法只使用了常数级别的额外空间,存储了少量的变量和指针。因此,空间复杂度为 O(1),与输入规模无关。
3、难度:difficult
2️⃣最大得分
链接如下:1537. 最大得分
【题⽬描述】
【算法流程】
【算法实现】
class Solution {
public:
const int num = 1e9 + 7;
int maxSum(vector<int>& nums1, vector<int>& 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;
}
};
【结果展示】
【性能分析】
具体的性能分析如下:
- 时间复杂度分析: O(n1 + n2),其中 n1 和 n2 分别是两个输入数组的长度。因为我们同时遍历了两个数组,所以时间复杂度与两个数组的总长度成线性关系。
- 空间复杂度分析: O(1),只使用了几个变量来保存结果,所以额外空间是固定的,不会随输入规模增加而增加。
总结
以上便是本期关于双指针算法的全部讲解内容。如果大家掌握了上述知识,再去勤加练习的话我相信以后在遇到此类问题都可迎刃而解。