string类问题之滑动窗口类型

Posted 毛线刷题笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了string类问题之滑动窗口类型相关的知识,希望对你有一定的参考价值。

Longest Substring Without Repeating Characters

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

Examples:

Given "abcabcbb", the answer is "abc", which the length is 3.

Given "bbbbb", the answer is "b", with the length of 1.

Given "pwwkew", 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.

原题链接: http://oj.leetcode.com/problems/longest-substring-without-repeating-characters/ 
这道题用的方法是在LeetCode中很常用的方法,对于字符串的题目非常有用。 首先brute force的时间复杂度是O(n^3), 对每个substring都看看是不是有重复的字符,找出其中最长的,复杂度非常高。

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        //暴力解法
        if (s == null || s.length() == 0) {
            return 0;
        }
        int len = s.length();
        int max = 1;
        for (int i = 0; i < len; i++) {
            for (int j = i + 1; j < len; j++) {
                if (isValid(s, i ,j) == true) {
                    max = Math.max(max, j - i + 1);
                }
            }
        }
        return max;
    }
    private static boolean isValid(String s, int start, int end) {
        HashSet<Character> letters = new HashSet<Character>();
        for (int i = start; i <= end; i++) {
            if(letters.contains(s.charAt(i))){
                return false;
            }
            letters.add(s.charAt(i));
        }
        return true;
    }
}
lengthOfLongestSubstring

优化一些的思路是稍微动态规划一下,每次定一个起点,然后从起点走到有重复字符位置,过程用一个HashSet维护当前字符集,认为是constant操作,这样算法要进行两层循环,复杂度是O(n^2)。 

最后,我们介绍一种线性的算法,也是这类题目最常见的方法。基本思路是维护一个窗口,每次关注窗口中的字符串,在每次判断中,左窗口和右窗口选择其一向前移动。同样是维护一个HashSet, 正常情况下移动右窗口,如果没有出现重复则继续移动右窗口,如果发现重复字符,则说明当前窗口中的串已经不满足要求,继续移动有窗口不可能得到更好的结果,此时移动左窗口,直到不再有重复字符为止,中间跳过的这些串中不会有更好的结果,因为他们不是重复就是更短。因为左窗口和右窗口都只向前,所以两个窗口都对每个元素访问不超过一遍,因此时间复杂度为O(2*n)=O(n),是线性算法。空间复杂度为HashSet的size,也是O(n). 代码如下:

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        if (s == null || s.length() == 0) {
            return 0;
        }
        int len = s.length();
        int max = 1;
        int slow = 0, fast = 0;
        HashSet<Character> letters = new HashSet<Character>();
        while (fast < len && slow < len ) {
            if (!letters.contains(s.charAt(fast))) {
                letters.add(s.charAt(fast));
                fast++;
                
            } else {
                letters.remove(s.charAt(slow));    
                slow++;
            }
            max = Math.max(max, fast - slow);
        }
        return max;
    }
}
lengthOfLongestSubstring

--------------------------------------------------------------------------------

Minimum Window Substring

  • Total Accepted: 74481
  • Total Submissions: 326266
  • Difficulty: Hard

 

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

For example,
S = "ADOBECODEBANC"
T = "ABC"

Minimum window is "BANC".

最短摘要算法
转自 Rudy-Yuan  http://www.rudy-yuan.net/archives/185/
 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).

For example,
S = “ADOBECODEBANC”
T = “ABC”

Minimum window is “BANC”.

一个最短摘要算法的简化版本。

用到滑动窗口的概念。整个算法的运行过程可通过下图表示,注意仔细体会!

注意

在上图中灰色部分,当窗口中出现了所有目标字符后,需要从窗口的最左边开始往右滑动窗口边界,将左边窗口中多出来的目标字符移到窗口外,从而找到一个备选的最短窗口!

 

这个题较难,值得好好理解。

java代码:

public class Solution {
    public String minWindow(String s, String t) {
        int[] needToFind = new int[256];
        int[] hasFound = new int[256];
        int findAll = 0;
        int start = 0, end = 0;
        int s_len = s.length();
        int t_len = t.length();
        
        int minLen = Integer.MAX_VALUE;
        int minStart = 0;
        int minEnd = s.length() - 1;
        
        char c;
        intial(t, needToFind);
        while (end < s_len) {
            c = s.charAt(end);
            if (needToFind[c] ==  0) {
                end++;
                continue;
            }
            hasFound[c]++;
            if (hasFound[c] <= needToFind[c]) {
                findAll++;
            }
                
            if (findAll == t_len) {
                while (needToFind[s.charAt(start)] == 0 || hasFound[s.charAt(start)] > needToFind[s.charAt(start)]) {
                    if (hasFound[s.charAt(start)] > needToFind[s.charAt(start)]) {
                        hasFound[s.charAt(start)]--;
                    }
                    start++;
                    }
                    
                if(minLen > (end-start+1)){
                    minLen = end-start+1;
                    minStart = start;
                    minEnd = end;
                }
                
            }    
            end++;
        }// end of  while (end < s_len) 

        if (findAll < t_len) {
            return "";
        }
        return s.substring(minStart, minEnd + 1);
    }
    
    private static void intial(String t, int[] needToFind) {
        int len = t.length();
        for (int i = 0; i < len; i++) {
            char c = t.charAt(i);
            needToFind[c]++;
        }
        return;
    }
}
View Code

真心很经典的题目

1. 利用find all 解决了每次新增加一个节点要用o(256)来判断是否合理的问题

2.两个指针,类似于counting sort的思想

3.用required char[i] 是否大于0来表示是否是我们需要的char

4.循环体的控制,外层循环是不断的把end往后移动,内层循环是把start向前移动

5. substring里面 end index 要注意+ 1

----------------------------------------------------------------------------------

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.

For example, given:
s: "barfoothefoobarman"
words: ["foo", "bar"]

You should return the indices: [0,9].
(order does not matter).

 

这道题看似比较复杂,其实思路和Longest Substring Without Repeating Characters差不多。因为那些单词是定长的,所以本质上和单一个字符一样。和Longest Substring Without Repeating Characters的区别只在于我们需要维护一个字典,然后保证目前的串包含字典里面的单词有且仅有一次。思路仍然是维护一个窗口,如果当前单词在字典中,则继续移动窗口右端,否则窗口左端可以跳到字符串下一个单词了。假设源字符串的长度为n,字典中单词的长度为l。因为不是一个字符,所以我们需要对源字符串所有长度为l的子串进行判断。做法是i从0到l-1个字符开始,得到开始index分别为i, i+l, i+2*l, ...的长度为l的单词。这样就可以保证判断到所有的满足条件的串。因为每次扫描的时间复杂度是O(2*n/l)(每个单词不会被访问多于两次,一次是窗口右端,一次是窗口左端),总共扫描l次(i=0, ..., l-1),所以总复杂度是O(2*n/l*l)=O(n),是一个线性算法。空间复杂度是字典的大小,即O(m*l),其中m是字典的单词数量。

 

首先是先把所给的字典利用HashMap (toFind)建一下,key存word,value存这个word出现的个数。

 因为每个单词长度一样,外层循序只许循环wordLen次,每次指针挪一次,每一次循环遍历整个字符串。

 内层循环每次遍历一个单词 (即 j = j + wordLen),把整个S字符串遍历检查。

 需要在每次大循环维护一个count,看是不是达到了给的字典字符串数量,同时维护一个left 指针表示窗口的左端,也就是起始的index---每个符合条件的字符串的起始index,需要存到返回结果中。

 为了能够检查是不是合格字符串,在这里维护一个hasFind的HashMap。用于记录窗口内找到的单词及其数量。

 

 首先检查一个单词是不是在原始字典中出现,没出现的话说明这个单词肯定不符合标准,left指针指向下一个单词的起始点,计数器和hasFind都要清零。即string一旦被打断,就要从新的起始位置开始重新查找所有的wrod。

 如果这个单词在原始字典里出现过,用更新原始字典的方法更新hasFind,如果这个单词出现的次数没有超过原始字典里记录的次数,那么count++。如果超过了,就需要挪动指针,并把超过的从hasFind里面删掉。用超过的这个单词作为while循环的控制变量,来从左侧开始删除单词。 注意,如果删除的单词在hasFind里面的数量比需要查找的数量小了,也就是说减少了已找到的数量,那么我们需要减小计数器。

 最后,如果count达到了L的length,说明找到了一个合格的字符串,那么将index存入返回结果res中,再把left挪到下一个单词处,更新hasFind即可。

public class Solution {
public List<Integer> findSubstring(String S, String[] L) {  
    // Note: The Solution object is instantiated only once and is reused by each test case.  
    List<Integer> result = new ArrayList<Integer>();  
    if(S==null || S.length()==0 || L==null || L.length==0)  {
        return result;  
    }
    HashMap<String,Integer> toFind = new HashMap<String,Integer>();  
    for(String temp : L)  
    {  
         if (!toFind.containsKey(temp)){
                toFind.put(temp, 1);
            }
            else{
                toFind.put(temp, toFind.get(temp) + 1); 
            }
    }  
    
    int wordLen = L[0].length();
    int sLen = S.length();
    int lLen = L.length;
    for(int i = 0; i < wordLen; i++)  {  
        HashMap<String,Integer> hasFind = new HashMap<String,Integer>();  
        int count = 0;  
        int left = i;  
        for(int j = i; j <= sLen - wordLen; j += wordLen) {  
            String current = S.substring( j, j + wordLen);  
            if(!toFind.containsKey(current)){
                hasFind.clear();
                count = 0;
                left = j + wordLen;
            } else {  
                //update hasFind dictionary
                if(hasFind.containsKey(current)) {
                    hasFind.put(current,hasFind.get(current) + 1);  
                }
                else {
                    hasFind.put(current,1);
                }
                //update count
             
                
                if(hasFind.get(current) <= toFind.get(current)) {
                    count++;  
                } else {  
                    while (hasFind.get(current) > toFind.get(current)) {  
                        String temp = S.substring(left,left + wordLen);  
                        hasFind.put(temp, hasFind.get(temp) - 1);  
                        left += wordLen;  
                        if(hasFind.get(temp) < toFind.get(temp)){
                            count--;
                        }
                    } 
                }  
               

                if(count == lLen) {  
                    result.add(left);  
                    String temp = S.substring(left, left + wordLen);  
                    hasFind.put(temp,hasFind.get(temp) - 1);  
                    count--;  
                    left += wordLen; 
                }
            
            }  
        }    //end for i
    }  //end for j
    return result;  

    }
}
findSubstring

 

以上是关于string类问题之滑动窗口类型的主要内容,如果未能解决你的问题,请参考以下文章

字符串类题目——滑动窗口和递归分治以及一些些位运算的结合

数据结构与算法:滑动窗口类题目总结

Flink详解系列之六--窗口机制

java数据结构与算法之最长无重复子串问题

协议森林图说TCP之滑动窗口和拥塞窗口

Redis之zset实现滑动窗口限流