这里聊一聊解一类问题,就是满足某一条件的substring最值问题。
最开始我们以Minimum Window Substring为例,并整理总结leetcode里所有类似题目的通解。

Given a string S and a string T, find the minimum window in S which will contain all the characters in T in complexity O(n).
S = "ADOBECODEBANC" , T = "ABC", Minimum window is "BANC".

我们要把问题拆解成三个字问题:唯一的表示substring, 检查substring是否符合要求,记录最值。
1.
如何唯一的表示一个substring, 有两种方法,一种用头尾指针,一种用尾指针加substring长度。
2.
检查substring是否符合要求,这道题目要求包含T中的每个字母,所以我们需要一个counter来记录T里每个character出现的次数,空间O(26) O(128) O(256) 都行, 可以和面试官讨论。类似题目唯一变化的就是这个部分,可以是每个字符只能出现一次,最多出现两个不同字符,每个字符最多出现K次等等。
3.
记录最值。根据扫描方法的不同,长度有时为r-l,此时r不包含在substring里,有时为r-l+1,此时包含在substring里。

这篇博客主要讲第二个子问题,如何判断substring是否符合要求。以Minimum Window Substring为例。

public class Solution {
    public String minWindow(String s, String t) {
        // empty check
        if(s == null || t == null || s.length() == 0 || t.length() == 0) return "";
        // initial counter for valid check
        int[] counter = new int[128];
        for(char c : t.toCharArray()){
            counter[c]++;
        }
        // variables help represent substring and min/max
        char[] chs = s.toCharArray();
        // 变量名解释,i表示头即先开始扫描的指针,j表示尾。i,j对counter的操作相反,一个加一个减或者一个减一个加。 
        // 上面的counter记录每个字符出现次数,count表示substring里满足条件的字符出现总次数(可以从0加到count,也可以从count减少到0.
        int j = 0, i=0, minLen = Integer.MAX_VALUE, start = 0, count = t.length();
        
        //这里可以使用for和while两种循环,个人偏向for, 因为i指针要一次走一个字符遍历完整个string.
        for( ; i < chs.length; i++){      //逐个扫描
            // 我们包含了第i个元素,并且要立即判断是否满足条件。使用--x, 而不是x--.
            if(--counter[chs[i]] > -1) {
                count--;
            }
            // 因为我们需要的是最值,中间值我们不在乎,所以一次收敛到最小。
            while(count == 0) {     // 快速收紧
                if(i -j + 1< minLen) {
                    minLen = i - j + 1;
                    start = j;
                }
                // 用 == 0 判断是因为有的substring里个别字符可能偏多,多余的字符不会算作满足条件。下方有简单的例子。 
                // 一次只移动一个char, 所以这里用 == 0
                if(counter[chs[j++]]++ == 0) {
                    count++;
                }
            }
        }
        
        return minLen == Integer.MAX_VALUE ? "" : new String(chs, start, minLen);
    }
}

X表示占位符,不是字母X。(使用空格会被自动消除,所以这里用了X)
S: ADOBECODEBANC T: ABC
i: ADOBECXXXXXXXX
j: XDOBECXXXXXXX
i: XDOBECODEBAXX
j: XXXXXXODEBAXX
注意这一步j最后落在了C后面,而不是B的后面。因为B在T里只出现了一次, i在扫描的时候会减小两次,counter[B] = -1, counter[c] = 0
i: XXXXXXODEBANC
j: XXXXXXXXXBANC

将S,T里字符换成String,变成Leetcode 30, Substring with Concatenation of All Words.
You are given a string, s, and a list of words, words, that are all of the same length. Find all starting indices of substring(s) in s that is a concatenation of each word in words exactly once and without any intervening characters.
s: "barfoothefoobarman", words: ["foo", "bar"], You should return the indices: [0,9]

字符长度为K,我们可能从0, 1, 2, K-1开始组成不同字符, 从K开始会和从O开始重合了。代码如下,不过多解释,可以直接跳过。

public class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> res = new ArrayList<Integer>();
        int N = s.length(), M = words.length, K = words[0].length();
        // check input
        if(s == null || K == 0 || N < M*K) return res;
        Map<String, Integer> map = new HashMap<>(), curMap = new HashMap<>();
        // initial counter
        for(String word: words) {
            map.put(word, map.getOrDefault(word,0) + 1);
        }
        // str denote first word, temp denote last word
        String str = null, temp = null;
        for(int i=0; i< K; i++) { 
            int count = 0;
            // two poniter scan, each time move K steps
            for(int l=i, r=i; r+K <= N; r += K) {
                str = s.substring(r, r+K);
                if(map.containsKey(str)) {
                    curMap.put(str, curMap.getOrDefault(str,0) + 1);
                    
                    if(curMap.get(str) <= map.get(str)) count++;
                    // has a word above the counter of it
                    while(curMap.get(str) > map.get(str)) {
                        temp = s.substring(l, l+K);
                        curMap.put(temp, curMap.get(temp) - 1);
                        l += K;
                        if(curMap.get(temp) < map.get(temp)) count--;
                    }
                    // should be exactly M, when meet, delete last word
                    if(count == M){
                        res.add(l);
                        temp = s.substring(l, l+K);
                        curMap.put(temp, curMap.get(temp) - 1);
                        l += K;
                        count--;
                    }
                } else {
                    curMap.clear();
                    count = 0;
                    l = r + K;
                }
            }
            curMap.clear();
        }

        return res;
    }
}

下面来三个longest substring.
LC3 Longest Substring Without Repeating Characters
需要查重并且记录上次出现的位置,选择map.
以abcdba为例,i走到b, 用map做检查,发现出现过,把j移到map.get(b)的下一个。
i走到a, 用map做检查,发现出现过,把j移到map.get(a)的下一个。发现不对,因为下一个字符是b已经出现过两次,所以需要一个比较,取map.get(b),map.get(a)的最大值。

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        // empty check
        if(s == null || s.length() == 0) return 0;
        // <char, last appear pos>
        Map<Character, Integer> map = new HashMap<>();
        // scan whole string and calculte
        int len = 0;
        char[] chs = s.toCharArray();
        for(int i=0, j=0; i<chs.length; i++) {
            if(map.containsKey(chs[i])) {
                j = Math.max(j, map.get(chs[i])+1);  // abcdba, need to compare
            }
            map.put(chs[i], i);
            if(i-j+1 > len) {
                len = i-j+1;
            }
        }
        return len;
    }
}

LC340. Longest Substring with At Most K Distinct Characters

Given a string, find the length of the longest substring T that contains at most 2 distinct characters.
For example, Given s = “eceba”,
T is "ece" which its length is 3.

counter记录每个字符出现次数。 count表示有多少个不同字符,题目要求最多出现k个。

public class Solution {
    public int lengthOfLongestSubstringKDistinct(String s, int k) {
        // corner case
        if(s== null || s.length() == 0)  return 0;
        // counter, count denote k
        int[] counter = new int[128];
        // l denote tail, r denote head, two pointer scan
        int j = 0, len = 0, count = 0;
        for(int i=0; i<s.length(); i++) {
            // only denote first appearence
            if(counter[s.charAt(i)]++ == 0) {
                count++;
            }
            // at most k, // very important part
            if(count > k) { 
                while( --counter[s.charAt(j++)] > 0);   
                count--;
            }
            if(i - j + 1 > len) {
                len = i - j +1;
            }
        }
        // return max
        return len;
    }
}

LC159. Longest Substring with At Most Two Distinct Characters

Given a string, find the length of the longest substring T that contains at most 2 distinct characters.
For example, Given s = “eceba”,
T is "ece" which its length is 3.

是上个题目的简易版,或者特殊版。只有两个字符,我们就不需要一个数组了,只需要两个位置标记就行。p1, p2. 为了让计算最值方便,让p1始终小于p2.

这里需要注意的就是p1,p2在repeating characters出现的时候,同时会指向这个字符串的最后一个。
在判断第三个字符是否出现的时候,首先要比较p1 != p2.

public class Solution {
    public int lengthOfLongestSubstringTwoDistinct(String s) {
        if(s.isEmpty()) return 0;
        int p1 = 0, p2 = 0, max = 1, len = 1; 
        char[] chs = s.toCharArray();
        for(int i=1; i<chs.length; i++) {
            if(p1 != p2 && chs[i] != chs[p1] && chs[i] != chs[p2]) {
                if(len > max) max = len;
                len = i - p1;       // always keep p1,p2 ordered p1<=p2
                p1 = p2;
                p2 = i;
            } else {  
                if(chs[i] == chs[p1]) {  // 0 or repeating charcters lead to p1=p2
                    p1 = p1 == p2 ? i:p2;
                }
                len++;
                p2 = i;
            }
        }
        
        if (len > max) max = len;
        return max;
    }
}

今天先写到这里,以后补充。


大米中的大米
12 声望5 粉丝

你的code是面向面试编程的,收集和整理leetcode discussion里个人认为的最优且最符合我个人思维逻辑的解法。