找出唯一字符

题目描述

来自LeetCode第387题:字符串中的第一个唯一字符

给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。 s只包含小写字母。

图片:

alt text

分析

整体思路是遍历每一个字符,统计每个字符出现的次数,然后再遍历一遍字符串,找到第一个出现次数为1的字符。

因此我们需要一个数组,用来:统计26个字母出现的次数。 第二次遍历的时候,从左到右查看每个字符是否只出现了一次。

按照这个逻辑,这个题天然地适合用哈希表来做。每个字符的ASCII码值可以作为哈希表的索引,出现的次数作为值。 那么哈希生成函数就是:

1
2
3
hash(char c) {
    return 'c' - 'a';
} 

这时候,就可以用一个长度为26的数组来模拟哈希表。

代码实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int firstUniqChar(char* s) {
    int times[26];
    int len = strlen(s);
    for (int i=0;i<len;i++){
        times[s[i]-'a']++;
    }
    for(int i=0;i<strlen(s);i++){
        if (times[s[i]-'a']==1){
            return i;
        }
    }
    return -1;
}

问题延申

原题是非常简单的,只要求第一次的索引,所以代码也无需优化,第二次遍历原字符串即可。 但是如果题目变种一下,要求返回所有只出现一次的字符呢? 注意,这里改成了只出现的第一个的字符,而不是它的索引。

新的问题用原来的逻辑也是可行的,但是效率不高。 因为第二次遍历原字符串时,可能会多次查找同一个字符的出现次数。 假设原字符串很大,那么第二次遍历的复杂度与第一次相当。

那么第二次遍历,如何如何防止对一个字符多次查表呢? 因为第一次遍历是从左到右(当然也可以从右到左),第二次遍历也是从左到右,第一次遍历的字母种类,第二次也要查一次。 那么我们可以用一个队列来存储第一次遍历时,出现过的字母。 第二次遍历时,无需遍历原字符串s,而是遍历队列中的字母。

代码实现

无队列版:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int firstUniqChar(char* s) {
    int times[26];
    int len = strlen(s);
    for (int i=0;i<len;i++){
        times[s[i]-'a']++;
    }
    for(int i=0;i<strlen(s);i++){
        if (times[s[i]-'a']==1){
            return i;
        }
    }
    return -1;
}

加入队列版:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int firstUniqChar(char* s) {
    int times[26];
    int queue[26];
    int last = -1; // 队列尾指针
    int len = strlen(s);
    for (int i=0;i<len;i++){
        times[s[i]-'a']++;
        if (times[s[i]-'a']==1){
            queue[++last] = s[i]-'a'; // 入队
        }
    }
    for(int i=0;i<=last;i++){
        if (times[queue[i]]==1){
            return queue[i]+'a'; // 返回字符
        }
    }
    return -1;
}

复杂度分析

原题的时间复杂度是O(n),空间复杂度是O(1)。

延申题的时间复杂度也是O(n),空间复杂度是O(1)。

不过,延申题使用队列后,第二次遍历的次数变少了。 以后遇到要两次遍历,前后的顺序相同时,为了避免重复,可以考虑用队列。 毕竟,队列是先进先出。

使用 Hugo 构建
主题 StackJimmy 设计