3. 无重复字符的最长子串(C++题解含VS可运行源码)
1.力扣C++源码
class Solution {
public:
int lengthOfLongestSubstring(string s) {
map<char, int> hash;
int ans = 0;
int i = 0;
int n = s.length();
for (int j = 0; j < n; j++) {
if (hash.find(s[j]) != hash.end()) {
i = max(hash.find(s[j])->second + 1, i);
}
hash[s[j]] = j;
ans = max(ans, j - i + 1);
}
return ans;
}
};
2.题解
(1)解题思路:滑动窗口
我们不妨以示例一中的字符串 abcabcbb 为例,找出从每一个字符开始的,不包含重复字符的最长子串,那么其中最长的那个字符串即为答案。对于示例一中的字符串,我们列举出这些结果,其中括号中表示选中的字符以及最长的字符串:
- 以 a(b)cabcbb 开始的最长字符串为 a(bca)bcbb;
- 以 ab©abcbb 开始的最长字符串为 ab(cab)cbb;
- 以 abc(a)bcbb 开始的最长字符串为 abc(abc)bb;
- 以 abca(b)cbb 开始的最长字符串为 abca(bc)bb;
- 以 abcab©bb 开始的最长字符串为 abcab(cb)b;
- 以 abcabc(b)b 开始的最长字符串为 abcabc(b)b;
- 以 abcabcb(b) 开始的最长字符串为 abcabcb(b)。
发现了什么?如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!这里的原因在于,假设我们选择字符串中的第 k 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 rk 。那么当我们选择第 k+1 个字符作为起始位置时,首先从 k+1 到 rk 的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 rk ,直到右侧出现了重复字符为止。
这样一来,我们就可以使用滑动窗口来解决这个问题了:
- 我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 rk ;
- 在每一步的操作中,我们会将左指针向右移动一格,表示
我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着
以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度; - 在枚举结束后,我们找到的最长的子串的长度即为答案。
判断重复字符
在上面的流程中,我们还需要使用一种数据结构来判断 是否有重复的字符,常用的数据结构为哈希表(即 C++中的map)。在左指针向右移动的时候,我们从哈希表中移除一个字符,在右指针向右移动的时候,我们往哈希表中添加一个字符。
(2)方法一:滑动窗口
我们使用 哈希表 将字符存储在当前窗口 [i, j)(最初 j = i)中。 然后我们向右侧滑动索引 j,如果它不在 哈希表 中,我们会继续滑动 j。直到 s[j] 已经存在于 哈希表 中。此时,我们找到的没有重复字符的最长子字符串将会以索引 i 开头。如果我们对所有的 i 这样做,就可以得到答案。即,当 s[j] 已经存在于 哈希表 中时,清空哈希表,然后将 i 向后移一位再重复上述操作。
map<char, int> hash;//定义一个hash表
int ans = 0;
int i = 0;//左指针
int j = 0;//右指针
int n = s.length();//字符串长度
while (i < n && j < n) {
//hash.find()的用法:
//关键字查询,找到则返回指向该关键字的迭代器,否则返回指向end的迭代器
//hash.end(),指向最后一个元素的下一个位置
if (hash.find(s[j]) == hash.end()) {
//hash[s[j++]] = j; // 等价于hash[s[j]]=j;j++; j自增必须要在下一条语句中的j-i之前
hash[s[j]] = j;//将右指针j出的元素加入哈希表
j++;
ans = max(ans, j - i);//最大值函数max(,)
}
else {
//hash.erase(s[i++]); // 根据键值删除哈希表中元素。该条语句等价于hash.erase(s[i]);i++; 每回向右滑动一位(此处可以改善,见方法三)
hash.erase(s[i]); //删除左指针i指向的元素
i++;
}
}
return ans;
(3)方法二:优化的滑动窗口
基于上一个方法,当我们找到重复的字符时,我们可以立即跳过该窗口。也就是说,如果 s[j]在 [i, j)范围内有与 j’重复的字符,我们不需要逐渐增加 ii。 我们可以直接跳过 [i,j’] 范围内的所有元素,并将 i变为 j’ + 1。
map<char, int> hash;
int ans = 0;
int n = s.size();
for (int i = 0, j = 0; j < n; j++) {
if (hash.find(s[j]) != hash.end()) {
//i = max(hash.find(s[j])->second + 1, i); //当发现重复字符时,i直接跳到重复字符的下一位,改进了方法2中一个字符一个字符滑动窗口的方法
//i = hash.find(s[j])->second + 1;//错误,要和i比较去最大值
//之所以要max(,),因为已经存在的元素并没有清除掉,仍然存在哈希表中
//如果这个元素在指针i之前,那么i就又跳到前面去了,长度(j-i+1)就长了,结果就错了
}
hash[s[j]] = j;
ans = max(ans, j - i + 1);
}
return ans;
3.VS可运行源码
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<map>
#pragma warning(disable:4996)
using namespace std;
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//方法二:滑动窗口
//我们使用 哈希表 将字符存储在当前窗口[i, j)(最初 j = i)中。
//然后我们向右侧滑动索引 j,如果它不在 哈希表 中,我们会继续滑动 j。
//直到 s[j] 已经存在于 哈希表 中。
//此时,我们找到的没有重复字符的最长子字符串将会以索引 i 开头。
//如果我们对所有的 i 这样做,就可以得到答案。
//即,当 s[j] 已经存在于 哈希表 中时,清空哈希表,
//然后将 i 向后移一位再重复上述操作。
//map<char, int> hash;//定义一个hash表
//int ans = 0;
//int i = 0;//左指针
//int j = 0;//右指针
//int n = s.length();//字符串长度
//while (i < n && j < n) {
// //hash.find()的用法:
// //关键字查询,找到则返回指向该关键字的迭代器,否则返回指向end的迭代器
// //hash.end(),指向最后一个元素的下一个位置
// if (hash.find(s[j]) == hash.end()) {
// //hash[s[j++]] = j; // 等价于hash[s[j]]=j;j++; j自增必须要在下一条语句中的j-i之前
// hash[s[j]] = j;//将右指针j出的元素加入哈希表
// j++;
// ans = max(ans, j - i);//最大值函数max(,)
// }
// else {
// //hash.erase(s[i++]); // 根据键值删除哈希表中元素。该条语句等价于hash.erase(s[i]);i++; 每回向右滑动一位(此处可以改善,见方法三)
// hash.erase(s[i]); //删除左指针i指向的元素
// i++;
// }
//}
//return ans;
//自己写一遍:
/*map<char, int> hash;
int ans = 0;
int i = 0;
int j = 0;
int n = s.length();
while (i < n && j < n) {
if (hash.find(s[j]) == hash.end()) {
hash[s[j]] = j;
j++;
ans = max(ans, j - i);
}
else {
hash.erase(s[i]);
i++;
}
}
return ans;*/
//方法三:优化的滑动窗口
//基于上一个方法,当我们找到重复的字符时,我们可以立即跳过该窗口。
//也就是说,如果 s[j]在[i, j)范围内有与 j’重复的字符,
//我们不需要逐渐增加 ii。
//我们可以直接跳过[i,j’] 范围内的所有元素,并将 i变为 j’ + 1。
map<char, int> hash;
int ans = 0;
int n = s.size();
for (int i = 0, j = 0; j < n; j++) {
if (hash.find(s[j]) != hash.end()) {
//i = max(hash.find(s[j])->second + 1, i); //当发现重复字符时,i直接跳到重复字符的下一位,改进了方法2中一个字符一个字符滑动窗口的方法
//i = hash.find(s[j])->second + 1;//错误,要和i比较去最大值
//之所以要max(,),因为已经存在的元素并没有清除掉,仍然存在哈希表中
//如果这个元素在指针i之前,那么i就又跳到前面去了,长度(j-i+1)就长了,结果就错了
}
hash[s[j]] = j;
ans = max(ans, j - i + 1);
}
return ans;
//自己写一遍:
/*map<char, int> hash;
int ans = 0;
int i = 0;
int n = s.length();
for (int j = 0; j < n; j++) {
if (hash.find(s[j]) != hash.end()) {
i = max(hash.find(s[j])->second + 1, i);
}
hash[s[j]] = j;
ans = max(ans, j - i + 1);
}
return ans;*/
}
};
int main()
{
cout << "请输入一个字符串:";
string s;
cin >> s;
Solution test;
int ret=test.lengthOfLongestSubstring(s);
cout << "无重复字符的最长子串长度为:" <<ret;
system("pause");
return 0;
}