默认格式:
class Solution {
public String minWindow(String s, String t) {
}
}
解题思路:
看到题目思考不到一分钟,脑子里只有两个字,滑块,但是我还不会使用滑块算法,先去看看视频学习一下。
每天就只是解决问题是进步非常缓慢的,所以我决定改变解决方式,掌握解决问题的方法,如果遇到不懂的解决方法就先去学习,学完之后再回来写算法,今天学习的是滑动窗口问题。下面是一些网上的抄录
滑动窗口这类问题一般需要用到 双指针来进行求解,另外一类比较特殊则是需要用到特定的数据结构。
我们只讨论一般的这种双指针的情况:
用两个int类型来存指针,start,end
被指针包括的内容就是窗口,而窗口滑动问题的核心就在于如何移动这个窗口
现在再解释一下这两个指针有什么用,一般滑动窗口问题都是用来找子串的,我们就把被窗口包括的串进行判断,根据结果来决定窗口的滑动,是要增加元素还是减少元素呢,而由于我们滑动之前的部分和滑动之后的部分的状态还是有些可以继承的部分,也就是我们可以少判断一些重复的内容,这就是使用滑动窗口的优势。
了解了原理之后,剩下的就是根据情况写出子串的判断的方法,算法的主要部分都集中在一个位置,就是end标签后移的时候,新的元素加入,对当前元素的影响进行判断,如果符合某种条件,那么就把start往后移或者不变。
了解了这些知识之后,来练习一下今天的题目。
假设一个字符串 A B B C B A B B C C
然后我们看看起点和终点应该怎么变化,反正起始肯定是0,这里就不考虑什么覆盖不覆盖的问题了,就是简单的同时包含ABC的最短字符串,其实道理是一样的,就是这样容易理解很多。
直接判断当前串(0,0)是否是包含ABC,那么肯定不包含,所以不包含怎么办,增加元素呗,那就变成(0,1)还是没有,(0,2)、(0,3)
当start=0,end=3的时候,终于,ABBC包含ABC的所有元素了,那么包含之后怎么移动?
那当然是缩短了,从哪里缩短,从前面缩短啊,你想一下,如果这个串是AAAAAAABC,要在长度等于9的时候才能全部包含,那么你会想怎么求最短的子串吗?那很明显就是把前面的A都掐掉,所以就是这个道理
(0,3)->(1,3)
这个时候A少了,不能包含所有元素了,怎么办?找下一个A,一直往下找,直到找到位置
那不是变得很长了吗?我们要找的不是最短的串吗?你用这种方法,过程中遇到的子串加上前一个字符组成的子串,不是比你找到A为止的子串更短吗?
好像是这样的,ABBCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA,从(1,3)开始找的话得到的新的窗口应该是(1,30(猜的)),那还不如(0,4)这样的来得短呢?
其实不是的,因为即使你最短的(0,4)也比不过之前计算出的(0,3)短,所以在我们找到30这个位置的时候,把起点直接设置为30-长度-1=27,也就是说,直接判断(27,30)是否符合要求,不符合直接开始缩减,就继续拿这道题举例子。
当前是(1,3)经过刚才所说的变化变成(1,5),然后又变成了(2,5),判断是否含有所有的子串,有的。。。尴尬,不影响,应该已经能理解了,那么我们在(2,5)中查找是否有更短的可能(也有可能一样长),找到当前子串中最短的符合条件的子串后再继续上述的步骤。
这个问题理解了那么这道题也就理解了。
现在还有最后一个问题,如何记录状态。
如果用上面的窗口前端直接跳转的方法,需要重置当前的所有状态,(你不知道丢失了多少),并且重新开始统计子串中各关键字符的个数。难点在于状态的改变上,能降低时间但是会消耗更多的磁盘。
如果不使用直接跳转的方式,使用一个一个移动的方式,这个的好处在于状态一直连续保持,不需要重置,但是明明可以跳过的位置却要傻傻地记录数据,这会浪费时间。
思来想去还是选择下面那种方法吧。
代码部分:
还有部分错误。
题目的要求比我想的多,字符可以重复,有大小写。需要根据这些请求进行升级
class Solution {
//起始窗口的前端和后端都是0
int start=0;
int end=0;
//标记当前窗口中有几种字符
int flag=0;
//长度和最短位置的起始坐标
int length=0;
int index=0;
//26个英文字母,多一个都没有,一点内存不浪费
int[] ts=new int[60];
public String minWindow(String s, String t) {
if (s=="")
return s;
//起始值设为-1,表示没有这个字符
for(int i=0;i<60;i++){
ts[i]=-1;
}
//给关键元素的位置赋初值
for(int i=0;i<t.length();i++){
//ASCII-65表示下标 A的ASCII=65 -> 0
ts[t.charAt(i)-65]=0;
}
//滑块移到最右边表结束
while (end!=s.length())
{
endMove(s);
startMove(s);
}
start--;
return s.substring(start,end );
}
//窗口后端移动
public void endMove(String s){
for(;end<s.length();end++){
//后端字符是否存在,如果存在
if(ts[s.charAt(end)-65]>=0){
//判断是否为新增的,新增的就让flag+1,当flag=长度时,表示当前字符串符合
if (ts[s.charAt(end)-65]==0){
flag++;
}
//个数增加,为了删减的时候准备的
ts[s.charAt(end)-65]++;
}
//所有字符全都出现,后端移动任务完成
if(flag==s.length())
{
end++;
return;
}
}
}
//窗口前端移动startMove
public void startMove(String s){
for(;start<s.length();start++){
//前端字符是否是核心字符,如果是
if(ts[s.charAt(start)-65]>0){
//对应位置上的个数-1
ts[s.charAt(start)-65]--;
//只要少了一个就代表不满了,直接返回
if (ts[s.charAt(start)-65]==0){
flag--;
//每次后移结束就是为了收束最短子串,所以判断长短
if (length>end-start)
{
length=end-start;
index=start;
}
start++;
return;
}
}
}
}
}
果然是我为了节省内存的原因导致的,题目的意思已经非常明显地把我像使用Map去引导了,我居然剑走偏锋非要用数组去存。
重新搞一个数据结构,用Key存字符,V存个数,不存在表示不是关键字符。
搞完了,前后花了4个半小时左右,原理倒是很早就搞懂了,但是细节部分的处理比较费时间,很容易把自己搞晕,特别是特殊的情况下比较难处理,容易出现下标越界的问题,如果思路清晰还是挺容易的。
这个成绩不算什么厉害的,不过我很满意了,能够自己完成困难难度的算法题。
package month5;
import org.junit.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class minWindow {
@Test
public void test(){
String s1="";
String s2="";
System.out.println(minWindow(s1, s2));
}
//起始窗口的前端和后端都是0
int start=0;
int end=0;
//标记当前窗口中有几种字符
int flag=0;
//长度和最短位置的起始坐标
int length=Integer.MAX_VALUE;
int index=0;
HashMap<Character,Integer> keyMap=new HashMap<Character, Integer>();
public String minWindow(String s, String t) {
if (s=="")
return s;
//给关键元素的位置赋初值,还有个数
for(int i=0;i<t.length();i++){
//如果没出现过,那么置为0
if(keyMap.get(t.charAt(i))==null){
keyMap.put(t.charAt(i), -1);
}
else{
//如果出现过,那就-1
keyMap.put(t.charAt(i),keyMap.get(t.charAt(i))-1);
}
}
//滑块移到最右边表结束
while (end<=s.length()-1)
{
endMove(s);
if (start==s.length()){
break;
}
startMove(s);
}
if (length==Integer.MAX_VALUE)
return "";
return s.substring(index,index+length );
}
//窗口后端移动
public void endMove(String s){
for(;end<s.length();end++){
//后端字符是否存在,如果存在
if(keyMap.get(s.charAt(end))!=null){
//个数增加,为了删减的时候准备的
keyMap.put(s.charAt(end),keyMap.get(s.charAt(end))+1);
//判断这个字符的个数是否达到要求了,新增的就让flag+1,当flag=长度时,表示当前字符串符合
if (keyMap.get(s.charAt(end))==0){
flag++;
}
}
//所有字符全都出现,后端移动任务完成
if(flag==keyMap.size())
{
end++;
return;
}
}
//如果主动跳出循环表示循环到末尾了,而且没有满足子串符合条件,那么可以不执行窗口前端移动的工作了
//直接把窗口前端搬到末尾+1的位置
start=s.length();
}
//窗口前端移动startMove
public void startMove(String s){
for(;start<s.length();start++){
//前端字符是否是核心字符,如果是
if(keyMap.get(s.charAt(start))!=null){
//对应位置上的个数-1
keyMap.put(s.charAt(start),keyMap.get(s.charAt(start))-1);
//只要少了一个就代表不满了,直接返回
if (keyMap.get(s.charAt(start))<0){
flag--;
//每次后移结束就是为了收束最短子串,所以判断长短
if (length>end-start)
{
length=end-start;
index=start;
}
start++;
return;
}
}
}
}
}