头条最热笔试题:利用滑动窗口来解决字符串匹配

项目介绍

  • 本项目通过分解各大厂的常见笔面试题,追本溯源至数据结构和算法的底层实现原理,知其然知其所以然;
  • 建立知识结构体系,方便查找,欢迎更多志同道合的朋友加入项目AlgorithmPractice(欢迎提issue和pull request)。

什么是滑动窗口

  • 在尽可能一次遍历中,在源串中找出目标串的匹配模式。

五道题目七种解法

  • 序列匹配
  • 最小覆盖子串
  • 字符串子串包含检测
  • 找所有字母异位词
  • 最长无重复子串

正文开始

1、序列匹配

  • 给定一个源字符串,再给一个目标字符串,看看源串中是否存在目标字符串的序列,比如:
    • 正案例
      • 源字符串:“1a2b3c4d”,
      • 目标字符串:“1234”
      • 很明显,源串中存在目标字符串的序列。
    • 反案例:
      • 源字符串:“1a2b3cd”,
      • 目标字符串:“1234”
      • 很明显,源串中不存在目标字符串的序列。
    • 解法:因为我们的目的很简单,你只需要告诉我,存不存在,true or false,因此通过一次循环,source指针指向源串的位置,target指针指向目标串的位置,双指针同步工作,最后判断target指针是否等于目标串的长度即可,当然,这道题如果改成两者最长公共部分的序列,需要使用LCS的解法来处理,本题完整源码:SequenceExist。除了这种方法外,还有一个叫做字符串预处理的方法:字符串预处理法解决字符串匹配问题
    • 主要代码:
while (sourcelength < source.length() && targetlength < target.length()){
    if(targetchar[targetlength] == sourcechar[sourcelength]){
        targetlength++;
    }
        sourcelength++;
}
return targetlength == target.length();

2、最小覆盖子串

  • 给定一个源字符串,再给一个目标字符串,看看源串中是否存在目标字符串的序列,特别的是:对于这个序列,我们允许它在源字符串中乱序存在。但是如果仅仅是存在性判断,那么一个list或者hashmap完全可以搞定,将目标字符串存入list中,遍历源字符串,从list中删除这个字符,什么时候list空了,return true,否则reture false。
  • 我们要求更进一步,假设存在多个目标字符串的字符存在于源字符串中,那么找出包含目标字符串中所有字符的最小区间,比如:
    • 正案例
      • 源字符串:“2a41b156d3c24d”,
      • 目标字符串:“1234”
      • 很明显,源串中乱序存在目标字符串的序列,但是这其中最小的覆盖子串的区间是“156d3c24”。
    • 反案例:
      • 源字符串:“1a2b3cd”,
      • 目标字符串:“1234”
      • 很明显,源串中根本不存在目标字符串的序列。
    • 解法:这道题需要从滑动窗口的角度来考虑,完整源码:MinimumWindowSubstring。
    • 主要代码:
//右侧窗口扩张
while (right < source.length()) {
    char cright = source.charAt(right);
    right++;
    if (targetMap.containsKey(cright)) {
        int num = windowsMap.containsKey(cright) ? windowsMap.get(cright) + 1 : 1;
        windowsMap.put(cright, num);
        if (windowsMap.get(cright) == targetMap.get(cright)) {
            count++;
        }
    }
    //判断左侧窗口是否进行收缩,注意此处是个循环,不是if
    while (count == targetMap.size()) {

        //更新数据
        if (right - left < subLength) {
            begin = left;
            subLength = right - left;
        }
        char cleft = source.charAt(left);
        left++;
        if (targetMap.containsKey(cleft)) {
            if (windowsMap.get(cleft) == targetMap.get(cleft)) {
                count--;
            }
            windowsMap.put(cleft, windowsMap.get(cleft) - 1);
        }
    }
}

3、字符串子串包含检测

  • 描述:对于给定一个源字符串和目标字符串,看看源串中是否存在目标字符串,与刚才的最小覆盖子串不同,这次是字符串,而不是序列了。比如:
    • 正案例
    • 源字符串:“cd63412asd”,
    • 目标字符串:“1234”
    • 很明显,源串中乱序存在目标字符串。
  • 反案例:
    • 源字符串:“123c4d”,
    • 目标字符串:“1234”
    • 很明显,源串中不存在目标字符连续的串。
  • 解法:这道题也是需要从滑动窗口的角度来考虑,无非是判断窗口与目标串是否一致,完整源码:PermutationinString。
  • 主要代码:
if (right - left == target.length()) {
    if (count == targetMap.size()) {
        return true;
    }
    char cleft = source.charAt(left);
    if(windowsMap.containsKey(cleft)){
        windowsMap.put(cleft, windowsMap.get(cleft) - 1);
        count--;
    }
    left++;
}

4、找所有字母异位词

  • 描述:对于给定一个源字符串和目标字符串,如果源串中存在多个目标字符串,那么返回他们的其实地址。比如:
    • 正案例
    • 源字符串:“cd63412as2314d”,
    • 目标字符串:“1234”
    • 很明显,源串中乱序存在目标字符串,其实位置分别是[3,9]。
  • 反案例:
    • 源字符串:“123c4d”,
    • 目标字符串:“1234”
    • 很明显,源串中不存在目标字符连续的串。
  • 解法:这道题也是需要从滑动窗口的角度来考虑,与字符串子串包含检测一样,无非是多个list来存储每一次符合的字符串起始地址,完整源码:FindallAnagrams。
  • 主要代码:
if (right - left + 1 == target.length()) {
    //符合条件,进行存入
    if (point == targetMap.size()) {
        count.add(left);
        //1号位
    }
    //缩小窗口
    char cleft = source.charAt(left);
    if(windowsMap.containsKey(cleft)){
        windowsMap.put(cleft, windowsMap.get(cleft) - 1);
        point--;//point--只能放在此处,不可以放在 1号位
    }
    left++;
}

5、最长无重复子串长度

  • 描述:给定一个字符串,找出最长的不重复的字串,比如:
    • 正案例
      • 源字符串:“aassddffgghh”,
      • 最长无重复子串长度:2
      • 很明显,任意的as,或者sd、df、fg、gh,长度都是2。
    • 解法:这道题有两种解法,桶方法,或者从滑动窗口的角度来考虑,桶方法和滑动窗口法的完整源码:LNRSubstring。
    • 桶方法主要代码:
for (int begin = 0, j = 0; j < s.length(); j++) {
    //找出子串起始位置,通过重复值来判断
    begin = Math.max(bottle[s.charAt(j)], begin);
    //更新记录值
    count = Math.max(count, j - begin + 1);
    //更新桶序号对应的子串起始位置
    bottle[s.charAt(j)] = j + 1;
}
  • 滑动窗口主要代码
while(right < s.length()){
    Character c = s.charAt(right);
    if(!windows.containsKey(c)){
        windows.put(c,right);
        count = windows.size() > count ? windows.size() : count;
    }else {
        while (left < right && windows.containsKey(c)){
            windows.remove(s.charAt(left));
            left++;
        }
        windows.put(c,right);
    }
    right++;
}

你可能感兴趣的:(数据结构和算法及其应用,字符串,数据结构,java,字节跳动,算法)