-
快慢指针(主要解决链表中的问题)
- 链表中是否包括环,进而求链表中环的入口节点
- 链表的中间节点(重要作用就是对链表进行归并排序)
- 链表的倒数第k个节点
- 等等
-
左右指针(主要解决数组(或者字符串)中的问题)
- 二分查找
- 求排序树组中和为sum的两个数
- 反转数组(字符串)
- 滑动窗口(双指针技巧的最高境界)
- 等等(代码就不写了,前面写过)
一 快慢指针常用算法
// 1、快慢指针判断链表是否包含环并且返回环的入口节点
ListNode* hasCycle(ListNode* pHead){
ListNode* fast = pHead;
ListNode* slow = pHead;
while(fast != nullptr && fast->next != nullptr){
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
break;
}
// 没有环则返回
if(fast == nullptr && fast->next == nullptr)
return nullptr;
slow = pHead;
while(slow != fast){
slow = slow->next;
fast = fast->next;
}
return fast;
}
// 2、链表的中间节点
ListNode* middleNode(ListNode* pHead){
if(pHead == nullptr)
return nullptr;
ListNode* fast = pHead;
ListNode* slow = pHead;
while(fast != nullptr && fast->next != nullptr){
fast = fast->next->next;
slow = slow->next;
}
// 有奇数个节点(fast->next == nullptr),slow刚好是中间节点,有偶数个节点(fast == nullptr),slow是中间偏右一个节点
return slow;
}
// 3、链表的倒数第k个节点(这里简化一下加入链表节点大于k个)
ListNode* lastOfKthNode(ListNode* pHead){
if(pHead == nullptr || k == 0)
return nullptr;
ListNode* fast = pHead;
ListNode* slow = pHead;
// 先让快指针走k个节点
while(k--)
fast = fast->next;
while(fast != nullptr){
slow = slow->next;
fast = fast->next;
}
return slow;
}
二 左右指针常用算法
滑动窗口
滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。
TCP中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为0时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个1字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。
滑动窗口模板
套模板,只需要思考以下四个问题:
1、当移动right
扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动left
缩小窗口?
3、当移动left
缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
void slidingWindow(string s, string t){
unordered_map<char, int> need, window;
for(char c : s)
need[c]++;
int left = 0, right = 0;
// 窗口中的合法字符数
int valid = 0;
// 左开右闭区间
while(right < s.size()){
// c是将要移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列操作更新
...
// 判断左侧窗口是否需要收缩
while(window need shrink){
// d是将要移除窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列操作更新
...
}
}
}
例题一 最小覆盖子串
讨论套模板的4个问题:
- 如果一个字符进入窗口,应该增加
window
计数器; - 当
valid
满足need
时应该收缩窗口; - 如果一个字符将移出窗口的时候,应该减少
window
计数器; - 应该在收缩窗口的时候更新最终结果。
string minWindow(string s, string t){
unordered_map<char, int> need, window;
for(char c : t)
// 相当于Java中的need.put(c, need.getOrDefault(c, 0) + 1)
need[c]++;
int left = 0, right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while(right < s.size()){
// c是将加入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
// need.count(c)返回d的个数,这里大于0就行
if(need.count(c)){
window[c]++;
if(window[c] == need[c])
valid++;
}
// 判断左侧窗口是否需要收缩
while(valid == need.size()){
// 在这里更新最小覆盖子串
if(right - left < len){
start = left;
len = right - left;
}
// d是将移除窗口的字符
char d = s[left];
// 右移
left++;
// 进行窗口内数据的一系列更新
// need.count(d)返回d的个数,这里大于0就行
if(need.count(d)){
// 注意这一句判断是否相等不能省略,因为有可能window[d] > need[d]
if(window[d] == need[d])
valid--;
window[d]--;
}
}
}
return len == INT_MAX ? "" : s.substr(start, len);
}
例题二 字符串排列
这个题其实就是上个题,只不过是需要连续的子串,即包含T串中的字符,但不包含其它的字符。
#include <unordered_map>
class Solution {
public:
bool checkInclusion(string t, string s){
unordered_map<char, int> need, window;
for(char c : t)
need[c]++;
int left = 0, right = 0;
int valid = 0;
while(right < s.size()){
char c = s[right];
right++;
// 进行窗口内数据的一系列更新
if(need.count(c)){
window[c]++;
if(window[c] == need[c])
valid++;
}
// 这个条件就确定了连续的子串(这个地方写成if就行了,但是为了和模板一致,所以写成while)
while(right - left == t.size()){
// 在这里判断是否找到了合适的子串
if(valid == need.size())
return true;
char d = s[left];
left++;
// 进行窗口的一系列更新
if(need.count(d)){
if(window[d] == need[d])
valid--;
window[d]--;
}
}
}
return false;
}
};
例题三:找出所有字母异位词
和上一个题目一模一样,只不过需要返回所有的索引。
vector<int> findAnagrams(string s, string t){
unordered_map<char, int> need, window;
for(char c : t)
need[c]++;
int left = 0, right = 0;
int valid = 0;
vector<int> ans;
while(right < s.size()){
char c = s[right];
right++;
// 进行窗口内数据的一系列操作更新
if(need.count(c)){
window[c]++;
if(window[c] == need[c])
valid++;
}
// 判断左窗口是否需要收缩(这个地方写成if就行了,但是为了和模板一致,所以写成while)
while(right - left == t.size()){
if(valid == need.size())
ans.push_back(left);
char d = s[left];
left++;
// 进行窗口内数据的一系列操作更新
if(need.count(d)){
if(window[d] == need[d])
valid--;
window[d]--;
}
}
}
return ans;
}
例题四:最长无重复子串
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> window;
int left = 0, right = 0;
int res = 0;
while(right < s.size()){
char c = s[right];
right++;
window[c]++;
// 说明有重复字符,则需要移动窗口
while(window[c] > 1){
char d = s[left];
left++;
window[d]--;
}
// 在这里更新答案(right - left中已无重复字符,即窗口中无重复字符)
res = max(res, right - left);
}
return res;
}
};