不是什么算法大牛,只是爱好做算法题的一般社畜。
1、来看看题目
今天是个简单题,所谓简单题我重拳出击,所以本以为今天可以度过一个轻松的周末的。
我对于简单题的要求是:读完题的一瞬间脑内能形成大概的思路
如果你在面试里被面到算法题时,那么第一点要注意的便是时间。面试官不可能给你太多的时间让你长篇大论,所以你必须能尽快地得出解决方案,哪怕那个方案并非完美。
2、审题
题目很长,但很容易就看出是个词频统计的题目。
总结下来大概有这么些要点:
- 目标字符串licensePlate包含了 数字 和 空格,但补全词仅包含 小写字母
- licensePlate包含有大写字母,但词频统计会忽略大小写
- 同一个字母在licensePlate会出现多次,且补全词要求该字母的出现次数必须大于等于licensePlate中出现的次数
- 符合条件的补全词可能存在多个,此时返回最先遇到的最短的补全词
3、思路
今天的简单题并没有什么陷阱。
我们分别统计licensePlate的词频,并遍历words中提供的候选词,判断该词是否满足要求,记录并返回最先遇到的最短补全词。
思路简单明确,辣么开工!
4、撸代码
class Solution {
public String shortestCompletingWord(String licensePlate, String[] words) {
Map<Character, Integer> source = split(licensePlate);
String minWord = null;
for (String word : words) {
Map<Character, Integer> target = split(word);
if (!match(source, target)) {
continue;
}
if (minWord == null) {
minWord = word;
} else {
minWord = word.length() < minWord.length() ? word : minWord;
}
}
return minWord;
}
//统计词频
private Map<Character, Integer> split(String word) {
Map<Character, Integer> map = new HashMap<>();
for (char c : word.toCharArray()) {
if (Character.isLetter(c)) {
char t = Character.toLowerCase(c);
map.put(t, map.getOrDefault(t, 0) + 1);
}
}
return map;
}
//比较词频
private boolean match(Map<Character, Integer> source, Map<Character, Integer> target) {
for (Map.Entry<Character, Integer> entry : source.entrySet()) {
if (!target.containsKey(entry.getKey())) {
return false;
}
if (target.get(entry.getKey()) < entry.getValue()) {
return false;
}
}
return true;
}
}
以上是我撸出来的原始版本。
我做题的习惯在于保留完整的思路,所以代码尽量保证了可读性,没有刻意去精简。
5、解读
由于题目简单,撸代码时的思路也很明确,所以基本上流程就是顺着思路来的。
在split()
方法中,我统计了各个单词的词频,以Map的形式存储。注意我在审题中提到的,licensePlate中会出现扰乱我们的数字和空格,同时也要注意大小写问题。
所以在遍历字符串中的每个字符时,使用Character.isLetter()
方法来判断是当前字符是否是字母,同时用Character.toLowerCase()
方法来将大写字母转换为小写。
适当地依赖高级语言提供的遍历的api和函数库吧
在match()
方法中,我比较了当前单词和licensePlate的词频。由于题目规定补全词必须包含licensePlate中出现的所有字母,故在比较时,我循环licensePlate的所有词频,并比较模板单词是否包含此字母,并且此字母的出现次数大于等于licensePlate中出现的次数。
最后便是主函数shortestCompletingWord()
,我以split()
方法获取了licensePlate的词频,并循环words中出现的所有单词,在统计其词频后,以match()
方法进行比较。
如果比较通过,则以minWord记录。
由于当存在多个何时的补全词时,我们应返回最先遇到的最短的补全词,故比较minWord与目标单词长度时,只有后出现的单词长度<minWord.length()
时买才更新minWord。
6、提交
在我成功地通过了题目提供的几个case后,我点击了提交,得到的结果却让我有些惊讶。
我知道以我的水平很难在第一反应下做出最优解,可这个结果显然证明我连次优解也算不上。
于是我开始分析自己的代码。
7、咀嚼
由于我做题的思路普遍是空间换时间,所以很少在意内存消耗,尽量提高时间消耗。
但很明今天的题目是截然相反的。
我开始计算起时间复杂度:
在计算词频时,我对每个单词的每个字母都遍历了一遍,复杂度为O(N )。
在比较时,由于licensePlate出现的字母数量不定(0 - 26),以Σ计。
所以整体的时间复杂度为O(N + M*Σ)
统计词频这一步应该都无法绕开,所以我觉得更优的解估计出在比较上。
辣么是否存在耗时为O(1) 的方法呢?或许有,但我不知道
8、他人的智慧
由于今天也是工作日,所以即使我知道问题可能存在的地方,也没有过多的时间取思考。
所以在粗略想破了脑袋后,就决定去题解区吸取一下他人的智慧了。
能力进步全靠抄
官解的思路与我大同小异。
几位上镜率很高的大牛,思路也相差不大。
直到我翻到了一个号称双百的大牛的题解。
9、总结
大牛的题解
仿佛粗壮的牛子强行捅进了我的天灵盖,从而拓宽了我的思路
优秀的解法当然值得我们记住。
但同时也要牢记一点:时间
如果你刷题纯粹是处于兴趣,辣么另当别论。
但假如你抱有一点为了面试而准备的想法的话,就请时刻记得:
最好的题解是最你的大脑最快速简单地给出的题解。它当然必须兼具一些便于实现的特点,但同时也最好是最能让你调理清晰地得出结题步骤。
勤能补拙,共勉共勉