滑动窗口算法详解__java版(与C++的区别注意)

滑动窗口算法详解

一. 算法框架

滑动窗口算法:

int left =0,right =0;
while(right<s.size()){
	//增大窗口
	window.add(s[right]);
	right++;
	while(window needs shrink){
		//缩小窗口
		window.remove(s[left]);
		left++;
	}
}

其实对应的算法思路很简单,主要是细节比较繁琐。 比如如何向窗口添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。

滑动窗口套路框架:

void slidingWindow(String s,String t){
	Map<char,Integer> need = new HashMap<>();
	Map<char,Integer> window = new HashMap<>();
	for(char c:t) need.put(c,map.getOrDefault(c,0)+1);
	int left =0,right =0;
	int valid =0;
	while (right<s.size()){
		//c是将移入窗口的字符
		char c = s[right];
		//右移窗口
		right++;
		//进行窗口内数据的一系列更新
		...
		//debug输出的位置
		System.out.println("window:[%d,%d)\n",left,right);
		//判断左侧窗口是否要收缩
		while(window needs shrink){
			//d是将移出窗口的字符
			char d = s[left];
			left++;
			//进行窗口数据的一系列更新;
		}
	}
}

二.最小覆盖子串

滑动窗口算法详解__java版(与C++的区别注意)_第1张图片
滑动窗口算法的思路实际上是这样的:

  1. 使用双指针中的左右指针技巧,初始化left=right=0,把[left,right)称为一个窗口。
  2. 先不断增大right,扩大这个窗口,直到窗口满足我们所需的条件(这道题中即,包含了T所有字符)
  3. 再不断增大left,缩小这个窗口,使得我们在保证这个窗口满足条件的同时,尽可能的小。直到窗口内的内容不满足条件,跳出(即不包含T中所有字符了)。每次更新left的时候,也要对最终的结果进行一次更新。
  4. 重复第2和3步,直到right达到了字符串s的尾部。

实际上,增大right是在找可行解,找到可行解后,通过增大left来找最优解(即优化)。

在框架中使用了needs和window字典,他们充当的是计数器的功能。needs用来记录T中每个字符出现的次数,即:我们所需要的字符的情况;window字典则是用来记录当前实际窗口中包含对应字符的情况,通过两个map的对比,来判断是否满足了条件。

回到这道题:
首先,我们初始化needs和window两个哈希表,记录需要凑齐的字符和当前窗口中的字符情况。

String minstr(String s,String t){
	Map<Char,Integer> needs = new HashMap<>();
	Map<Char,Integer> window = new HashMap<>();
	for(Char c:t) needs.put(c,map.getOrDefault(c,0)+1);
	

然后,使用left和right变量初始化窗口两端,区间为左闭右开,这样保证了一开始窗口中没有任何元素。

	int left=0,right=0;
	int valid =0;
	while (right<s.size()){
		//开始滑动
	}

其中,valid变量表示已经满足条件的字符的个数,如果valid和need中key的数量相等(needs.size()),则说明窗口已满足条件,完全覆盖了T。

接下来需要考虑的问题是:

  1. 扩大窗口时,应该更新哪些数据?
  2. 什么条件下,窗口应暂停扩大,开始移动left? vaild = needs.size()
  3. 缩小窗口时,即移出字符时,应更新哪些数据?
  4. 需要的结果,应该在什么时候更新?应该在缩小窗口时。

完整代码,但是存在很多细节的小问题,后面会一一解答:

String minstr(String s,String t){
	Map<Char,Integer> needs = new HashMap<>();
	Map<Char,Integer> window = new HashMap<>();
	for(Char c:t) needs.put(c,needs.getOrDefault(c,0)+1); //java中不可以这样取单个字符,需要通过函数charAt
	int left=0,right=0;
	int valid =0;
	//记录最小覆盖子串的起始索引及长度
	int start =0,len=Integer.MAX_VALUE;
	while (right<s.length()){  //java中字符串的长度用length()函数 
		//c是将移入窗口的字符
		Char c = s[right];  //这里存在问题,java中需要通过charAt(索引)
		right++;
		//进行窗口一系列数据的更新
		if(needs.containsKey(c)){
			window.put(c,window.getOrDefault(c,0)+1);
			/*这里不能使用“==”,因为Integer类型值超出[-128,127]范围会new一个新的对象,
			等于是比较的对象值,所以应该用equals。*/
			if(window[c] == needs[c])  
			
				valid++;
		}
		while(valid == need.size()){
			//更新最小覆盖子串
			if(right-left <len){
				start = left;
				len = right - left;
			}
			//d是将要移出窗口的字符
			Char d = s[left]; //同上
			left++;
			if(needs.containsKey(d)){
				if(window[d] == needs[d])  //同上
					valid--;
				window.put(d,window.getOrDefault(d,0)-1);
			}
		}
	}
	//返回最小子串
	return len == Integer.MAX_VALUE?"":s.substr(start,len); // java中截取字符串中的子串,使用substring函数,且参数应为(起始index,终止index)

修改后的代码如下:

public String minWindow(String s,String t){
        Map<Character,Integer> needs = new HashMap<Character,Integer>();
        Map<Character,Integer> window = new HashMap<Character,Integer>();
        int tLen = t.length();
        for (int i = 0; i < tLen; i++) {
            char c = t.charAt(i);
            needs.put(c,needs.getOrDefault(c,0)+1);
        }
        int left=0,right=0;
        int valid =0;
        //记录最小覆盖子串的起始索引及长度
        int start =0,len=Integer.MAX_VALUE; boolean sign = false;
        while (right<s.length()){
            //c是将移入窗口的字符
            char c = s.charAt(right);
            right++;
            //进行窗口一系列数据的更新
            if(needs.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if(window.get(c).equals(needs.get(c)))
                    valid++;
            }
            while(valid == needs.size()){
                sign = true;
                //更新最小覆盖子串
                if(right-left <len){
                    start = left;
                    len = right - left;
                }
                //d是将要移出窗口的字符
                char d = s.charAt(left);
                left++;
                if(needs.containsKey(d)){
                    if(window.get(d).equals(needs.get(d)))
                        valid--;
                    window.put(d,window.getOrDefault(d,0)-1);
                }
            }
        }
        //返回最小子串
        return len==Integer.MAX_VALUE?"":s.substring(start,start+len);
    }

关于Integer和int的比较以及equals和“==”的使用,下面这个链接有很详细的讲解: https://blog.csdn.net/andyzhaojianhui/article/details/84324466

三. 字符串排列

滑动窗口算法详解__java版(与C++的区别注意)_第2张图片
这道题跟上面那道题不同的地方在于:这道题中s2是要包含s1这个子串或者这个子串的排列的,也就是说在s2中搜索的子串大小就应该为2

public boolean checkInclusion(String s1, String s2) {
        Map<Character,Integer> needs = new HashMap<>();
        Map<Character,Integer> window = new HashMap<>();
        int sz = s1.length();
        for(int i=0;i<sz;i++){
            char c = s1.charAt(i);
            needs.put(c,needs.getOrDefault(c,0)+1);
        }
        int left =0,right =0;
        int valid =0;
        while(right < s2.length()){
            char c = s2.charAt(right);
            right++;
            if(needs.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if(window.get(c).equals(needs.get(c)))
                    valid++;
            }
            while(right-left>=s1.length()){
                if(valid == needs.size())
                    return true;
                char d = s2.charAt(left);
                left++;
                if(needs.containsKey(d)){
                    if(window.get(d).equals(needs.get(d)))
                        valid--;
                    window.put(d,window.getOrDefault(d,0)-1);
                }       
            }     
        }
        return false;
    }

四.找所有字母异位词

滑动窗口算法详解__java版(与C++的区别注意)_第3张图片

public List<Integer> findAnagrams(String s, String p) {
        Map<Character,Integer> needs = new HashMap<>();
        Map<Character,Integer> window = new HashMap<>();
        ArrayList<Integer> res = new ArrayList<>();
        if(s.length()<p.length()) return res;
        int sz = p.length();
        for(int i =0;i<sz;i++){
            char c = p.charAt(i);
            needs.put(c,needs.getOrDefault(c,0)+1);
        }
        int left =0, right=0;
        int valid=0;
        while(right<s.length()){
            char c = s.charAt(right);
            right++;
            if(needs.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if(needs.get(c).equals(window.get(c)))
                    valid++;
            }
            while(right-left>=p.length()){
                if(valid == needs.size()){
                    res.add(left);
                }
                char d = s.charAt(left);
                left++;
                if(needs.containsKey(d)){
                    if(needs.get(d).equals(window.get(d)))
                        valid--;
                    window.put(d,window.getOrDefault(d,0)-1);   
                }
            }
        }
        return res;
    }

五.最长无重复子串

滑动窗口算法详解__java版(与C++的区别注意)_第4张图片

 public int lengthOfLongestSubstring(String s) {
        Map<Character,Integer> window = new HashMap<>();
        if (s.length()==0) return 0;
        int left =0,right=0;
        int MAX_str = Integer.MIN_VALUE;
        // ArrayList res = new ArrayList<>();
        while(right<s.length()){
            char c = s.charAt(right);
            right++;
            //窗口操作
            window.put(c,window.getOrDefault(c,0)+1);
            while(window.get(c)>1){
                char d = s.charAt(left);
                left++;
                 window.put(d,window.getOrDefault(d,0)-1);
            }
            MAX_str = Math.max(MAX_str,right-left);
        }
        return MAX_str;
    }

这实际上把问题变简单了。但是需要注意的是,如何判断收缩窗口的时机,这里是使用window[c]>1,这说明窗口中存在重复字符。 还有就是在哪儿更新res,因为我们要的是最长无重复子串,所以只要找到一个阶段保证窗口中没有重复字符串就行。

你可能感兴趣的:(滑动窗口算法详解__java版(与C++的区别注意))