子字符串问题的一种通用解法

本文针算法题中常见的子字符串类型题目,总结一种较为通用的方法,并以三个题目进行讲解。

LeetCode 76. Minimum Window Substring(最小覆盖子串)

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).

Example:

Input: S = "ADOBECODEBANC", T = "ABC"
Output: "BANC"

Note:

  • If there is no such window in S that covers all characters in T, return the empty string "".
  • If there is such window, you are guaranteed that there will always be only one unique minimum window in S.

题目要求:给一个字符串S和目标字符串T,找到S的最短子字符串要求包含T中所有字符。

思路:用map记录T中各个字符出现的次数,用counter记录T中总的字符数。从头开始遍历字符串,维护一个动态窗口,began指向窗口起始位置,end指向窗口结束位置。counter用我们用一个map记录窗口中的字符是否存在于T,由于T中字符可能存在重复,因此对于字符的计数是有必要的。遍历时,end指针右移,判断该位置字符是否存在于T,若是并且map中该字符对应的计数大于零,则map中该字符计数减一,counter减一(表示T中该字符被找到了一个,下面的任务是找到剩下的counter个字符)。当counter==0,说明T中所有的字符全被找到了,此时的窗口表示的字符串是一个有效的字符串,若比之前找到的有效字符串更短,则将head指向began,d更新为更端的长度。同时,我们判断此时的began处的字符在map中的计数,如果为负,则说明这个字符已经被减了多次(在窗口中冗余了,多出了我们需要的个数),那began可以直接右移一位,map中的计数加一(表示这个字符被踢出计数范围了);如果为0,说明窗口中该字符的个数不多不少,正是我们所需要的个数,因为我们需要寻找新的有效字符串,将began右移后,map中该计数加一,counter也要加一(表示现在那个有效的字符被踢出了现在需要寻找另一个一样的字符替换它)。遍历完就可以使用我们记录的头指针head和最短长度d返回结果了。

class Solution{
    public String minWindow(String s, String t) {
        int head = 0, begain = 0, end = 0, counter = t.length();
        int d = Integer.MAX_VALUE;
        HashMap<Character, Integer> map = new HashMap<>();
        for(char c : t.toCharArray()){
            map.put(c, map.getOrDefault(c, 0) + 1);
        }
        while(end < s.length()){
            if(map.containsKey(s.charAt(end))){
                map.put(s.charAt(end), map.get(s.charAt(end)) - 1);
                if(map.get(s.charAt(end)) > -1)
                    counter--;
            }
            end++;
            while(counter == 0){
                if(d > end - begain) d = end - (head = begain);
                if (map.containsKey(s.charAt(begain))){
                    map.put(s.charAt(begain), map.get(s.charAt(begain)) + 1);
                    if(map.get(s.charAt(begain)) > 0) counter++;
                }
                begain++;
            }
        }
        if(d != Integer.MAX_VALUE) return s.substring(head, head + d);
        else return "";
    }
}

这里面有个简化的地方。

上面的map中记录的全部是在目标字符串中出现的字符。这就导致我们每次滑动end指针和began指针时都要判断它是否是T中的字符,从而决定是否要对map进行加一或减一。比如在内循环while中,当我们判断了它是T中的字符,我们要对map中计数加一,当加一后若大于0,说明began滑动后这个缺失了,counter要加一,以使当前窗口表示的字符无效,并寻找新的字符。也就是说counter每次是根据map相应位置的计数判断是否改变的,那么我们能不能在遍历的时候直接将非T中的字符也放入map呢?答案是肯定的,因为非T中字符在窗口中总是冗余的,所以存入map,map中的计数总是为负,也表示冗余状态,这个判断出这个状态就无需对counter进行修改。因此我们就简化了每次对滑动处的字符的判断。代码如下:

class Solution{
    public String minWTindow(String s, String t) {
        int head = 0, began = 0, end = 0, counter = t.length();
        int d = Integer.MAX_VALUE;
        HashMap<Character, Integer> map = new HashMap<>();
        for(char c : t.toCharArray()){
            map.put(c, map.getOrDefault(c, 0) + 1);
        }
        while(end < s.length()){
            map.put(s.charAt(end), map.getOrDefault(s.charAt(end), 0) - 1);
            if(map.get(s.charAt(end)) > -1)
                counter--;
            end++;
            while(counter == 0){
                if(d > end - began) d = end - (head = began);
                map.put(s.charAt(began), map.get(s.charAt(began)) + 1);
                if(map.get(s.charAt(began)) > 0) counter++;
                began++;
            }
        }
        if(d != Integer.MAX_VALUE) return s.substring(head, head + d);
        else return "";
    }
}

我们可以总结一个解决子字符串类型题目的模板,如下:

class Solution{
    public String minWTindow(String s, String t) {
        int counter;                // 用于判断此时的窗口是否是有效子字符串
        int began = 0, end = 0;     // 两个指针,标记窗口的始末位置
        int head = 0;               // 最优有效子字符串的起使位置
        int d;                      // 最优有效子字符串的长度 
        HashMap<Character, Integer> map = new HashMap<>();  // 字符计数器
        for(){
            // 初始化字符计数器
        }
        while(end < s.length()){
            map.put(s.charAt(end), map.getOrDefault(s.charAt(end), 0) - 1);
            if(map.get(s.charAt(end)) > -1)
                // 修改counter
            end++;  // 滑动end指针
            while( /*counter状态判断*/ ){
                if( /*d:最优结果判断条件*/) d = ..; // 更新最优结果
                map.put(s.charAt(began), map.get(s.charAt(began)) + 1);
                if(map.get(s.charAt(began)) > 0) ..;    // 修改counter
                began++;    // 滑动began指针
            }
        }
        if(d != Integer.MAX_VALUE) return s.substring(head, head + d);
        else return "";
    }
}

好了,我们现在尝试用这个方法做一个简单点的题目。

LeetCode 3.Longest Substring Without Repeating Characters(无重复字符的最长子串)

Given a string, find the length of the longest substring without repeating characters.

Example 1:

Input: "abcabcbb"
Output: 3 
Explanation: The answer is "abc", with the length of 3. 

Example 2:

Input: "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.

Example 3:

Input: "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3. 
             Note that the answer must be a substring, "pwke" is a subsequence and not a substring.

题目要求:给一个字符串,寻找最长不含重复字符的子字符串。

思路:维护一个滑动窗口,map记录字符个数,counter初始为0,表示窗口是否是有效子字符串,大于0,说明有重复,需要滑动began,消除重复。遍历字符串,将end指针所指的字符放入map(相应位置加一),如果该字符计数大于一,就说明有重复了,counter加一。若counter大于0,说明有重复,滑动began。

只返回最长不含重复字符的子字符串长度,代码如下:

class Solution{
    public int lengthOfLongestSubstring(String s) {
        int begain = 0, end = 0, d = 0;
        HashMap<Character, Integer> map = new HashMap<>();
        int counter = 0;
        while (end < s.length()) {
            map.put(s.charAt(end), map.getOrDefault(s.charAt(end), 0) + 1);
            if(map.get(s.charAt(end)) > 1) counter++;
            end++;
            while (counter > 0) {
                map.put(s.charAt(begain), map.get(s.charAt(begain)) - 1);
                if(map.get(s.charAt(begain)) >= 1) counter--;
                begain++;
            }
            d = Math.max(d, end - begain);
        }
        return d;
    }
}

返回最长不含重复字符的子字符串,代码如下:

class Solution{
    public String LongestSubstring(String s) {
        int began = 0, end = 0, d = 0;
        int head = 0;
        HashMap<Character, Integer> map = new HashMap<>();
        int counter = 0;
        while (end < s.length()) {
            map.put(s.charAt(end), map.getOrDefault(s.charAt(end), 0) + 1);
            if(map.get(s.charAt(end)) > 1) counter++;
            end++;
            while (counter > 0) {
                map.put(s.charAt(began), map.get(s.charAt(began)) - 1);
                if(map.get(s.charAt(began)) >= 1) counter--;
                began++;
            }
            if(d < end - began) d = end - (head = began);
        }
        return s.substring(head, d);
    }
}

我知道你可能有别的方法,但使熟练使用这种方法后,是不是思路会更清晰呢。

就做两个题,可能离熟练还差点意思,那再来一道。

Longest Substring with At Most Two Distinct Characters

题目要求:寻找只包含两个不同字符的最长子字符串,比如输入:”abddcccffffee",结果就是“cccffff”。

代码如下:

class Solution{
    public String longestSubstring2(String s){
        int began = 0, end = 0, counter = 0;    // 记录窗口中不同的字符有多少个
        int head = 0, d = Integer.MIN_VALUE;    // 记录最优子字符串的起始位置和长度
        HashMap<Character, Integer> map = new HashMap<>();
        while (end < s.length()) {
            map.put(s.charAt(end), map.getOrDefault(s.charAt(end), 0) + 1);
            if (map.get(s.charAt(end)) == 1) counter++;     // 当新加入的字符计数为1,则表明这是个"新成员”,需要跟新counter
            end++;
            while (counter > 2){                // 当counter超过2,字符无效,就要滑动began使字符重新变得有效
                map.put(s.charAt(began), map.get(s.charAt(began)) - 1);
                if(map.get(s.charAt(began)) <= 0) counter--;
                began++;
            }
            // 这里需要注意!
            // 更新最优子字符串需要在内层while循环后,而不是内部,因为内部窗口还不是有效的子字符串(不同的字符超过2)
            if(d < end - began) d = end - (head = began);
        }
        if(d != Integer.MIN_VALUE) return s.substring(head, head + d);
        else return "";
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容