2351. 第一个出现两次的字母
LeetCode: 2351. 第一个出现两次的字母
简单 \color{#00AF9B}{简单} 简单
给你一个由小写英文字母组成的字符串
s
,请你找出并返回第一个出现 两次 的字母。注意:
- 如果
a
的 第二次 出现比b
的 第二次 出现在字符串中的位置更靠前,则认为字母a
在字母b
之前出现两次。s
包含至少一个出现两次的字母。
示例 1:
输入:s = "abccbaacz"
输出:"c"
解释:
字母 'a' 在下标 0 、5 和 6 处出现。
字母 'b' 在下标 1 和 4 处出现。
字母 'c' 在下标 2 、3 和 7 处出现。
字母 'z' 在下标 8 处出现。
字母 'c' 是第一个出现两次的字母,因为在所有字母中,'c' 第二次出现的下标是最小的。
示例 2:
输入:s = "abcdd"
输出:"d"
解释:
只有字母 'd' 出现两次,所以返回 'd' 。
提示:
2 <= s.length <= 100
s
由小写英文字母组成s
包含至少一个重复字母
方法1:哈希表
我们可以将每一个字符映射到哈希表中。初始状态下,每一个字符键所对应的值都为 false
。
开始遍历字符串中的字符。每次遍历到后,将该字符对应的哈希表值设为 true
。
如何来确定某个字符已经是第二次出现了呢?实际上,我们需要在设置 true
之前,对哈希表值进行判断。如果在设置之前已为 true
,则代表在之前的循环中,这个字符已经出现了,已经把对应位置设置为 true
了,所以这次遍历我们才能得到一个真值。这种情况下,该字符就是第二次出现了。
我们也可以将哈希表的记录类型设置为整型,以记录字符的出现次数。当某字符的计数为 2
时,就代表该字符第二次出现。
(没有那么重要)
需要注意的是:由于题目所给条件中指明,每个字符串都至少包含一个重复字符。也就是说,我们在循环的过程中一定能够找到目标字符,这时我们就可以直接返回结果了。但是为了确保函数的完整,我们在循环外仍然需要“虚空”返回一个值,确保函数的“每一条路”都能够返回char
类型的值。
在下面的例子中,便是最后的那句 “return 0;”
下面是布尔类型的哈希表实现。
class Solution {
public:
char repeatedCharacter(string s) {
bool hashmap[26] {false};
for (const char& c : s)
{
int code = c - 'a';
if (hashmap[code] == true)
return c;
hashmap[code] = true;
}
return 0;
}
};
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n)。其中,
n
为字符串s
的长度。 -
空间复杂度: O ( ∣ Σ ∣ ) O(|Σ|) O(∣Σ∣)。|Σ|为
字符集
的大小,本题中为26长的小写英文字母集。
参考结果
Accepted
92/92 cases passed (0 ms)
Your runtime beats 100.00 % of cpp submissions
Your memory usage beats 90.15 % of cpp submissions (5.9 MB)
方法2:位图(哈希表)<int类型实现>
在方法1中,哈希表的值类型是 bool
(或 int
之类)。实际上,对于本题的条件,对于一个字符要记录的东西只有“它出现过”和“它没有出现过”。这是一个只有2种结果的状态。那么计算机中,1位二进制的 0
和 1
便可以存储,何必使用占据 8
位的 bool
类型或 32
位的 int
类型呢?
因此,我们可以选择将一个字符映射到一个bit上。对于本题中给出的字符集,其为26个小写英文字母。也就是说我们实现本题的要求至少需要 26位
的空间去做哈希表。自然而然,C++中能够存放至少26位的最佳选择,那么就是占据 32
位空间的 int
类型变量了。
我们可以用一个 int
类型变量的最低位,也就是第 0 位,去存放字符 ‘a’ 的状态。当第0位为二进制 0
时,该字符还未出现过;当其为二进制 1
时,则该字符已经出现过。
那么如何去实现从 字符
到 位
的映射呢?首先我们要知道某个字符在字符集中的顺序索引,例如字符 ‘a’ 映射到第 0 位、字符 ‘b’ 映射到第 1 位。这个数值可以通过 c - 'a'
来得到。
接下来就是如何在位图中定位。我们可以通过移位操作和与操作来获取某字符键所对应的哈希表值。例如现在有一个无符号整型变量 bitmap
,并且我们需要知道字符 ‘a’ 的状态。那我们可以将变量 bitmap
与 01H
(0000 0001B
)作与操作,将高位的31位全部置1,将第0位保留。如果这个结果是 0
,则代表字符 ‘a’ 对应的哈希表值是 0
,也就是它还未出现过;如果结果不是 0
,则代表字符 ‘a’ 对应的哈希表值为 1
,也就是它已经出现过了。
同样地,如果要获取字符 ‘b’ 的状态,那么将 bitmap
与 02H
(0000 0010B
)作与操作即可。而字符与哈希表之间映射的表达式就为:
1 << (c - 'a')
即将整型的 0000 0001H
(32位)向左移 c - 'a'
位,即可得到该字符对应的哈希表索引。
#include <cstdint>
class Solution {
public:
char repeatedCharacter(string s) {
uint32_t bitmap = 0;
for (const char& c : s)
{
uint32_t position = 1 << (c - 'a');
if ((bitmap & position) != 0)
return c;
bitmap |= position;
}
return 0;
}
};
注意:
- 上述代码中包含了头文件
cstdint
是为了使用类型uint32_t
。这个类型实际上就是unsigned int
,可以在stdint.h
文件中看到其定义:
–typedef unsigned int uint32_t;
而使用uint32_t
这个写法只是为了说清该变量占据32
位。除此之外没有什么特别的含义。 位运算符
的优先级低于逻辑运算符
。所以在if
语句的判断条件中千万不要写成这样:
–bitmap & position != 0
它的运算实际上等同于bitmap & (position != 0)
- 从上面的思路其实可以看出,
位图
实际上就是一种哈希表
。
复杂度分析(同方法1)
-
时间复杂度: O ( n ) O(n) O(n)。其中,
n
为字符串s
的长度。 -
空间复杂度: O ( ∣ Σ ∣ ) O(|Σ|) O(∣Σ∣)。|Σ|为
字符集
的大小,本题中为26长的小写英文字母集。
方法3:位图(哈希表)<bitset类型实现>
也许很多人可能不太喜欢去思考如何实现从 元素
到 位图索引
的映射。
假设我们的字符集不是只有26个字母的小写英文字母集合,而是一个更大的,例如100个字符的集合。我们不好找一个变量去一次性覆盖100位,也没有办法把 0000 0001H
向左移99位。这种情况下或许我们会想到用一个存放 char
类型(8位)的数组去存放位图。这样这个数组就有 ⌈ 100 / 8 ⌉ = 13
的长度。然后我们再在这个条件下去实现映射(哈希函数)。
可能很多人都觉得这样去思考实在有点复杂。如果位图能像数组一样用下标去访问就会简单许多。想要访问第99位的话,我就直接传99这个数字进去。这里就可以用到C++标准库中提供的 bitset
类型了。
该类型可以通过包含头文件 bitset
使用。其具体的说明详见cppreference/bitset。
不妨先来看下代码:
#include <bitset>
class Solution {
public:
char repeatedCharacter(string s) {
std::bitset<26> hashmap {0};
for (const auto& c: s)
{
if (hashmap[c - 'a'] == true)
return c;
hashmap[c - 'a'] = true;
}
return 0;
}
};
代码中,我们首先定义了一个 26
定长的bitset。在随后的遍历中我们可以看到,我们就是把 c - 'a'
,也就是 c
是第几个字符,像访问数组一样作为下标去访问。我们也没有去实现特别的映射关系。
如果不使用 []运算符重载
的话,也可以用其它bitset提供的成员函数去实现。效果上是一样的。
#include <bitset>
class Solution {
public:
char repeatedCharacter(string s) {
std::bitset<26> hashmap {0};
for (const auto& c: s)
{
if (hashmap.test(c - 'a'))
return c;
hashmap.set(c - 'a');
}
return 0;
}
};
复杂度分析(同方法1)
-
时间复杂度: O ( n ) O(n) O(n)。其中,
n
为字符串s
的长度。 -
空间复杂度: O ( ∣ Σ ∣ ) O(|Σ|) O(∣Σ∣)。|Σ|为
字符集
的大小,本题中为26长的小写英文字母集。