一、引言
这道题题意非常简单,却让我思考了很久,或许难的并不是这道题,是纠结于解题思路的那种困扰吧。
这里是这道题:
Given a string, find the first non-repeating character in it and return it’s index. If it doesn’t exist, return -1.
Examples:
s = “leetcode”
return 0.s = “loveleetcode”
return 2.
简单翻译一下:
给定一个字符串,请找到第一个未重复出现的字符并返回它的下标,如果没有找到这样的字符,那么久返回 -1
看题而言,这道题还是比较简单的,那么这道题该怎么做呢?
二、适合 C++11 的思考
首先让我们分析这道题:
首先,我们需要辨别字符是否重复出现,那么可否使用异或呢?答案是不可以,为什么呢?因为字符有可能重复 2 次以上,这不符合异或的使用范围。那么只能使用 std::map 或者 std::unordered_map 之类的映射来存储这个“字符 - 出现次数”的映射关系了
其次,我们还需要辨别是否是第一个未重复出现的字符。那么为了拿到这个第一个,我们肯定要通过某种方法来找到这第一个未重复的字符,可以是遍历原数组,也可以从记录有字符的出现位置的地方读取
根据上述的思路,我写出了第一个版本的代码:
// my first solution , runtime = 76 ms
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<char, int> char_map;
char unique = '\0';
for (auto c : s) ++char_map[c];
for (auto c : s)
if (char_map[c] == 1) {
unique = c;
break;
}
return s.find(unique);
}
};
简要阐述下这段代码:
首先,我选择了 std::unordered_map 来记录“字符 - 出现次数”的映射关系,为了得到这个映射关系,我使用了第一个范围 for 循环了原字符串
然后,为了拿到“第一个”这一条件,我又对原字符串进行了第二次遍历,在这次遍历中,我首先判断当前字符的出现次数是否为 1,然后满足条件的话,则记录这个字符的值
最后,我使用了 STL 的函数 std::find 来返回指定字符的位置。这个函数会返回指定范围内满足条件的第一个元素的位置,如果没有找到的话则返回 std::nPos (默认为 -1)。刚好切合题意的一个标准库函数 : )
这一个方法比较简单,但是我有些不满意,总觉得我遍历了两次原数组,怎么都觉得有些浪费,其实一次遍历就可以同时记录指定字符的位置了的。
于是就有了第二个版本的代码:
// my second solution , runtime = 86 ms
class Solution2 {
public:
int firstUniqChar(string s) {
unordered_map<int, pair<int, int>> char_map;
int index = s.size();
for (int i = 0; i < s.size(); ++i) {
char_map[s[i]].first++;
char_map[s[i]].second = i;
}
for (auto item : char_map) {
if (item.second.first == 1) {
index = min(index, item.second.second);
}
}
return index == s.size() ? -1 : index;
}
};
这段代码呢,思路有一点点小小的变化:
为了不对原数组进行第二此遍历,我将所有字符的位置信息也存储下来了,也就是说在数据结构
unordered_map<int, pair<int, int>> char_map
中,我将“字符”看作了键,pair<int, int>
结构看作了值,然后这个包含了两个int
元素的值中第一个元素存储了元素的出现次数,第二个元素则存储字符的出现位置另外,注意看
index
变量的用法,首先初始化为s.size()
也就是字符串的长度(index 是永远不可能取到这个值的,因为下标最大值总是比长度小 1,这里故意选了一个最大值作为初始值),然后我们在遍历完了原数组得到了需要的数据之后,对我们的映射关系记录进行遍历,当找到字符的出现次数为 1 的时候,再取这个字符的出现位置进行记录,但是这个记录只记录最小的出现位置最后,我们根据 index 值是否有过更改返回 -1 或者 index 原值
这个方法可以说还是比较优雅的,可能逻辑上复杂了点导致代码不如第一个方法好看了。不过就我而言,我还是觉得第二个方法更适合我这种逻辑洁癖 :)
三、高票答案一日游
当然了,做完了一道 LeetCode 上面的题你可能只收获了一半,还有一半在高票答案中需要你自己去挖掘,这里我们就来看看高票答案的奇妙。
看到了最高票答案是 Java 版本的,不过没关系,解题思路比语言区别更重要(我个人是使用 C++11 的):
这里单独把他的代码贴出来:
public class Solution {
public int firstUniqChar(String s) {
int freq [] = new int[26];
for(int i = 0; i < s.length(); i ++)
freq [s.charAt(i) - 'a'] ++;
for(int i = 0; i < s.length(); i ++)
if(freq [s.charAt(i) - 'a'] == 1)
return i;
return -1;
}
}
这是一个非常奇妙的思路,这里我们不看作者的解释,直接看代码吧:
首先,作者声明了一个拥有 26 个位置的整型数组(char 类型的本质其实就是整型数字),那么这个数组是用来干嘛的呢?让我们看接下来的代码
然后,作者对原数组进行了第一次遍历(这是必然的,只有遍历才能获得足够的信息),关键就是作者是如何记录相关信息的;这里作者读取了遍历的字符,然后将该字符与
a
进行相减,将freq
数组指定此值位置上的值递增;那么这里到底是什么意思呢?其实字符的值与a
相减就可以取到字符与a
的位置,那么 0 ~ 25 也就对应了小写字母 a ~ z;也就是说下标对应字母,值对应了出现次数;作者没有使用 map ,仅仅使用了 一个数组就完成了这一映射关系的记录,非常非常妙!!!最后,作者通过第二次遍历原数组,找到了第一个出现次数为 1 的字符,对其位置进行了返回;没有找到的话返回 -1
可以说这个方法的精髓就在于所需信息的存储方式,我之前思考的方式都是使用 map ,但是作者使用数组来模拟这个映射关系的思路非常非常的巧妙,不得不让人称赞!
四、总结
有人说,LeetCode 上面的题目这么简单,有必要做吗?
又有人说,LeetCode 上面都是些题而已,对我们提升自己有用吗?
或许还有人说,你就是 LeetCode 刷完了,你又能干嘛,最多是个只会做题的机器。
而我说:
题目仅仅只是题目而已,真正的收获并非答案,而是做出答案的过程,或查找资料,或绞尽脑汁思考多日无果,或查看 Discuss 区域借鉴他人的方法,这些都是收获
LeetCode 还会继续,而我不会忘了自己还是个菜鸟的事实:
To be Stronger!