子串問題(滑動(dòng)窗口)

1.無重復(fù)字符的最長(zhǎng)子串(3 - 中/劍指48)

題目描述:給定一個(gè)字符串蝗柔,請(qǐng)你找出其中不含有重復(fù)字符的 最長(zhǎng)子串 的長(zhǎng)度。

示例 :

輸入: s = "abcabcbb"
輸出: 3 
解釋: 因?yàn)闊o重復(fù)字符的最長(zhǎng)子串是 "abc"情龄,所以其長(zhǎng)度為 3迄汛。

思路:本題是面試的高頻題目捍壤,也是hash表的一個(gè)具體應(yīng)用。思路是維持一個(gè)隊(duì)列(窗口)鞍爱,保持隊(duì)列中的元素滿足題目要求(元素不重復(fù))鹃觉。

具體實(shí)現(xiàn)是:使用hashmap記錄每個(gè)字符的索引位置,便于更新下一個(gè)窗口的左邊界睹逃,遍歷字符串盗扇。

  • 如果窗口中含有這個(gè)元素,移動(dòng)移除左邊界元素(左邊索引+1)沉填;
  • 否則加入窗口疗隶,更新最長(zhǎng)子串長(zhǎng)度。

代碼實(shí)現(xiàn):

public int lengthOfLongestSubstring(String s) {
    int n = s.length();
    if (s == null || n == 0) {
        return 0;
    }
    Map<Character, Integer> map = new HashMap<>();
    int max = 0;
    int left = 0;
    for (int i = 0; i < n; ++i) {
        char c = s.charAt(i);
        if (map.containsKey(c)) {
            left = Math.max(left, map.getOrDefault(c, 0) + 1);
        }
        map.put(c, i);
        max = Math.max(max, i - left + 1);
    }
    return max;
}

2.串聯(lián)所有單詞的子串(30 - 難)

題目描述:給定一個(gè)字符串 s 和一些長(zhǎng)度相同的單詞 words翼闹。找出 s 中恰好可以由 words 中所有單詞串聯(lián)形成的子串的起始位置斑鼻。

注意:子串要與 words 中的單詞完全匹配,中間不能有其他字符猎荠,但不需要考慮 words 中單詞串聯(lián)的順序卵沉。

示例 :

輸入:
  s = "barfoothefoobarman",
  words = ["foo","bar"]
輸出:[0,9]
解釋:
從索引 0 和 9 開始的子串分別是 "barfoo" 和 "foobar" 。
輸出的順序不重要, [9,0] 也是有效答案法牲。

思路:由于單詞長(zhǎng)度固定史汗,我們維護(hù)一個(gè)長(zhǎng)度為所有元素長(zhǎng)度總和的隊(duì)列(窗口)。本題也是考察hash表拒垃,鍵為存儲(chǔ)的單詞停撞,值存的單詞出現(xiàn)的次數(shù)。具體實(shí)現(xiàn)見代碼悼瓮。注意:

  • 可以用indexOf()戈毒,判斷一個(gè)字符或者字符串是否存在一個(gè)字符串中,不存在返回負(fù)數(shù)横堡;否則將這個(gè)單詞加入哈希表埋市;

  • 當(dāng)單詞數(shù)組長(zhǎng)度超過字符串長(zhǎng)度,直接剔除命贴。

  • 對(duì)遍歷的每一個(gè)窗口道宅,用一個(gè)subMap記錄,left和right記錄左右邊界(更新的話胸蛛,是先將單詞從字符串中截取出來污茵,再在subMap中刪除);

  • 通過移動(dòng)右邊界right葬项,依次得到一個(gè)單詞泞当。如果wordMap中沒有這個(gè)單詞,更新左邊界民珍,清除這次記錄(包括subMap和單詞數(shù)量記錄數(shù)count)襟士;否則盗飒,加入subMap,注意count符合要求統(tǒng)計(jì)左邊界陋桂,超過限制刪除左邊界箩兽。

代碼實(shí)現(xiàn):

public static List<Integer> solution2(String s, String[] words) {
    List<Integer> res = new ArrayList<>();
    Map<String, Integer> wordsMap = new HashMap<>();
    if (s.length() == 0 || words.length == 0) return res;
    for (String word: words) {
        // 主串s中沒有這個(gè)單詞,直接返回空
        if (s.indexOf(word) < 0) return res;
        wordsMap.put(word, wordsMap.getOrDefault(word, 0) + 1);
    }
    int oneLen = words[0].length(), wordsLen = oneLen * words.length;
    if (wordsLen > s.length()) return res;

    for (int i = 0; i < oneLen; ++i) {
        int left = i, right = i, count = 0;
        // 統(tǒng)計(jì)每個(gè)符合要求的word
        Map<String, Integer> subMap = new HashMap<>();
        // 右窗口不能超出主串長(zhǎng)度
        while (right + oneLen <= s.length()) {
            // 得到一個(gè)單詞
            String word = s.substring(right, right + oneLen);
            right += oneLen;
            // words[]中沒有這個(gè)單詞章喉,那么當(dāng)前窗口肯定匹配失敗,直接右移到這個(gè)單詞后面
            if (!wordsMap.containsKey(word)) {
                left = right;
                subMap.clear();
                count = 0;
            } else {
                subMap.put(word, subMap.getOrDefault(word, 0) + 1);
                ++count;
                // 如果這個(gè)單詞出現(xiàn)的次數(shù)大于words[]中它對(duì)應(yīng)的次數(shù)身坐,又由于每次匹配和words長(zhǎng)度相等的子串
                // 如 ["foo","bar","foo","the"]  "| foobarfoobar| foothe"
                // 第二個(gè)bar雖然是words[]中的單詞秸脱,但是次數(shù)抄了,那么右移一個(gè)單詞長(zhǎng)度后 "|barfoobarfoo|the"
                // bar還是不符合部蛇,所以直接從這個(gè)不符合的bar之后開始匹配摊唇,也就是將這個(gè)不符合的bar和它之前的單詞(串)全移出去
                while (subMap.getOrDefault(word, 0) > wordsMap.getOrDefault(word, 0)) {
                    // 從當(dāng)前窗口字符統(tǒng)計(jì)map中刪除從左窗口開始到數(shù)量超限的所有單詞(次數(shù)減一)
                    String w = s.substring(left, left + oneLen);
                    subMap.put(w, subMap.getOrDefault(w, 0) - 1);
                    // 符合的單詞數(shù)減一
                    --count;
                    // 左窗口位置右移
                    left += oneLen;
                }
                // 當(dāng)前窗口字符串滿足要求
                if (count == words.length) res.add(left);
            }
        }
    }
    return res;
}

3.最小覆蓋子串(76 - 難)

題目描述:給你一個(gè)字符串 s 、一個(gè)字符串 t 涯鲁。返回 s 中涵蓋 t 所有字符的最小子串巷查。如果 s 中不存在涵蓋 t 所有字符的子串,則返回空字符串 "" 抹腿。要求:時(shí)間復(fù)雜度O(N)岛请。

注意:如果 s 中存在這樣的子串,我們保證它是唯一的答案警绩,兩個(gè)字符串只含有英文字符崇败。

示例 :

輸入:s = "ADOBECODEBANC", t = "ABC"
輸出:"BANC"

思路:可以通過滑動(dòng)窗口解決,注意:最小覆蓋子串長(zhǎng)度不唯一肩祥,但至少與t相同后室。我們可以定義兩個(gè)變量:start(最小覆蓋子串的起始位置),size(最小覆蓋子串的長(zhǎng)度)

關(guān)鍵:如何判斷窗口中含有t的所有元素混狠?可以使用數(shù)組或者哈希表記錄字符出現(xiàn)的次數(shù)岸霹。

  • 使用數(shù)組記錄t中(我們需要尋找的)字符的個(gè)數(shù),cnt記錄要找字符總數(shù)将饺。

  • 當(dāng)我們遍歷s字符串時(shí)贡避,遇到需要的字符我們就將他出現(xiàn)的次數(shù) - 1∮杌。總次數(shù)cnt == 0贸桶,此時(shí)找到一個(gè)覆蓋子串。

  • 上述還不是一次尋找的最小覆蓋子串桌肴,需要找左邊界皇筛,如何找呢?還是通過出現(xiàn)的次數(shù)坠七,方法:在遍歷過程中水醋,s中出現(xiàn)每個(gè)的字符在need數(shù)組中-1旗笔,這樣我們?cè)谡易筮吔缡菫樨?fù)數(shù),一定要恢復(fù)拄踪!

  • 這時(shí)蝇恶,可以統(tǒng)計(jì)長(zhǎng)度size,更新起始索引start惶桐;

  • 最終撮弧,移動(dòng)窗口,如何移動(dòng)(刪除左邊界元素)呢姚糊?將其加入need數(shù)組贿衍,更新左邊界和cnt個(gè)數(shù) + 1,下次遍歷出現(xiàn)這個(gè)字符就可以刪除(也可以理解為替換)救恨。

代碼實(shí)現(xiàn):

public String minWindow(String s, String t) {
    if (s == null || s.length() == 0 || t == null || t.length() == 0 || s.length() < t.length()) {
        return "";
    }
    // 記錄需要字符的個(gè)數(shù)
    int[] need = new int[128];
    for (int i = 0; i < t.length(); i++) {
        need[t.charAt(i)]++;
    }
    int left = 0;
    int size = Integer.MAX_VALUE, cnt = t.length();
    int start = 0;
    for (int i = 0; i < s.length(); i++) {
        if (need[s.charAt(i)] > 0) cnt--;
        need[s.charAt(i)]--;
        // 窗口中已經(jīng)包含所有需要的字符
        if (cnt == 0) {
            while (left < i && need[s.charAt(left)] < 0) {
                need[s.charAt(left)]++;
                left++;
            }
            if (i - left + 1 < size) {
                size = i - left + 1;
                start = left;
            }
            // 窗口移動(dòng)贸辈,注意更新need數(shù)組和cnt值
            need[s.charAt(left)]++;
            left++;
            cnt++;
        }
    }
    return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
}

4.找到字符串中所有字母的異位詞(438 - 中)

題目描述

給定一個(gè)字符串 s 和一個(gè)非空字符串 p,找到 s 中所有是 p 的字母異位詞的子串肠槽,返回這些子串的起始索引擎淤。字符串只包含小寫英文字母,并且字符串 sp 的長(zhǎng)度都不超過 20100秸仙。

說明:

  • 字母異位詞指字母相同嘴拢,但排列不同的字符串。
  • 不考慮答案輸出的順序寂纪。

示例 :

輸入:
s: "cbaebabacd" p: "abc"

輸出:
[0, 6]

解釋:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母異位詞炊汤。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母異位詞。

思路:顯然滑窗弊攘,窗口大小為p字符串的長(zhǎng)度抢腐,關(guān)鍵:那么如何比較兩部分的元素是否是異位詞呢?

  • 可以直接調(diào)庫函數(shù)Arrays.equals()襟交,判斷兩個(gè)字符串是否是異位詞(即兩個(gè)數(shù)組是否相同)迈倍。

  • 本題仍使用數(shù)組存儲(chǔ)字符出現(xiàn)次數(shù),但是本題兩個(gè)串申請(qǐng)了兩個(gè)空間捣域,方便進(jìn)行上邊的對(duì)比啼染。

  • 這里判斷與上題相比簡(jiǎn)單了許多,只要長(zhǎng)度滿足我們就進(jìn)行判斷焕梅,兩部分異構(gòu)迹鹅,記錄起始索引;否則贞言,移除左邊界斜棚,這里直接在sMap刪除。

代碼實(shí)現(xiàn):

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> ans = new ArrayList<>();
    if (s.length() < p.length()) return ans;
    int[] pMap = new int[26];
    for (int i = 0; i < p.length(); i++) {
        pMap[p.charAt(i) - 'a']++;
    }

    int[] sMap = new int[26];
    int left = 0;
    for (int i = 0; i < s.length(); i++) {
        sMap[s.charAt(i) - 'a']++;
        if (i - left + 1 == p.length()) {
            if (Arrays.equals(sMap, pMap)) {
                ans.add(left);
            }
            sMap[s.charAt(left) - 'a']--;
            left++;
        }
    }
    return ans;
}

5.至多包含兩個(gè)不同字符的最長(zhǎng)子串(159 - 中)

題目描述:給定一個(gè)字符串 s ,找出 至多 包含兩個(gè)不同字符的最長(zhǎng)子串 t 弟蚀,并返回該子串的長(zhǎng)度蚤霞。

示例 :

示例 1:
輸入: "eceba"
輸出: 3
解釋: t 是 "ece",長(zhǎng)度為3义钉。

示例 2:
輸入: "ccaabbb"
輸出: 5
解釋: t 是 "aabbb"昧绣,長(zhǎng)度為5。

思路:維護(hù)一個(gè)窗口捶闸,元素類型至多兩種夜畴。本題關(guān)鍵:如何判斷窗口中的元素類型大于2了呢?

這里使用Hashmap記錄當(dāng)前字符在窗口中最靠右的索引值删壮,為什么這么做呢贪绘?這樣我們可以控制窗口的移動(dòng),right每右移一步:

  • 若已經(jīng)存在醉锅,更新map中當(dāng)前元素最靠右的位置

  • 若不存在,添加到map唯一存在发绢,索引也是最靠右的

這樣就好辦了硬耍,如果我們?cè)仡愋统?,即map的大小等于3边酒。

  • 記錄最小索引值经柴,用于下邊刪除和更新左邊界。

  • 刪除操作是刪除索引最小的那些或那個(gè)元素墩朦,因?yàn)橛涗浀氖怯疫吔缗魅希@樣我們也不用擔(dān)心左邊界問題。

代碼實(shí)現(xiàn):

public int lengthOfLongestSubstringTwoDistinct(String s) {
    int strLength = s.length();
    if (strLength < 3) return strLength;
    int left = 0, right = 0;
    HashMap<Character, Integer> map = new HashMap<>();
    int maxLen = 2;

    while (right < strLength) {
        map.put(s.charAt(right), right);
        right++;
        if (map.size() == 3) {
            int minIdx = Collections.min(map.values());
            map.remove(s.charAt(minIdx));
            left = minIdx + 1;
        }
        maxLen = Math.max(maxLen, right - left);
    }
    return maxLen;
}
  • ps:map.values()顯示map中所有的值氓涣,返回值類型Collection<Integer>牛哺,用min()獲取最小值。

  • map.remove(key):刪除map中鍵為key的元素

6.至多包含k個(gè)不同字符的最長(zhǎng)子串(340 - 難)

題目描述:給定一個(gè)字符串 s 劳吠,找出 至多 包含k個(gè)不同字符的最長(zhǎng)子串 t 引润,并返回該子串的長(zhǎng)度。

示例 :

示例 1:
輸入: "eceba" 2
輸出: 3
解釋: t 是 "ece"痒玩,長(zhǎng)度為3淳附。

示例 2:
輸入: "aa", 1
輸出: 2
解釋: t 是 "aa",長(zhǎng)度為2蠢古。

思路:維護(hù)一個(gè)窗口奴曙,元素類型至多k種。具體實(shí)現(xiàn):類比上一題草讶,解決方案還是使用hash表記錄字符出現(xiàn)的最右索引洽糟。

代碼實(shí)現(xiàn):

public int lengthOfLongestSubstringKDistinct(String s, int k) {
    int strLength = s.length();
    if (strLength < k + 1) return strLength;
    if (strLength * k == 0) return 0;
    int left = 0, right = 0;
    HashMap<Character, Integer> map = new HashMap<>();
    int maxLen = k;

    while (right < strLength) {
        map.put(s.charAt(right), right);
        right++;
        if (map.size() == k + 1) {
            int minIdx = Collections.min(map.values());
            map.remove(s.charAt(minIdx));
            left = minIdx + 1;
        }
        maxLen = Math.max(maxLen, right - left);
    }
    return maxLen;
}

7.替換后最長(zhǎng)重復(fù)字符(424 - 中)

題目描述:給你一個(gè)僅由大寫英文字母組成的字符串,你可以將任意位置上的字符替換成另外的字符,總共可最多替換 k 次脊框。

在執(zhí)行上述操作后颁督,找到包含重復(fù)字母的最長(zhǎng)子串的長(zhǎng)度。

示例 :

輸入:s = "ABAB", k = 2
輸出:4
解釋:用兩個(gè)'A'替換為兩個(gè)'B',反之亦然浇雹。

思路:可以想一下k = 0的情況(不替換字符)沉御,即求字符串的最長(zhǎng)重復(fù)元素。關(guān)鍵點(diǎn):如何判斷一個(gè)字符串改變了k個(gè)字符就變成了一條連續(xù)的子串昭灵?

只要保證【當(dāng)前子串的長(zhǎng)度 <= 當(dāng)前子串出現(xiàn)字符最多的個(gè)數(shù) + k】吠裆,這樣一定可以將當(dāng)前子串替換為一條連續(xù)的子串(元素都相同)。

  • 當(dāng)滿足上述條件烂完,右邊界擴(kuò)張试疙,并更新之前出現(xiàn)重復(fù)字母的最多個(gè)數(shù)(這個(gè)個(gè)數(shù)用一個(gè)數(shù)組記錄);

  • 一旦不滿足上述條件抠蚣,我們移除左邊界(直接在數(shù)組中將左邊界對(duì)應(yīng)的次數(shù) - 1)祝旷;

注意:我們窗口維護(hù)了可能的最長(zhǎng)子串,所以返回值為窗口大小嘶窄,即s.length() - left怀跛。

代碼實(shí)現(xiàn):

public int characterReplacement(String s, int k) {
    int n = s.length(); 
    if (s == null || n < 2 || n < k) return n;
    int[] map = new int[26];
    int left = 0;
    int ans = 0, preMax = 0;

    for (int i = 0; i < n; i++) {
        int index = s.charAt(i) - 'A';
        map[index]++;
        preMax = Math.max(preMax, map[index]);
        if (i - left + 1 > preMax + k) {
            map[s.charAt(left) - 'A']--;
            left++;
        }
    }
    return n - left;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市柄冲,隨后出現(xiàn)的幾起案子吻谋,更是在濱河造成了極大的恐慌,老刑警劉巖现横,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漓拾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡戒祠,警方通過查閱死者的電腦和手機(jī)骇两,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姜盈,“玉大人脯颜,你說我怎么就攤上這事》肪荩” “怎么了栋操?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)饱亮。 經(jīng)常有香客問我矾芙,道長(zhǎng),這世上最難降的妖魔是什么近上? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任右核,我火速辦了婚禮划滋,結(jié)果婚禮上凄杯,老公的妹妹穿的比我還像新娘顿涣。我一直安慰自己善涨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般失球。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上帮毁,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天实苞,我揣著相機(jī)與錄音,去河邊找鬼烈疚。 笑死黔牵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的爷肝。 我是一名探鬼主播猾浦,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼灯抛!你這毒婦竟也來了金赦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤牧愁,失蹤者是張志新(化名)和其女友劉穎素邪,沒想到半個(gè)月后外莲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猪半,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年偷线,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了磨确。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡声邦,死狀恐怖乏奥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情亥曹,我是刑警寧澤邓了,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站媳瞪,受9級(jí)特大地震影響骗炉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蛇受,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一句葵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦乍丈、人聲如沸剂碴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至铭若,卻和暖如春洪碳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叼屠。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國打工瞳腌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人镜雨。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓嫂侍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親荚坞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挑宠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容