双指针算法
文章目录
- 双指针算法
- 概念剖析
- 实战练习
- [643. 子数组最大平均数 I - 力扣(LeetCode)](https://leetcode.cn/problems/maximum-average-subarray-i/)
- [61. 旋转链表 - 力扣(LeetCode)](https://leetcode.cn/problems/rotate-list/description/)
- [481. 神奇字符串 - 力扣(LeetCode)](https://leetcode.cn/problems/magical-string/)
- [11. 盛最多水的容器 - 力扣(LeetCode)](https://leetcode.cn/problems/container-with-most-water/description/)
- [42. 接雨水 - 力扣(LeetCode)](https://leetcode.cn/problems/trapping-rain-water/description/)
- 总结
概念剖析
废话不多说,直接进入正题
-
第一点需要明确,双指针不是用指两个c语言中的指针去操作,其他没有指针的语言也可以实现,是两个“指针”不断位移来解决问题的一种算法,比如经常谈及的滑动窗口(特殊在两个指针移动时二者的距离始终不变)就是双指针算法的一种,熟悉的快速排序和归并排序在实现过程中也运用了该思想
-
大部分使用双指针的算法都可以用两层循环去暴力(遍历数组)解决,那么双指针的优势在哪里呢?
O(n)
的效率,朴素做法通常为O(n^2)
,大部分算法题是有时间限制的,暴力枚举通常无法通过所有测试点 -
而根据两个指针的方向不同,也可以分为同向指针和相向指针两种,像滑动窗口就是同向,同向中自然会涉及到移动的速度,就有了快慢指针法的概念,不过今天的实战中并没有该部分的典型习题,说着其实还挺像物理中的追及相遇问题
概念就是这么简单,具体的还需要在实战中体会
实战练习
先来一道经典的滑动窗口热热身
643. 子数组最大平均数 I - 力扣(LeetCode)
double findMaxAverage(int* nums, int numsSize, int k) {
int sum = 0;
int max;
for (int left = 0; left <= numsSize - k; left++) {
if (left == 0) {
for (int i = 0; i < k; i++) {
sum += nums[i];
}
max = sum;
}
else {
sum = sum - nums[left - 1] + nums[left + k - 1];
max = fmax(max, sum);
}
}
return (double)max / k;
}
单纯的暴力遍历,求子数组的最大平均数,长度固定,即求子数组最大和
计算最大和除了最开始需要循环求出第一组数的和,后来依靠双指针的思想,窗口每移动一次只需要加上右边进来的元素,并减去左边出去的元素即可
热身完毕,数据的载体并不一定是数组,有一种反转链表的方法叫双指针法,同样也是双指针思想的应用,这里不对其做具体的解释,来看一道其他的题
61. 旋转链表 - 力扣(LeetCode)
这题真简单,和双指针能有什么关系,上来就写直接过
struct ListNode* rotateRight(struct ListNode* head, int k) {
struct ListNode *tep = head;
while (k >= 0) {
tep = tep->next;
if (tep == NULL) {
tep = head;
}
k--;
}
return tep;
}
显然这是错的,因为这个链表不是环,这样写只能找到正确的头,会把头在原本链表中前面的数据全部丢失,如果是环这道题也没必要出了,所有我们可以考虑将链表变成环再断开,这样问题就得以解决了
理清思路后还有一点需要注意 在为了效率用k % n
得出新的k
的时,必须用n
减去这个值,否则出来的结果总是会少移一位
struct ListNode* rotateRight(struct ListNode* head, int k) {
if (k == 0 || head == NULL || head->next == NULL) {
return head;
}
int n = 1;
struct ListNode *tail = head;
while (tail->next != NULL) {
tail = tail->next;
n++;
}
k = n - k % n;
if (k == n) {
return head;
}
tail->next = head;
while (k--) {
tail = tail->next;
}
struct ListNode *result = tail->next;
tail->next = NULL;
return result;
}
有人又要问了,这里面也没见left
和right
,怎么能叫双指针解法,双指针是一种思想,其本身并没有太多的限制,这道题体现为head
和tail
,在两个指针的作用下连接成环,完成这最关键的一步
当然还有些题连载体都没有给你,比如下来这道,这个题与其他题不同的一点在于它没有给要进行操作的所有数据,只给了规律,这就需要先构造出原数组了
481. 神奇字符串 - 力扣(LeetCode)
好神奇,一神奇就蒙了,相信这是绝大数人见到此题的第一反应
它的问法本身是极其简单的,这就说明这道题的考察点实际上在构造这一步,这题既然都在双指针算法的文章下出现了,毫无疑问双指针的运用就是核心了
那么问题来了,怎么用?
有一点和上一个题很相似,没有传统意义上的left
和right
指针,现在的问题转化为两个指针往哪放
构建一个不断延伸的数组,且我们已经知道了前几位和具体的规律,那么每次延伸时只需要关注下一位即可,则必定有一个指针始终先一步指到下一位提供可以构造的位置
构造好了还需要算出1
的个数,则另一个指针必须指向当前最右边的位置,让构造出的数字能顺利进行统计
int magicalString(int n) {
if (n < 4) {
return 1;
}
char s[n + 1];
memset(s, 0, sizeof(s));
s[0] = '1', s[1] = '2', s[2] = '2';//构建初始数列
int cnt = 1, size, tep;
int i = 2, j = 3;
while (j < n) {
size = s[i] - '0';
tep = 3 - (s[j - 1] - '0');
while (size > 0 && j < n) {
s[j++] = '0' + tep;
if (tep == 1) {
cnt++;
}
size--;
}
i++;
}
return cnt;
}
tep
是每次需要增加的数字,而size
则是需要增加的数量,通过两个指针与初始数列不断模拟即可解决此题
干巴巴的找数组里面的符合条件的数还是很无聊的,上情景
11. 盛最多水的容器 - 力扣(LeetCode)
盛水的面积在本题中始终是一个矩形,面积长×宽
即可算出,让本题的难度大大降低
与常说的短板效应一样,装水的多少取决于相对较短的那边,则两个关键长度分别是数组下标差和其内元素较小的那个
而数组中的元素对面积的影响明显大于数组下标差,所以优先要找到存在的最大值
左右指针向中间移动,装水的多少取决于短板,那就固定长板就好了,有了长板,那面积真就取决于短板了,这个时候在找到一个尽可能大的短板即可
int maxArea(int* height, int heightSize) {
int left = 0, right = heightSize - 1;
int max = fmin(height[0], height[1]);
while (left < right) {
max = fmax(max, (right - left) * fmin(height[left], height[right]));
if (height[left] < height[right]) {
left++;
}
else {
right--;
}
}
return max;
}
这个题还行吧,但它还是理想化了,实际生活中水的存储不能总是一整块矩形,所以下面这道题应运而生
42. 接雨水 - 力扣(LeetCode)
上一道题进化了,这次我们采用了最真实的物理引擎,不让每一次的雨水都理想化降下来形成一块矩形,自然难度也是肉眼可见的提升
核心的思路还是上一道题中的短板决定储水量,固定长板,指针也还是左右两个指针向中间走,可是突然就不会了,如果不是在双指针的题库中见到这道题,博主甚至想不到可以用双指针去解
但既然核心思路相同,那我们就再去尝试求矩形的面积,可这不是不规则图形吗,别着急,换个角度思考,虽然不规则但是不是还是由许多矩形构成的,这就是问题的突破口
把这些连着的柱子强行看成分开的一根根柱子,再把储水的区域从下到上每层每层地分成一个个小矩形,问题的突破口正式出现在眼前
现在再次观察题目中的样图,每一个矩形和它旁边的柱子拎出来单独看,这是不是和第一题已经差不多了,只不过形成的只有单层的,因为在前面每一个多层已经被我们分成一个个单层了,那这个时候各个矩形的面积就是下标之差,整体面积就可以求解了
int trap(int* height, int heightSize) {
int sum = 0;
int tep = 0;
int left = 0, right = heightSize - 1;
while (left < right) {
if (height[left] < height[right]) {
if (tep < height[left]) {
tep = height[left];
}
else {
sum += tep - height[left];
}
left++;
}
else {
if (tep < height[right]) {
tep = height[right];
}
else {
sum += tep - height[right];
}
right--;
}
}
return sum;
}
事实上这种复杂题目一般都不止一种方法,像这道题还能用动态规划和单调栈去做,以后笔者学习更多思想后这道题还会拿出来再说的
总结
总结一下,看完这五道题目,我们能发现能用双指针去解决的题目都有几点共性
-
要让两个“指针”出现并起作用,首先得有一大片数据, 通常 这些数据的载体是数组,总之需要我们找到载体
-
其次是要在这些数据中找到一个条件达成,如果没有规定左右区间的长度,又会出现与左右两端的数据进行比较取出极限值的情况
像接雨水这道题,题目中包含的条件非常隐晦,以至于很难想到用双指针去做,但经过分析找到它也存在这种边界上的比较,即短边限制储水量,在见到足够多的题后如果能顿悟到这一点,就会很自然的将双指针的思想运用到题目中了
有句话说的很好,菜就多练,这句话对所有的算法题都很适用,想要将双指针思想活学活用,多做题是唯一的捷径了