一、题目
3. 无重复字符的最长子串。难度:中等。
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列, 不是子串。
更多细节请参看官方题目详情。
二、题解
2.1 思路分析
从左向右遍历字符串的每一位,找不含重复元素的最长子串。
拿题目中的s = "pwwkew"举例说明
(1)start=0,end=0
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
维护两个变量 start 和 end ,他们可以在每个字符上滑动,在滑动的过程中,我们去找最长的无重复元素的子串。他们的初始位置都在第0个元素上,这里是p,此时不含有重复字符的 最长子串 为 p, 其长度为1【思考代码层面:ans=end-start+1】。
(2)end++;start=0,end=1
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
将 end 向后移动,可以知道 p、w都只出现了一次,且连续, 此时不含有重复字符的 最长子串 为 pw, 其长度为2【思考代码层面:ans=end-start+1】。
(3)end++;start=0,end=2
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
继续将 end 向后移动,判断新的字符是否重复出现,是,可以知道新的(也就是end)位置上的元素 w 重复出现了,与上一个字符重复,所以此时最新的子串应该从当前位置开始重新计算,更新start,新的start的位置应该是重复位的下一位,同时保存历史最长子串的长度2,与未来的最长子串的长度比较选取较大的值为最终的结果。
更新如下
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
当前位置不含有重复字符的 最长子串 为 pw, 其长度为2。继续向后遍历。
(4)end++;start=2,end=3
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
继续将 end 向后移动,判断新的字符是否重复出现,否,此时不含有重复字符的 最长子串 为原来的 pw 或者现在的wk, 其长度均为2【思考代码层面:ans = max{ans,end-start+1}】。
(5)end++;start=2,end=4
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
继续将 end 向后移动,判断新的字符是否重复出现,否,此时不含有重复字符的 最长子串 为 wke, 其长度为3【思考代码层面:ans = max{ans,end-start+1}】。
(6)end++;start=2,end=5
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
继续将 end 向后移动,判断新的字符是否重复出现,是,可以知道新的(也就是end)位置上的元素 w 重复出现了,与第2个位置处的字符重复,更新start,新的start的位置应该是重复位的下一位,所以需要更新start为3。
更新如下
| p | w | w | k | e | w |
|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 |
| start | |||||
| end |
(7)end++;start=2,end=6
end后移,超出字符串最大长度,遍历结束,返回最长字串长度3
2.2 代码思路分析及优化
以上就是这道题的总体思路了,接下来我们就要思考代码层面,如何达到效率最高?
值得我们重点考虑的就是,如何判断新的位置的字符是否重复出现?和什么位置重复?进而准确更新start。一般是否重复的问题我们会想到map、hashmap;那么start的位置呢?由上面的分析我们可以知道,start的位置取决于重复元素上一次出现的位置,然后再+1,所以我们应该记录重复元素上一次出现的位置,所以我们可以用 hashmap 的 value 来记录每个字符上一次出现的位置,这里起名为 characterPosMap。start和end及时更新即可。
ans=0; (1)start=0,end = 0;ans = max{ans,end- start + 1 = 1} =1;更新map如下所示
| key | p |
|---|---|
| value | 0 |
(2)end++,start=0,end = 1;ans = max{ans,end- start + 1 = 2}=2;更新map如下所示
| key | p | w |
|---|---|---|
| value | 0 | 1 |
(3)end++,start=0,end = 2;w 重复,更新 start = 重复的位置+1=characterPosMap[w]+1=1+1=2;更新map如下所示
| key | p | w |
|---|---|---|
| value | 0 | 2 |
(4)end++,start=2,end = 3;ans = max{ans,end- start + 1 = 2}=2;更新map如下所示
| key | p | w | k |
|---|---|---|---|
| value | 0 | 2 | 3 |
(5)end++,start=2,end = 4;ans = max{ans,end- start + 1 = 3}=3;更新map如下所示
| key | p | w | k | e |
|---|---|---|---|---|
| value | 0 | 2 | 3 | 4 |
(6)end++,start=2,end = 5; 新的w是重复的,start = 重复的位置+1=characterPosMap[w]+1=2+1=3;更新w的位置为新的位置5,具体更新map如下所示
| key | p | w | k | e |
|---|---|---|---|---|
| value | 0 | 5 | 3 | 4 |
(7)end++,end=6,结束,return ans = 3;
2.3 C++ 相关语法
2.3.1 哈希表
前面已经介绍过了,可以自行阅读学习。
2.3.2 字符串 string 的元素访问
使用字符串类的成员函数 operator[] 或 at 来访问 C++ 中的字符串对象中的每个字符。
(1)operator[]
operator[] 可以像访问数组元素一样,使用下标来访问字符串中的字符,如下所示:
cppCopy code
std::string str = "hello";
for (int i = 0; i < str.size(); i++) {
char ch = str[i];
// 使用 ch 进行操作
}
(2)at
at 函数与 operator[] 函数类似,但会执行边界检查,以确保不会越界访问字符串。下面是使用 at 的示例:
cppCopy code
std::string str = "hello";
for (int i = 0; i < str.size(); i++) {
char ch = str.at(i);
// 使用 ch 进行操作
}
(3) count
我们经常使用 count(key) > 0 的方式来判断哈希表中是否有当前的元素,true就是存在,false就是不存在。
2.4 题目代码
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> characterPosMap;
int start = 0, end = 0;
int ans = 0;
for (int i = 0; i < s.size(); i++)
{
if (characterPosMap.count(s.at(i)) > 0)
{
start = characterPosMap[s.at(i)] + 1;
end++;
characterPosMap[s.at(i)] = i;
}
else
{
characterPosMap.insert(make_pair(s.at(i),i));
ans = max(ans, end - start + 1);
end++;
//characterPosMap.emplace(s.at(i), i);
}
}
return ans;
}
};
但这段代码,对于例子 "tmmzuxt" 是有问题的,我们的代码的结果是4,预期是5。原因是对于最后一个字符t在历史中是出现过的,但是并不在当前字符串中所以当前字符串的长度还可以再增加1,所以对于if (characterPosMap.count(s.at(i)) > 0)这个的分支,我们还应该判断当前字符是否存在于[begin,end]中,重复字符不在这个中,是需要更新 ans 的长度的。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> characterPosMap;
int start = 0, end = 0;
int ans = 0;
for (int i = 0; i < s.size(); i++)
{
if (characterPosMap.count(s.at(i)) > 0)
{
if(characterPosMap[s.at(i)] < start)
{
ans = max(ans, end - start + 1);
end++;
characterPosMap[s.at(i)] = i;
}
else
{
start = characterPosMap[s.at(i)] + 1;
end++;
characterPosMap[s.at(i)] = i;
}
}
else
{
characterPosMap.insert(make_pair(s.at(i),i));
//characterPosMap.emplace(s.at(i), i);
ans = max(ans, end - start + 1);
end++;
}
}
return ans;
}
};
使用 characterPosMap.insert(make_pair(s.at(i),i));
使用 characterPosMap.emplace(s.at(i), i);
总体相差不大,可忽略。
三、总结
| 题目 | 知识点 | 进度 |
|---|---|---|
| 136 | 数组、位运算(异或)、哈希表 | 1 |
| 121 | 数组、动态规划 | 2 |
| 3 | 字符串、滑动窗口、哈希表 | 3 |