LeetCode分类专题(四)——双指针和滑动窗口1

iwehdio的博客园:https://www.cnblogs.com/iwehdio/

学习自:

本文包括力扣:76、567、438、3、26、27、283

1、双指针

  • 双指针技巧可以分为两类,一类是「快慢指针」,一类是「左右指针」。前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环;后者主要解决数组(或者字符串)中的问题,比如二分查找。

  • 快慢指针一般都初始化指向链表的头结点 head,前进时快指针 fast 在前,慢指针 slow 在后。

  • 判定链表中是否含有环:

    • 如果链表中不含环,那么这个指针最终会遇到空指针 null 表示链表到头了,这还好说,可以判断该链表不含环。但是如果链表中含有环,那么这个指针就会陷入死循环,因为环形数组中没有 null 指针作为尾部节点。
    • 经典解法就是用两个指针,一个每次前进两步,一个每次前进一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。
    • 而且相遇时,慢指针走的步数就是环的长度。

    image-20210222111234949

  • 已知链表中含有环,返回这个环的起始位置:

    image-20210222111221069

    • 当快慢指针相遇时,让其中任一个指针重新指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。

      • 第一次相遇时,假设慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步,也就是说比 slow 多走了 k 步(也就是环的长度)。

      image-20210222111305468

      • 相遇点距环的起点的距离为 m,那么环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。
      • 巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。

      image-20210222111328627

    • 所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后就会相遇,相遇之处就是环的起点了。

    image-20210222111413496

  • 寻找链表的中点:

    • 让快指针一次前进两步,慢指针一次前进一步,当快指针到达链表尽头时,慢指针就处于链表的中间位置。

    • 当链表的长度是奇数时,slow 恰巧停在中点位置;如果长度是偶数,slow 最终的位置是中间偏右。

      image-20210222111739715

      image-20210222111811475

    • 寻找链表中点的一个重要作用是对链表进行归并排序。

  • 寻找链表的倒数第 k 个元素:

    • 让快指针先走 k 步,然后快慢指针开始同速前进。这样当快指针走到链表末尾 null 时,慢指针所在的位置就是倒数第 k 个链表节点。

    image-20210222111957608

  • 26 删除排序数组中的重复项

    image-20210223102137553

    • 用快慢指针,首先判断快慢指针指向的元素是否重复,如果重复就向后移动快指针,如果不重复就向后移动慢指针并且把快指针指向的元素移动到慢指针。
    • 到最后[0,慢指针]就是不重复元素,数组长度就是慢指针+1。
    class Solution {
        public int removeDuplicates(int[] nums) {
            int lo=0, hi=0, offset=0;
            while(hi<nums.length) {
                if(nums[lo]!=nums[hi]) {
                    lo++;
                    nums[lo] = nums[hi];
                }
                hi++;
            }
            return ++lo;
        }
    }
    
  • 27 移除元素

    image-20210223104902812

    image-20210223104913119

    • 与上一题类似,只不过移动慢指针和交换值的顺序变了。因为第一个值必定不重复,但是可能等于目标值。
    • 体会一下,if中放的是移动慢指针的条件。if外放的是移动快指针的条件,这样也不需要额外判断快指针是否越界。
    class Solution {
        public int removeElement(int[] nums, int val) {
            int lo=0, hi=0;
            while(hi<nums.length) {
                if(nums[hi]!=val) {
                    nums[lo] = nums[hi];
                    lo++;
                }
                hi++;
            }
            return lo;
        }
    }
    
  • 283 移动零

    image-20210223110501716

    • 所谓的移动,其实就是删除所有0,然后再在尾部填充0。
    class Solution {
        public void moveZeroes(int[] nums) {
            int lo=0, hi=0;
            while(hi<nums.length) {
                if(nums[hi]!=0) {
                    nums[lo] = nums[hi];
                    lo++;
                }
                hi++;
            }
            for(int i=lo; i<nums.length; i++) {
                nums[i] = 0;
            }
        }
    }
    

2、滑动窗口

  • 滑动窗口实际上是左右指针的一个特例,主要逻辑就是从左侧增大窗口和从右侧缩小窗口:

    /* 滑动窗口算法框架 */
    void slidingWindow(string s, string t) {
        unordered_map<char, int> need, window;
        for (char c : t) need[c]++;
    
        int left = 0, right = 0;
        int valid = 0; 
        while (right < s.size()) {
            // c 是将移入窗口的字符
            char c = s[right];
            // 右移窗口
            right++;
            // 进行窗口内数据的一系列更新
            ...
    
            /*** debug 输出的位置 ***/
            printf("window: [%d, %d)
    ", left, right);
            /********************/
    
            // 判断左侧窗口是否要收缩
            while (window needs shrink) {
                // d 是将移出窗口的字符
                char d = s[left];
                // 左移窗口
                left++;
                // 进行窗口内数据的一系列更新
                ...
            }
        }
    }
    
    • 两个...处的操作分别是右移和左移窗口更新操作。
  • 76 最小覆盖子串

    image-20210222113656325

    • 首先先确定如何记录子串中的字符个数信息,一种方法是哈希表,但是因为只有字符,也可以用char来作为int数组的下标。
    • 然后,初始化目标子串的数组,记录每个字符的出现次数。用count来记录目标字符串的字符个数,用valid来记录当前窗口中可以覆盖目标字符的个数。(或者说count-valid是里覆盖还差几个字符)
    • 获取右指针处的元素,移动右指针,记录当前字符的出现次数加一。
      • 如果这个字符在目标字符串中出现过,并且在当前窗口中出现的次数(加一后)小于等于目标字符串中的出现次数,就valid+1。
      • 如此循环,直到valid==count,覆盖条件满足,开始准备移动左指针。
    • 当valid==count说明覆盖完成,首先记录当前的子串位置和长度。然后获取左指针处的元素,移动做之中,记录当前字符的出现次数减一。
      • 如果这个字符在目标字符串出现过,并且在当前窗口中出现的次数(减一后)小于目标字符串中出现的自出,就valid-1。
    class Solution {
        public String minWindow(String s, String t) {
            int[] need = new int[128], cur = new int[128];
            int count = t.length(), len = Integer.MAX_VALUE;
            for(int i=0; i<t.length(); i++) {
                need[t.charAt(i)]++;
            }
            int lo = 0, hi = 0, valid = 0, start = 0;
            while(hi<s.length()) {
                char a = s.charAt(hi);
                hi++;
                cur[a]++;
                if(need[a]>0 && need[a]>=cur[a]) {
                    valid++;
                } 
    
                while(valid==count) {
                    if(len>hi-lo) {
                        len = hi - lo;
                        start = lo;
                    }
                    char b = s.charAt(lo);
                    lo++;
                    cur[b]--;
                    if(need[b]>0 && need[b]>cur[b]) {
                        valid--;  
                    }
                }
            }
            return len==Integer.MAX_VALUE? "" : s.substring(start, start+len);
        }
    }
    
  • 567 字符串的排列

    image-20210222163031783

    • 跟之前的一样,只不过开始收缩区间的条件变为了区间大于子串s1的长度。

      class Solution {
          public boolean checkInclusion(String s1, String s2) {
              int[] obj = new int[128], cur = new int[128];
              int count = s1.length(), valid = 0;
              for(int i=0; i<count; i++) {
                  obj[s1.charAt(i)]++;
              }
              int lo = 0, hi = 0;
              while(hi<s2.length()) {
                  char a = s2.charAt(hi);
                  hi++;
                  cur[a]++;
                  if(obj[a]>0 && obj[a]>=cur[a]) {
                      valid++;
                  }
      
                  while(hi-lo>=count) {
                      if(valid==count) return true;
                      char b = s2.charAt(lo);
                      lo++;
                      cur[b]--;
                      if(obj[b]>0 && obj[b]>cur[b]) {
                          valid--;
                      }   
                  }
              }
              return false;
          }
      }
      
    • 但是其实如果出现了子串中没有的字符,直接重置效率更高。

      class Solution {
          public boolean checkInclusion(String s1, String s2) {
              int[] obj = new int[128], cur = new int[128];
              int count = s1.length();
              for(int i=0; i<count; i++) {
                  obj[s1.charAt(i)]++;
              }
              int lo = 0, hi = 0;
              while(hi<s2.length()) {
                  char a = s2.charAt(hi);
                  hi++;
                  cur[a]++;
                  if(obj[a]>0) {
      
                      while(obj[a]<cur[a]) {
                          char b = s2.charAt(lo);
                          lo++;
                          cur[b]--;
                      }
                  } else {
                      lo = hi;
                      for(int i=0; i<cur.length; i++) {
                          cur[i] = 0;
                      }
                  }
                  if(count==hi-lo) {
                      return true;
                  }
              }
              return false;
          }
      }
      
      
  • 438 找到字符串中所有字母异位词

    image-20210222174803716

    image-20210222174812279

    • 跟字符串的排列类似,只不过是每次都把结果保存到List中。
    class Solution {
        public List<Integer> findAnagrams(String s, String p) {
            List<Integer> ans = new ArrayList<>();
            int[] obj = new int[128];
            int[] cur = new int[128];
            for(int i=0; i<p.length(); i++) {
                obj[p.charAt(i)]++;
            }
            int count = p.length(), valid = 0;
            int lo = 0, hi = 0;
            while(hi<s.length()) {
                char a = s.charAt(hi);
                hi++;
                cur[a]++;
                if(obj[a]>0 && obj[a]>=cur[a]) {
                    valid++;
                }
    
                while(hi-lo>=count) {
                    if(valid==count) {
                        ans.add(lo);
                    }
                    char b = s.charAt(lo);
                    lo++;
                    cur[b]--;
                    if(obj[b]>0 && obj[b]>cur[b]) {
                        valid--;
                    }
                }
            }
            return ans;
        }
    }
    
  • 3 最长无重复子串

    image-20210223095634128

    • 用一个标志位来标记是否出现了重复元素,出现重复元素置为false,且为false时为缩小窗口的条件。
    • 置为false是只有一个元素是重复的,所以如果有一个元素移除一个后还有一个,就可以置为true了。
    • 完成增缩窗口后,判断窗口长度是否需要更新。
    class Solution {
        public int lengthOfLongestSubstring(String s) {
            int len = 0;
            int[] save = new int[128];
            int lo=0, hi=0;
            boolean flag = true;
            while(hi<s.length()) {
                char a = s.charAt(hi);
                hi++;
                save[a]++;
                if(save[a]>1) flag = false;
    
                while(!flag) {
                    char b = s.charAt(lo);
                    lo++;
                    save[b]--;
                    if(save[b]==1) flag = true;
                }
                if(len<hi-lo) len = hi-lo;
            }
            return len;
        }   
    }
    

iwehdio的博客园:https://www.cnblogs.com/iwehdio/
来源与结束于否定之否定。
原文地址:https://www.cnblogs.com/iwehdio/p/14434988.html