LeetCode算法 双指针详解

93 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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;
     }
 };