别问为啥距离昨天的两数相除,我这么快就又搬了一题上来,问就是我太菜(是真的菜,,咋都这么难)
今天,,其实昨天做的这道题—串联所有单词的子串,一直做到今天上午,最终还是输在了效率上,为啥子我的代码总是运行的这么慢??或许这就是大佬与我这种菜鸡的区别所在
题目介绍
给定一个字符串s和一些长度相等的单词words。找出s中恰好可以由words中所有单词串联形成的子串的起始位置。
注意,子串要与words中的单词完全匹配,中间不能有其他字符,不考虑串联顺序
举例:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
我的思路1
首先说下,我的思路在leetcode中也通过了,只是在效率上,实在是看不过去,刷新了我刷题上运行效率的下限(运行时间在2000左右,内存在38左右)
我的思路比较简单,i指向s的开头,j指向words的第一个元素,利用一个和words同样大小的bool数组表示元素是否已被使用。然后,就是从当前的i向后判断words大小个单词,如果全部符合,就记录下i,否则,就直接break,将i加1。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public List<Integer> findSubstring(String s, String[] words){ int len = words[0].length(); int i = 0; String g = ""; for(int m=0;m<words.length;m++){ g += words[m]; } if(g.equals(s)){ return new ArrayList<>(Arrays.asList(0)); }
Boolean[] tag = new Boolean[words.length]; Arrays.fill(tag,Boolean.FALSE); List<Integer> ans = new ArrayList<>(); while(i<s.length()){ int j = 0; int i_temp = i; while(j<words.length && i+len<=s.length()){ if(s.substring(i,i+len).equals(words[j]) && tag[j]!=true){ tag[j] = true; j = 0; i = i + len; continue; } j++; } if(!Arrays.asList(tag).contains(false)){ ans.add(i_temp); } Arrays.fill(tag,Boolean.FALSE); i = i_temp + 1; } return ans; }
|
其实导致这个算法很慢的原因在于,我对i的移动范围太大,而且我没有一次性移动words的总长度,只是一个单词的长度
优化:可以在(1)后面直接将i到i+len_sum的子串提出来,再利用while循环判断该子串里是否包含了words里的所有单词,是则记录下当前的i,不是就恢复标志数组tag,并跳出while循环,直接将i加1
我的思路2
按照刚才思路一后的优化,加上代替tag数组的HashMap,得出了下面的思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public List<Integer> findSubstring(String s, String[] words){ if(s==null || words==null || s.equals("")){ return new ArrayList<>(Arrays.asList(0)); }
int word_num = words.length; int one_word = words[0].length(); int len_sum = word_num * one_word; List<Integer> ans = new ArrayList<>();
HashMap<String,Integer> map = new HashMap<>();
for(String word : words){ map.put(word,map.getOrDefault(word,0)+1); }
for(int i=0;i<=s.length()-len_sum;i++){ String word_temp = s.substring(i,i+len_sum); HashMap<String,Integer> map_temp = new HashMap<>(map); int j = 0; boolean flag = true; while(j+one_word<=len_sum){ String word = word_temp.substring(j,j+one_word); if(map_temp.containsKey(word) && map_temp.get(word)>0){ map_temp.put(word,map_temp.get(word)-1); j = j + one_word; }else{ flag = false; break; } } if(flag==true){ ans.add(i); } } return ans; }
|
这个思路里,精彩的地方就是利用HashMap记录words里每个单词的个数,便于判断我们截取下来的子串里的单词是否为words里的,以及是否被使用过的(用过一次就将个数减1,直到0)
大佬的思路
关于这种对字符串与其子串的类似的题目,可以想想滑动窗口的方法是否可以用
大致上的思路其实是类似的,只是这个思路的外循环只需要经历单个words里的单词长度,因为这样s里所有的单词都会被访问到
先看代码吧,边看代码边解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public List<Integer> findSubstring(String s, String[] words){ if(s==null || words==null || s.equals("")){ return new ArrayList<>(Arrays.asList(0)); } int one_word = words[0].length(); int word_num = words.length; int len = s.length(); List<Integer> ans = new ArrayList<>();
HashMap<String,Integer> map = new HashMap<>(); for(String word:words){ map.put(word,map.getOrDefault(word,0)+1); } for(int i=0;i<one_word;i++){ int left = i; int right = i; int count = 0; HashMap<String,Integer> map_temp = new HashMap<>(); while(right+one_word<=len){ String word = s.substring(right,right+one_word); right += one_word; map_temp.put(word,map_temp.getOrDefault(word,0)+1); count++; while(map_temp.getOrDefault(word,0)>map.getOrDefault(word,0)){ String word_temp = s.substring(left,left+one_word); map_temp.put(word_temp,map_temp.get(word_temp)-1); left += one_word; count--; } if(count==word_num){ ans.add(left); } } } return ans; }
|
首先,依旧是利用HashMap(map1)将单词的个数记录下来;接着,在循环的内部,首先定义left与right,left是指当前窗口的左边,right自然就是右边,新建一个map2,存放截取的子串里的单词个数(可能会有map1里没有的,没关系,继续看),count是用来记录当前子串里已经加入map2的个数
对当前截取子串的操作:将子串里的单词放进map2,并记录其次数,同时right移动,count++,再将该单词在map2里的次数与map1里比较,如果大于,则表明当前单词在该窗口中要么已经用完了,要么直接map1里没有该单词,于是当前的left-right窗口不行,便移动left,将窗口整体向后移,至于移到什么地方呢?
1 2
| String s = "wordgoodgoodgoodbestword"; String[] d = new String[]{"word","good","best","good"};
|
以这个例子举例说明:当left在0,right移到第三个good的时候,会发现good用完了,那么就需要移动left,自然而然想到,需要将当前窗口移到第三个good刚好将good用完的地方,也就是将left移到第二个good的开头处。
实际上,如果当前的子串单词是map1里没有的,那么就可以直接将left移到right(该没有单词的下一个单词的开头)处,比如说:
1 2
| String s = "wordgoodgaodgoodbestword"; String[] d = new String[]{"word","good","best","good"};
|
当前left在0,right在8,获取”goad”子串后,right=12,发现”goad”在map1里没有,那么直接可以将left移到12
一定要记住:我们的每一轮开始都是有个基准的,比如说,上面这个例子的基准就是当前的i=0,意思就是说,必须从0开始往后每四个是一个单词
结束
以上就是我这个菜鸡在这道题上的一点理解,还是得膜拜各种大佬@—@
(希望明天的题目不至于再让我来写一篇博客记录了)