开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第20天,点击查看活动详情
双指针
- 与滑动窗口类似
- 链表中的双指针:快慢指针、循环检测
滑动窗口与双指针
双指针,本质上就是维护一个窗口。因此,双指针,经常贯穿在数组、字符串与窗口有关的题目中。总体的思想框架:
-
L,R一起指向数组的开始 -
L指向开始,R指向数组最后一个元素if (expression_1) { ++L; } else if(expression_2) { --R; } else { // 找到问题的解 }
盛水最多的容器
双指针问题,比较难的是找到移动指针的条件。即 expression 具体是什么?
对于这道题,暴力解法就是对于每个位置i,遍历他能盛最多的水,就能得到最后的最大面积。这时候i,j都是从左边开始的。双指针一个指向头部,一个指向尾部。根据木桶理论,要是使得水盛的多,要使最短的木板比较高才行。因此每次移动指针选择高度低的,向对方的方向移动。每次移动都计算一次最大值。
class Solution {
public:
int maxArea(std::vector<int>& height) {
int size = height.size();
int L=0, R=size-1;
int maxArea_ =0;
while(L < R) {
int area = std::min(height[L], height[R]) * (R - L);
maxArea_ = std::max(area, maxArea_);
if(height[L] < height[R])
{
++L;
}
else
{
--R;
}
}
return maxArea_;
}
};
外观数列
class Solution {
public:
string countAndSay(int n) {
std::string str("1",1);
while(--n)
{
__countAndSay(str);
}
return str;
}
private:
void __countAndSay(std::string& str) {
int L =0, R=0;
std::string newStr;
int length = str.length();
while(R < length) {
if(str[L] != str[R])
{
newStr.push_back(R - L + '0');
newStr.push_back(str[L]);
L = R;
}
if(R == length-1 && str[L] == str[R])
{
newStr.push_back(length - L + '0');
newStr.push_back(str[L]);
break;
}
if(str[L] == str[R]) {
++R;
}
}
str.swap(newStr);
}
};
快慢双指针之循环检测
经常把一些问题转换为是否存在环。也叫佛洛依德的龟兔问题。
检测环的入口
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(!head)return nullptr;
ListNode* fast_ptr = head;
ListNode* slow_ptr = head;
// 第一步证明有环
while(fast_ptr->next && fast_ptr->next->next){
fast_ptr = fast_ptr->next->next; // 前进两步
slow_ptr = slow_ptr->next; //前进一步
if (slow_ptr == fast_ptr) //相遇处
break;
}
// 如果发生了,说明不是 break 跳出循环,即没有环
if(!(fast_ptr->next && fast_ptr->next->next))
return nullptr;
// 有环后
while(head != fast_ptr){
// fast_ptr 和 head 以同步的速度前进
head = head->next;
fast_ptr = fast_ptr->next;
}
return head;
}
};
快乐数
关键点:如果是快乐数就不会重复,最终会等于1。不是快乐数,那么最终就会 循环 。针对检测是否包含重复元素:
- 哈希表
- 双指针:弗洛伊德循环查找算法,俗称 “龟兔赛跑”
hash表
bool isHappy(int n) {
if(n==1) return true;
std::unordered_set<int> set_;
int64_t sum=0; // 防止溢出。加大范围
int64_t num = static_cast<int64_t>(n);
while(true) {
while(num) {
sum += pow(num%10, 2);
num /=10;
}
if(sum ==1) return true;
if(set_.find(sum) != set_.end()) return false;
set_.insert(sum);
std::swap(sum, num);
}
return false; // 编译需要
}
快慢指针解法
这里没有链表结构,但可把每次计算的平方和当作获取链表上的一个节点,得到的链是一个隐式的链表。
- 如果 n 是一个快乐数,即没有循环,那么快指针最终会比慢指针先到达数字 1。
- 如果 n 不是一个快乐的数字,那么最终快跑者和慢跑者将在同一个数字上相遇。 代码参考
class Solution {
public:
bool isHappy(int n) {
if(n==1) return true;
int slow = n;
int fast = getNext(n);
while(fast !=1 && slow != fast) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}
return fast ==1;
}
private:
int getNext(int num) {
int sum=0;
while(num) {
sum += ::pow(num%10, 2);
num /=10;
}
return sum;
}
};
寻找重复数
其他使用到双指针问题
重排链表
对于链表的这个问题题解,比较值得借鉴的一点是将原来的链表分为两个链表,反转后半部分的链表。
class Solution {
public:
void reorderList(ListNode* head) {
if(head==nullptr || head->next==nullptr)
return;
ListNode* fast =head->next;
ListNode* slow =head;
while(fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
}
ListNode* later = slow->next; // 后半部分的链表头节点
slow->next = nullptr; // 断开与后半部分连接
M_reverseList(later); // 反转后半部分链表
ListNode* front = head;
while(front && later) {
ListNode* laterNext = later->next;
ListNode* frontNext = front->next;
front->next = later;
later->next = frontNext;
front = frontNext;
later = laterNext;
}
}
void M_reverseList(ListNode*& head) {
ListNode* dummy = nullptr;
while(head) {
ListNode* next = head->next;
head->next = dummy;
dummy = head;
head = next;
}
head = dummy;
}
};