About 4 min


多个字符串的最长公共前缀

// 14. 最长公共前缀
class Solution {
    public String longestCommonPrefix(String[] strs) {
        char[][] cs = new char[strs.length][];
        int minLen = strs[0].length();
        for (int i = 0; i < strs.length; i++) {
            cs[i] = strs[i].toCharArray();
            minLen = Math.min(minLen, cs[i].length);
        }
        char[] result = new char[minLen];
        int resultIdx = 0;
        for(int i = 0; i< minLen; i++){
            char c = cs[0][i];
            int j = 1;
            for(; j < cs.length; j++){
                if(c != cs[j][i]){
                    break;
                }
            }
            if(j == cs.length){
                result[resultIdx++] = c;
            }else{
                break;
            }
        }
        return new String(result, 0, resultIdx);
    }
}

匹配子串

暴力匹配

手机暴力匹配,效果等同调用 indexOf 函数

class Solution {
    public int strStr(String haystack, String needle) {
        return haystack.indexOf(needle);
    }
}
// 28. 找出字符串中第一个匹配项的下标
class Solution {
    public int strStr(String haystack, String needle) {
        char[] c1 = haystack.toCharArray();
        char[] c2 = needle.toCharArray();

        int i = 0, j = 0;
        while (i < c1.length && j < c2.length) {
            if (c1[i] != c2[j]) {   
                i = i - j + 1;      // 回退j个的下一个。不是直接i++
                j = 0;              // 清零
            } else {
                i++;
                j++;
            }
        }
        if (j == c2.length) {
            return i - c2.length;
        } else {
            return -1;
        }
    }
}

KMP

模式串自身的前后缀(不包含自身)匹配。

lps: 下标i处的值表示,i+1个字符的最长公共前缀的个数。

比如,j及其前面有3个已匹配的字符,而3个已匹配的字符的最长公共前缀的个数,对应lps中就是下标2的值。

前进:lps[i] = j + 1,回退:j = lps[j-1]

alt text

i和j犬牙交错,lps和i同步:

  • 第一个字符必然没有前后缀。
  • i从1开始,j从0开始。

遍历i,回退是j动i不动

匹配

i和j同步,都从0开始。

// 28. 找出字符串中第一个匹配项的下标
class Solution {
    public int strStr(String haystack, String needle) {
        char[] origin = haystack.toCharArray();
        char[] pattern = needle.toCharArray();
        int[] lps = new int[pattern.length];

        int i = 1, j = 0;
        while (i < pattern.length) {
            if (pattern[i] == pattern[j]) {
                lps[i] = j + 1;
                i++;
                j++;
            } else {
                if (j == 0) {   // j已经是0了,不能再退,直接i++
                    i++;
                } else {
                    j = lps[j - 1];
                }
            }
        }

        i = 0;
        j = 0;
        // while (pattern.length - j <= origin.length - i) {   // pattern剩余未匹配的要小于等于origin剩余未匹配的。没必要,ms都看不出来
        while (i < origin.length) {
            if (origin[i] == pattern[j]) {
                i++;
                j++;
            } else {
                if (j == 0) {
                    i++;
                } else {
                    j = lps[j - 1];
                }
            }
            // 匹配完所有模式串字符
            if (j == pattern.length) {
                return i - j;
            }
        }
        return -1;
    }
}

重复的子字符串

观察特性:

我们可以把字符串 s 写成 $s's'\cdots s'$ 的形式,$s'$就是重复子串。

如果我们移除字符串 s 的前n'个字符(一个完整的 $s'$),再将这些字符保持顺序添加到剩余字符串的末尾,那么得到的字符串仍然是 s。

那么如果将两个 s 连在一起(相当于s的头尾拼接了),并移除第一个和最后一个字符(不让它匹配首个和最后的重复字符串),那么得到的字符串一定包含 s,即s 是它的一个子串

最简单:

// 459. 重复的子字符串
class Solution {
    public boolean repeatedSubstringPattern(String s) {
        String str = s + s;
        // return str.indexOf(s, 1) != s.length();
        return str.substring(1, str.length() - 1).contains(s);
    }
}

用KMP来写 contains()

// 459. 重复的子字符串
class Solution {
    public boolean repeatedSubstringPattern(String s) {
        return kmp(s + s, s);
    }

    public boolean kmp(String s1, String s2) {
        char[] origin = s1.toCharArray();
        char[] pattern = s2.toCharArray();
        int[] lps = new int[pattern.length];

        int i = 1, j = 0;
        while(i<pattern.length){
            if(pattern[i] == pattern[j]){
                lps[i] = j + 1;
                i++;
                j++;
            } else{
                if(j == 0){
                    i++;
                }else{
                    j = lps[j - 1];
                }
            }
        }

        i = 1;      // 去掉头部
        j = 0;
        while(i < origin.length - 1){       // 去掉尾部
            if(origin[i] == pattern[j]){
                i++;
                j++;
            } else{
                if(j == 0){
                    i++;
                }else{
                    j = lps[j - 1];
                }
            }
            if(j == pattern.length){
                return true;
            }
        }
        return false;
    }
}

最长回文子串

检测方式:以下标i为圆心,向两边同步扩展。

两种回文:单个下标i开始,两个下标i和i+1开始。

alt text

// 5.最长回文子串
class Solution {
    int left;
    int right;

    public String longestPalindrome(String s) {
        char[] cs = s.toCharArray();
        for (int i = 0; i < s.length(); i++) {
            extend(cs, i, i);
            if (i <= s.length() - 1)
                extend(cs, i, i + 1);
        }
        return new String(cs, left, right - left + 1);
    }

    public void extend(char[] cs, int i, int j) {
        while (i >= 0 && j < cs.length && cs[i] == cs[j]) {
            i--;
            j++;
        }
        // while超了,还原
        i++;
        j--;
        if (j - i > right - left) {
            left = i;
            right = j;
        }
    }
}

最小覆盖子串

字典统计:确定覆盖子串

一旦覆盖:缩小i

// 76.最小覆盖子串
class Solution {
    boolean flag;
    int left;
    int right;

    public String minWindow(String s, String t) {
        char[] cs = s.toCharArray();
        char[] ct = t.toCharArray();

        // 字符字典,统计不同字符的个数
        int[] scount = new int[128];
        int[] tcount = new int[128];

        // 有几种字符是存在的
        int condition = 0;

        for (char c : ct) {
            tcount[c]++;
        }
        for (int tc : tcount) {
            if (tc > 0)
                condition++;
        }

        int i = 0;
        int tempCondition = 0;
        // 遍历 s 字符串
        for(int j = 0; j < cs.length; j++){
            char cj = cs[j];
            scount[cj]++;
            if (scount[cj] == tcount[cj]) {
                tempCondition++;
            }
            // 缩小i:条件满足且i不超过j
            while (condition == tempCondition && i <= j) {
                // 判断当前 [i...j] 部分
                // 第一次
                if (!flag) {
                    left = i;
                    right = j;
                    flag = true;
                } else if (j - i < right - left) {
                    left = i;
                    right = j;
                }
                // 更新i
                char ci = cs[i];
                scount[ci]--;
                if (scount[ci] < tcount[ci]) {
                    tempCondition--;
                }
                i++;
            }
        }
        return flag ? new String(cs, left, right - left + 1) : "";
    }
}