双指针是一种解决问题的技巧或者思维方式,指在访问一个序列中的数据时使用两个指针进行扫描,两个指针可以是同向的,也可以是反向的;我们的关注点可以是这两个指针指向的两个元素本身,也可以是两个指针中间的区域。
概念
快慢指针是两个指针同向移动,某一时刻来看两个指针一个在前,一个在后,即快指针和慢指针。造成两个指针一前一后的原因有两种情况:
①.两个指针速度相同,但是出发时间不同,也可以认为是出发起始位置不同,在两个指针都出发后会以固定的距离间隔一前一后的向前移动;
②.两个指针速度不同,但是从同一个起点同时出发,之后会以固定的速度差一前一后的向前移动,两个指针的距离间隔随时间规律的递增;
快慢指针的应用
⑴.面试题22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
来源:力扣(LeetCode)
①.题目分析
访问链表的节点只能从前往后进行节点的遍历。对应本题一般解法可以先从头到尾进行一趟遍历得到链表长度 length ,然后再从头开始进行一趟 遍历,但是只遍历 length - k 个节点即可。
利用双指针可以只进行一次遍历即可,两个指针都从头结点出发,让快指针先走 k 步,然后慢指针再出发,两个指针以相同的速度前进,则两个 指针间的距离始终为 k ,所以当快指针刚好走过链接尾节点时,慢指针刚好到达倒数第 k 个节点的位置。
②.代码示例
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode *fast = head, *slow = head; //初始化
while(k--) { //让false指针先走K步
fast = fast->next;
}
while(fast != nullptr) {//两个指针以相同的步长同时移动,直到 fast == nullptr
fast = fast->next;
slow = slow->next;
}
return slow;//slow刚好为倒数第k的位置
}
};
⑵.876. 链表的中间结点
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.来源:力扣(LeetCode)
①.题目分析
访问链表的节点只能从前往后进行节点的遍历。对应本题一般解法可以先从头到尾进行一趟遍历得到链表长度 length ,然后再从头开始进行一趟 遍历,遍历到第 N/2 个元素即可。
同样,利用双指针可以只进行一次遍历即可,两个指针同时从头节点结点出发,快指针每次走两步,慢指针走一步,则相同的时间内快指针前进 的距离始终是慢指针的两倍,所以当快指针刚好走过链接尾结点时,慢指针刚好到达链表的中间结点位置。
②.代码示例
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* middleNode(ListNode* head) {
ListNode * fast= head, * slow = head;//初始化
while(fast != NULL && fast->next !=NULL)
{
slow = slow->next;//慢指针走一步
fast = fast->next;//快指针走两步
fast = fast->next;
}
return slow;
}
};
⑶.141. 环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。来源:力扣(LeetCode)
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
①.题目分析
我们对链表从头结点开始进行遍历,若链表中存在环,则环内的结点会重复一直遍历,不存在环或者环外的结点只会遍历一次;因此我们用一个 set 来存储已经遍历过的结点,当遍历到一个结点时查找 set中是否存在,若已经存在则一定存在环。
也可以使用双指针来解答本题,两个指针同时从头节点结点出发,快指针每次走两步,慢指针走一步,则单位时间内快指针相对慢指针移动距离的增量为 1 ,即经过相同的时间 k ,快指针比慢指针多走了 k 步;当两个指针都进入环中时,假设规定慢指针在前、快指针在后,即快指针追赶慢指针,因为单位时间内快指针可以把距离缩短 1 步,因此随着时间的推移快指针肯定会追赶上慢指针。若不存在环,则快指针和慢指针不会在链表中的某个结点相遇。
②.代码示例
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode * fast= head,* slow = head;//初始化
while(fast != NULL && fast->next !=NULL)
{
slow = slow->next;//慢指针走一步
fast = fast->next;//快指针走两步
fast = fast->next;
if( fast == slow)//两者相遇则有环
return true;
}
return false;
}
};
⑷.142. 环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。来源:力扣(LeetCode)
①.题目分析
我们对链表从头结点开始进行遍历,若链表中存在环,则环内的结点会重复一直遍历,不存在环或者环外的结点只会遍历一次;因此我们用一个 set 来存储已经遍历过的结点,当遍历到一个结点时查找 set中是否存在,若已经存在则一定存在环,并且第一个遇到的重复访问的点即为环的第一个点。
也可以使用双指针来解答本题,两个指针同时从头节点结点出发,快指针每次走两步,慢指针走一步,则单位时间内快指针相对慢指针移动距离的增量为 1 ,即经过相同的时间 k ,快指针比慢指针多走了 k 步;当两个指针都进入环中时,假设规定慢指针在前、快指针在后,即快指针追赶慢指针,因为单位时间内快指针可以把距离缩短 1 步,因此随着时间的推移快指针肯定会追赶上慢指针;当两个指针相遇时,假设慢指针总共走的距离为 S,则快指针走的距离为 2S,可以得出环的长度为 S ,设环起点到相遇点的距离为 K ,则从头结点到环入口结点的距离为 S-K,同样从相遇点到环入口结点的距离也为 S-K ,若此时把一个指针挪动到头节点,两者以相同的速度前进,一定会相遇在环的入口点。
②.代码示例
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode * fast= head,* slow = head;//初始化
while(fast != NULL && fast->next !=NULL)
{
slow = slow->next;//慢指针走一步
fast = fast->next;//快指针走两步
fast = fast->next;
if( fast == slow)//两者相遇则有环
break;
}
slow = head;//把慢指针挪回头结点
while (slow != fast)
{
fast = fast.next;//相同速度前进
slow = slow.next;
}
return slow;
}
};
概念
左右指针是初始将两个指针分别放在头和尾的位置,然后相向开始移动,直到两个指针相遇;
左右指针一般用在有序数组中,可以批量的排除不符合要求的元素,将两重循环O(n2)的时间复杂度转化为一重循环O(n+m)的线性复杂度。
左右指针的应用
⑴.二分查找
二分查找就是利用有序的特点,一次可以排除一半数据。
//nums中查找等于target的元素的下标
int searchInsert(vector& nums, int target)
{
int left = 0;
int right = nums.size()-1;
while(left <= right)
{
int mid = left + (right-left)/2;
if( nums[mid] > target )//mid处的元素值比target大,则区间[mid,right]中的元素都比target大
{
right = mid-1;//缩小右边界,排除一半数据
}
else if( nums[mid] < target )//mid处的元素值比target小,则区间[left,mid]中的元素都比target小
{
left = mid +1;//扩大左边界,排除一半数据
}
else
{
resault = mid;//找到等于target 的元素了
}
}
return -1;
}
⑵.167. 两数之和 II - 输入有序数组
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。来源:力扣(LeetCode)
①.题目分析
常规暴力解法也是使用双指针,先固定一个指针 left,然后另一个指针 right 开始遍历,若没有找到,则移动 left ,然后再次用 right 进行遍历。
利用数组已经有序的特点,可以参考二分查找的思想,批量的进行排除数据。开始将 left 、right 分别放在数组的两端,比较 nums[left] + nums[right]:
● 若 nums[left] + nums[right] > target ,因为nums[left] 已经是最小值,则 right 和 [left,right-1] 中的元素组合都可以排除;需要缩小right
● 若 nums[left] + nums[right] < target ,因为nums[right] 已经是最大值,则 left 和 [left+1,right] 中的元素的组合都可以排除;需要增大left
②.代码示例
class Solution {
public:
vector twoSum(vector& numbers, int target) {
int left = 0, right = numbers.size() - 1;//初始化left和right指针
while (left < right)
{
int sum = numbers[left] + numbers[right];
if (sum == target)
return {left + 1, right + 1};
else if (sum < target)//比target小,则需要增大较小的数
left++;
else//比target大,则需要缩小较大的数
right--;
}
return {-1, -1};
}
};
⑶.15. 三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]来源:力扣(LeetCode)
①.题目分析
该题的本质和求两数之和是一样的,相当于两数只和是动态的,再加一层循环。
class Solution {
public:
vector> threeSum(vector& nums) {
vector> resault;
if( nums.size() < 3)
return resault;
sort(nums.begin(),nums.end());
for(int i= 0;i < nums.size()-2;i++)
{
if(i > 0 && nums[i] == nums[i-1])
{
continue;
}
if(nums[i] > 0)
{
return resault;
}
int start = i+1;
int end = nums.size()-1;
while(start < end )
{
if(nums[start]+nums[end] > 0- nums[i])
{
end--;
}
else if(nums[start]+nums[end] < 0-nums[i] )
{
start++;
}
else
{
resault.push_back({nums[i],nums[start],nums[end]});
int s = nums[start];
while(nums[start] ==s && start < nums.size()-1)
{
start++;
}
s = nums[end];
while(nums[end] ==s && end > start)
{
end--;
}
}
}
}
return resault;
}
};
概念
窗口代表着数组或者序列上的一组元素,用 left 和 right 两个指针分别表示其左右边界,从左到右移动 left 和 right 指针,就像是一个窗口在序列上进行滑动,不断的有新元素从右侧进入窗口,同时有旧的元素从左侧移除窗口,当前窗口内的元素是否满足需求制约着窗口的扩张与收缩。
滑动窗口可以用来解决子串问题。
滑动窗口基本原理
⑴.窗口初始化
设置窗口初始化值为 0 ,即 left 和 right 都位于序列的左端位置。
⑵.窗口扩张
窗口扩张时 left 指针保持不动,向右移动 right 指针;在窗口扩张的初期,窗口右侧纳入新的元素对结果的影响时积极的,有以下两种情况:
①.新加入的元素使窗口得到的结果更优,则每一步都是一个当前最优解;停止扩张的临界条件是新加入的元素刚好使窗口不符合条件。
②.新加入的元素使窗口离达到目标条件更近;停止扩展的临界条件是新加入的元素刚好使窗口符合目标条件。
当窗口达到停止扩张的临界条件时,继续扩张对结果的影响时消极的,此时应该进行窗口收缩。
⑶.窗口收缩
窗口收缩时 right 指针保持不动,向右移动 left 指针;窗口收缩的初期,窗口左侧丢弃的元素对结果的影响的积极的,有以下两种情况:
①.若窗口停止扩张的临界条件是新加入的元素刚好使窗口不符合目标条件,则丢弃一个旧元素可能使窗口满足条件,停止收缩的临界条件是丢弃一个元素使窗口刚好满足目标条件。
②.若窗口停止扩张的临界条件是新加入的元素刚好使窗口符合目标条件,则丢弃一个旧元素可能使窗口的结果更优,停止收缩的临界条件是丢弃一个元素使窗口刚好不满足目标条件。
当窗口达到停止收缩的临界条件时,继续收缩对结果的影响时消极的,此时应该进行窗口扩张。
④.滑动停止
滑动窗口的过程就是不断的进行窗口扩张与收缩,当窗口扩张对结果的影响使积极时就进行窗口扩张,当窗口收缩对结果的影响时积极时就进行窗口收缩,不断重复这个过程。当right 指针到达序列末尾时,窗口滑动停止。
滑动窗口的应用
⑴.76. 最小覆盖子串
给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串。
示例:
输入: S = “DAOBECODEBANC”, T = “ABC”
输出: “BANC”
说明:如果 S 中不存这样的子串,则返回空字符串 “”。
如果 S 中存在这样的子串,我们保证它是唯一的答案。来源:力扣(LeetCode)
①.题目分析
本题中窗口是否满足需求的条件是:窗口中的元素是否包含了目标中所有的字符种类以及对应的个数,本题每次窗口扩张可能达到一个可行解,然后进行窗口收缩优化这个可行解得到最优解,然后扩张窗口寻找下一个可行解。
②.代码示例
class Solution {
public:
string minWindow(string s, string t) {
//特殊的边界处理
if( s.length() < t.length()) return "";
if( s.length() == 0) return "";
if( t.length() == 0) return "";
unordered_map need_charmap;//需要寻找的 字符-个数 对应
for(int i = 0;i < t.length();i++)
{
need_charmap[t[i]]++;//初始化need_charmap
}
unordered_map cur_charmap;//已经找到的 字符-个数 对应
unordered_set cur_chartype;//已经找齐的字符种类
int minLeft = 0;//初始化目标窗口左指针为0
int minLenght = INT_MAX;//初始化满足需要的最小长度为一个绝对最大值,然后不断优化这个值
int left = 0 ,right = 0;//初始化窗口边界
while(right < s.length())//窗口滑动停止条件
{
char temp = s[right];//本次窗口扩张纳入的新元素
//更新窗口内元素
if( need_charmap.find(temp) != need_charmap.end() ) //新元素是目标字符
{
cur_charmap[temp] ++;//已经找到的字符个数+1
if( cur_charmap[temp] >= need_charmap[temp] )
{
cur_chartype.insert(temp);//标记该类字符已经找齐了
}
while( cur_chartype.size() == need_charmap.size())//所有字符种类已经找齐了
{
//记录这个可行解,判断是否最优
if( right-left+1 < minLenght)
{
minLeft = left;
minLenght = right-left+1;
}
temp = s[left];//本次窗口收缩要丢弃的元素
if( need_charmap.find(temp) != need_charmap.end() ) //是目标字符
{
cur_charmap[temp] --;
if( cur_charmap[temp] < need_charmap[temp] )//数量不够了
{
cur_chartype.erase(temp);//从找齐的结果中删除这个字符
}
}
left++;//收缩窗口,继续优化这个解
}
}
right++;//窗口继续扩张,寻找下一个可行解
}
return minLenght < INT_MAX ? s.substr(minLeft,minLenght) : "";
}
};
⑵.3. 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。来源:力扣(LeetCode)
①.题目分析
本题中窗口是否满足需求的条件是:窗口扩张纳入的新元素和已有元素不重复,本题每次窗口扩张可能都是一个当前最优解,当不符合条件时,收缩窗口使其符合条件,然后扩张窗口寻找下一个最优解。
②.代码示例
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//特殊的边界处理
if( s.length() == 0) return 0;
int res = 0;//初始化返回值
int left = 0,right = 0;//窗口初始化
unordered_map cur_map;
while( right < s.length())
{
char rightStr = s[right];//窗口扩张得到的新元素
cur_map[rightStr]++;//更新窗口内元素
if( cur_map[rightStr] == 1)
{
res = max(res,right-left+1);//记录最优解
}
else//刚好不满足需求时,收缩窗口
{
while( cur_map[rightStr] > 1 )
{
char leftChar = s[left];
cur_map[leftChar] --;
left++;
}
}
right++;
}
return res;
}
};