LeetCode刷题笔记(2):双指针

双指针是用两个指针去遍历数组,协同完成检索任务。一般是利用两个指向元素的关系,决定之后指针的移动操作,找到目标或完成任务,如

当两个指针指向同一数组,并且同向移动时,可以形成滑动窗口,快慢指针等;

当两个指针指向同一数组,并且反向移动时,可以对有序数组形成检索。

1. 两数之和 II - 输入有序数组(167)

使用反向指针,检索数组。

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
      int p1=0,p2=numbers.size()-1;
      while(p1!=p2){
        if(numbers[p1]+numbers[p2]<target){
          p1++;
        }
        else if(numbers[p1]+numbers[p2]>target){
          p2--;
        }
        else{
          break;
        }
      }
      // return{p1+1,p2+1};
      vector<int> ans;
      ans.push_back(p1+1);
      ans.push_back(p2+1);
      return ans;

    }
};

语法:可以直接return{p1+1,p2+1}; 这是C++11新特性:“返回用大括弧初始化的该函数对象(返回的类型和函数类型一样)。即返回用p1+1和p2+1初始化后的twosum对象。

2. 合并两个有序数组(88)

用双指针合并两个有序数组是常见用法,但是本题要求不能使用额外空间,而是直接合并到数组1中。如果从nums1和nums2的头部开始比较,则需要移动nums1的后面元素。因此我们需要从尾部开始比较,把确定位置的元素插入nums1的尾部空闲部分。

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
      int p1 = m-1,p2 = n-1,p3 = m+n-1;
      while(p1 >= 0 &&p2 >= 0){
        if(nums1[p1] < nums2[p2]){
          nums1[p3--] = nums2[p2];
          p2--;
        }
        else{
          nums1[p3--] = nums1[p1];
          p1--;
        }
      }
      // 若nums2中还有剩余元素
      if(p2>=0){
        while(p2>=0){
          nums1[p3--] = nums2[p2];
          p2--;
        }
      }
    }
};

3. 环形链表 II(142)

快慢指针可以用于判断链表中是否存在环。让快指针每次走两个结点,慢指针每次走一个结点。如果存在环,则进入环后,快指针最终将追上慢指针。

本题还要求判断入环结点的位置,这需要一定的数学推导。首先,慢指针在进入环的第一圈就会被追上,因为两个指针距离范围为0-S-1(设环长为S),每走一次快指针靠近慢指针一步,所以在S步内就会被追上。注意:由于每次是接近一步,所以只会恰好相遇,而不会出现快指针经过慢指针的情况。

如下图所示,设链表中环外部分的长度为 a。slow 指针进入环后,又走了 b 的距离与 fast 相遇。此时,fast 指针已经走完了环的 nn 圈,因此它走过的总距离为 a+n(b+c)+b = a+(n+1)b+nc。

根据题意,任意时刻,\textit{fast}fast 指针走过的距离都为 \textit{slow}slow 指针的 22 倍。因此,我们有
        a+(n+1)b+nc=2(a+b)        ⟹        a=c+(n−1)(b+c)

现在把fast移动回起点,让fast和slow每次都移动一格。则fast走过a距离时到达入环结点,slow则位于 b+a = n(b+c),正好也回到入环结点。即第二次在入环结点处相遇。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *fast =  head,*slow = head;
        do{
          // 出现空指针,说明遍历到头,没有环
          if(fast==nullptr || fast->next==nullptr) return nullptr;
          fast = fast->next->next;
          slow = slow->next;
        }while(fast!=slow);
        fast = head;
        // 第二次相遇处,即为入环的第一个结点
        while(fast!=slow){
          fast = fast->next;
          slow = slow->next;
        }
        return fast;
    }
};

方法二:利用哈希表,当出现某个结点被第二次访问时,说明存在环,且该结点就是入环结点。

用unordered_set存储指针,当指针已存在说明是环。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
      // unordered_set是只去重不进行内部排序的set
        unordered_set<ListNode *> visited;
        while (head != nullptr) {
          // count()函数时查找集合中是否存在该元素
            if (visited.count(head)) {
                return head;
            }
            visited.insert(head);
            head = head->next;
        }
        return nullptr;
    }
};

除了判断环,快慢指针还可以用来寻找链表中倒数第n个结点(让快指针先走n步)。

4. 最小覆盖子串(76)

本题要从字符串s中找到包含字符串t所有字符的最小字串,可以用双指针形成滑动窗口检索,具体思路如下:

(1)对s长度小于t长度,直接返回“”;

(2)遍历t,使用map统计各个字符的出现次数;

(3)遍历s,先找到一个包含t的字串,方法是:固定p1在s[0],先移动p2,碰到t中字符,就使其对应map次数-1,直到map所有字符次数<=0,就找到了一个包含t的字串;如果找不到,返回"";

(4)确定最小字串,使p1前移,只要p1经过的字符没在t中,或者滑动窗口该字符个数有多,就可以前移,缩小滑动窗口左边,之后更新子串长度;p2继续右移,直到抵达一个t中有的字符,并更新map。循环上述过程,直到p2到达s末端,就会找到所有的子串,并比较出最短子串。

class Solution {
public:
    unordered_map<char,int> mp;
     bool isFind(){
      for(auto it=mp.begin();it!=mp.end();it++){
        if(it->second>0) return false;
      }
      return true;
    }
    string minWindow(string s, string t) {
      // s长度小于t长度直接返回""
      int slen = s.length(),tlen = t.length();
      if(slen<tlen){
        return "";
      }
      // 统计t中字符数量
      
      for(int i=0;i<tlen;i++){
        auto it = mp.find(t[i]);
        if(it!=mp.end()){
          mp[t[i]]++;
        }else{
          mp[t[i]]=1;
        }
      }
      // 先找到一个从s[0]开始的包含t的字串
      int p1=0, p2=0,subStr = slen;
      bool flag = false;
      while(p2<slen){
        auto it = mp.find(s[p2]);
        if(it!=mp.end()){
          mp[s[p2]]--;
        }
        if(isFind()){//找到包含t的子串 
          flag =true;
          break;
        }
        ++p2;
      }
      if(flag == false) return "";
      // 寻找最小字串
      int idx1=p1,idx2=p2;//记录下标
      while(p2<slen){
        // 先前移p1,缩小滑动窗口的范围
        while(p1<=p2){
          auto it = mp.find(s[p1]);
          if(it != mp.end()){
            // p1再往前移,就会有元素没有被包含到
            if(it->second==0) break;
            else ++(it->second);
          }
          p1++;
        }
        //更新子串长度
        if(p2-p1+1 < subStr){
          subStr = p2-p1+1;
          idx1 = p1;
          idx2 = p2;
        }
        // p2前移继续寻找
        while(p2<slen){
          auto it = mp.find(s[++p2]);
          if(it != mp.end()){
            --(it->second);
            break;
          }
        }        
      }
      string ans;
      for(int i=idx1;i<=idx2;i++){
        ans+=s[i];
      }
      return ans;
      //return s.substr(idx1,subStr);
    }
};

语法:(1)使用unordered_map比map更快,因为它不需要排序。unordered_map是用哈希表实现的,map是用红黑树实现的;

(2)返回子串可以用substr(pos,len)函数,pos为开始位置,len为子串长度。

本题的映射只需要在字符和int间建立,使用map的话大量find很不方便,实际上可以自己建立数组用于哈希,操作起来更方便。

class Solution {
public:
    string minWindow(string s, string t) {
        vector<int> need(128,0);
        int count = 0;  
        for(char c : t)
        {
            need[c]++;
        }
        count = t.length();
        int l=0, r=0, start=0, size = INT_MAX;
        while(r<s.length())
        {
            char c = s[r];
            if(need[c]>0)
                count--;
            need[c]--;  //先把右边的字符加入窗口
            if(count==0)    //窗口中已经包含所需的全部字符
            {
                while(l<r && need[s[l]]<0) //缩减窗口
                {
                    need[s[l++]]++;
                }   //此时窗口符合要求
                if(r-l+1 < size)    //更新答案
                {
                    size = r-l+1;
                    start = l;
                }
                need[s[l]]++;   //左边界右移之前需要释放need[s[l]]
                l++;
                count++;
            }
            r++;
        }
        return size==INT_MAX ? "" : s.substr(start, size);
    }
};

语法:(1)INT_MAX、INT_MIN表示整型的最大、最小整数;

(2)for(char c : str)是C++11借鉴Python的一种遍历用法,即让c等于字符串的每个字符进行遍历操作;其它容器也可做类似操作,如对double类型数组a,可以这样遍历for(double x:a),x逐一等于a的每一个元素。

5. 平方数之和(633)

判断C是否能表示为两个整数的平方和,类似于两数之和,双指针反向寻找:

(1)先确定范围,[0,(int)sqrt(c)];

(2)令p1,p2分别等于0和(int)sqrt(c),根据p1和P2平方和进行移动。

class Solution {
public:
    bool judgeSquareSum(int c) {
      int p1 = 0, p2 = int(pow(c,0.5));
      while(p1<=p2){
        int sum = pow(p1,2)+pow(p2,2);
        if(sum == c) return true;
        else if(sum < c) --p2;
        else ++p1;
      }
      return false;
    }
};

注意:sum在c很大时,可能会超出Int界限,所以改用了long long。在官方解答中使用了long类型。int 和 long的区别如下:

int long longlong在不同平台上长度可能不一致,但必须遵循:int不少于16位(2字节),Long不少于32位,long long不少于64位,且sizeof(int) <= sizeof(long) <= sizeof(long long)。在某些平台上,int和long可能都是4字节,32位,这时它们是一样的。

6. 验证回文字符串 Ⅱ(680)

只删除一个元素情况下,能否使字符串变成回文串。同样使用双指针检验,当出现不一致时,考察删除p1或p2指向元素,只要有一种情况下能子串是回文串即可。

class Solution {
public:
    //判断字串是否是回文字符串
    bool check(string s,int i,int j){     
      while(i < j){
        if(s[i] == s[j]){
          ++i;
          --j;
        }
        else return false;
      }
      return true;
    }
    
    bool validPalindrome(string s) {
      int p1 = 0, p2 = s.length()-1;
      bool flag = false;
      while(p1 <= p2){
        if(s[p1] == s[p2]){
          ++p1;
          --p2;
        }
        else {
          if(check(s,p1+1,p2)||check(s,p1,p2-1)) return true;
          else return false;
        }
      }
      return true;
    }
};

开始我犯了一个错误,认为当出现不一致时,只要s[p1+1] == s[p2],就一定是按照删除p1。但碰到如:string a = "cuppucu";的情况,只有删除末尾的u才能成立。因此必须严格地对两种删除情况都做检查,只要有一个成立就行。

7. 通过删除字母匹配到字典里最长单词(524)

(1)先用双指针判断单词是否是s的子序列:如果比较后,单词字符串被遍历完,则说明是子序列;

(2)更新子序列;

预先对字典进行排序,可能可以更快一点,

class Solution {
public:
    
    string findLongestWord(string s, vector<string>& dictionary) {
     
      string ans="";
      for(string str:dictionary){
        int p1=0,p2=0;
        while(p1<s.length()&&p2<str.length()){
          if(s[p1]==str[p2]) {
            ++p1;
            ++p2;
          }
          else{
            ++p1;
          }
        }
        // 本单词不是s子串
        if(p2<str.length()) continue;
        if(str.length()>ans.length() ||(str.length()==ans.length() && str<ans)) ans = str;
        
      }
      return ans;
    }
};

Python版

class Solution:
  # ->返回注释,说明返回的类型;self代表当前对象的地址。self能避免非限定调用造成的全局变量
    def findLongestWord(self, s: str, dictionary: List[str]) -> str:
      ans = ""
      for t in dictionary:
        i=j=0
        while i<len(s) and j<len(t):
          if s[i]==t[j]:
            j+=1
          i+=1
        
        # t被遍历完,说明是s的子序列
        if j==len(t):
         if len(t)>len(ans) or len(t)==len(ans) and t<ans:
           ans = t
      return ans

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值