双指针问题 总结
目录
1、快慢指针的技巧和常用算法(一般为快指针移动一个位置,慢指针移动两个)
1、快慢指针的技巧和常用算法(一般为快指针移动一个位置,慢指针移动两个)
(1)可以求未知长度单链表的中间元素
思路:当快指针走到链表尾时,此时慢指针的位置即为所求。因为慢指针走了快指针的一半。
当链表的⻓度是奇数时,slow 恰巧停在中点位置;如果⻓度是偶数,slow 最终的位置是中间偏右。
while (fast != null && fast.next != null)
{
fast = fast.next.next;
slow = slow.next;
}
// slow 就在中间位置
return slow;
寻找链表中点的⼀个重要作⽤是对链表进⾏归并排序。
例如对数组的归并排序就是求中点,然后递归地把数组⼆分,最后合并两个有序数组。
对于链表,合并两个有序链表是很简单的,难点就在于⼆分。 此时用快慢指针能很方便地找到链表的中点,这样就能实现链表 了的⼆分。
(2)可以判断链表是否有环
思路:当链表无环时,快指针走到链表末尾时就会退出循环,给出判断;
当链表有环时,快指针一定会追上慢指针,此时可以判断出链表有环。
boolean hasCycle(ListNode head)
{
ListNode fast, slow;
fast = slow = head;
while (fast != null && fast.next != null)
{
fast = fast.next.next;
slow = slow.next;
if (fast == slow)
return true;// 有环
}
return false; //无环
}
(3)寻找倒数第 k 个节点
我们的思路还是使⽤快慢指针,让快指针先⾛ k 步,然后快慢指针开始同速前进。
这样当快指针⾛到链表末尾NULL时,慢指针所在的位置就是倒数第 k 个链表节点。
ListNode *findkthtoLast(listNode *head, int k)
{
ListNode *runner = head;
ListNode *chaser = head;
if(head == NULL || k < 0)
return NULL;
for(int i=0; i<k; ++i) // 快指针先走 k 步
{
runner = runner->next;
}
if(runner == NULL) //要判断是否k大于链表长度
return NULL;
while(runner->next != NULL){
chaser = chaser->next;
runner = runner->next;
}
return chaser;
}
(4)求一个循环链表的循环开始节点
思路:
快指针以两倍速遍历,因为有环,所以慢指针肯定会和快指针相遇;
相遇后,将慢指针指向head节点, 此时快指针在相遇的节点,两个指针再以相同的速度遍历链表,
再次相遇的节点即为循环开始的节点。
分析:
第⼀次相遇时,假设慢指针 slow ⾛了 k 步,那么快指针 fast ⼀定⾛了 2k 步,也就是说 fast ⽐ slow 多⾛了 k 步(也就是环的⻓度)。
设相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k-m,也就是说如果从 head 前进 k - m 步就能到达环起点。 巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。
所以,只要我们把快慢指针中的任⼀个重新指向 head,然后两个指针同速 前进,k - m 步后就会相遇,相遇之处就是环的起点了。
(图片来自 labuladong 公众号文章,公众号对算法的分析和总结很全面,感兴趣的可以关注学习,本文就是参考其中的文章)
(5)判断两个链表是否相交及找到第一个交点
判断是否有交点:
先判断两个链表是否有环,如果一个有环一个没环,肯定不相交;若都无环则判断两个链表的尾节点是否相等;若两个链表都有环,则判断一个链表环开始的节点是否在另一个链表上。
找第一个交点:
求出两个链表的长度L1,L2(如果有环,则将快慢指针第一次相遇的节点作为尾节点来算),假设L1 < L2,用两个指针分别从两个链表的头部开始走,长度为L2的链表先走(L2 - L1),然后两个指针一起走,直到两者相遇,相遇的节点就是第一个交点。
若对空间复杂度没有要求,可以考虑使用栈来解决
创建两个栈,从头遍历两个链表,第一个栈存储第一个链表的节点,第二个栈存储第二个链表的节点。
每遍历到一个节点时,就将该节点入栈。两个链表都入栈结束后。则通过top判断栈顶的节点是否相等,即可判断两个单链表是否相交。因为我们知道,若两个链表相交,则从第一个相交节点开始,后面的节点都相交。 若两链表相交,则循环出栈,直到遇到两个出栈的节点不相同,则这个节点的后一个节点就是第一个相交的节点。
2、左右指针常用的算法与技巧
左右指针在数组中实际是指两个索引值,⼀般初始化为 left = 0, right = nums.length - 1。
(1)二分查找(很简单)
int binarySearch(int[] nums, int target)
{
int left = 0;
int right = nums.length-1;
while(lefr <= right)
{
int mid = (left + right) / 2;
if(nums[mid] == target)
return mid;
else if(nums[mid] < target)
left = mid + 1;
else if(nums[mid] > target)
right = mid - 1;
}
return -1;
}
(2) 两数之和
对于有序数组,就应该很快的想到双指针的技巧。leetcode 167. 两数之和
/*
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
来源:力扣(LeetCode)
*/
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0;
int right = numbers.size()-1;
int sum;
while(left <= right)
{
sum = numbers[left] + numbers[right];
if(sum == target)
return {left+1, right+1};
if(sum > target)
--right;
if(sum < target)
++left;
}
return {-1, -1};
}
};
(3) 反转数组
void reverse(int[] nums)
{
int left = 0;
int right = nums.length - 1;
while (left < right)
{
// swap(nums[left], nums[right])
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
(4) 滑动窗口 算法
滑动窗⼝算法的思路是这样:
(问题描述如: 在字符串S中找出:包含T所有字母的最小子串)
1) 我们在字符串S中使⽤双指针中的左右指针技巧,初始化left = right = 0,把索引闭区间 [left, right] 称为⼀个「窗⼝」。
2) 我们先不断地增加 right 指针扩⼤窗⼝ [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
3) 此时,我们停⽌增加 right,转⽽不断增加 left 指针缩⼩窗⼝ [left, right],直到窗⼝中的字符串不再符合要求(不包含 T 中的所有字符了)。 同时,每次增加 left,我们都要更新⼀轮结果。
4) 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
这里贴出 labuladong 总结出的滑动窗⼝算法的模板:
int left = 0, right = 0;
while (right < s.size())
{
window.add(s[right]);
right++;
while (valid)
{
window.remove(s[left]);
left++;
}
}
Leetcode有几篇滑动窗口的应用题目,供大家参考: