今天刷了一道力扣题:
3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
错误解法
看起来没什么难度,可是,当我用三重循环暴力求解的时候,被提示:
好吧 时间复杂度都O(n^3)了,确实容易超时。。。
小技巧:如果提示中给的输入范围很大,那么该题大概率不能暴力求解TAT
解题思路
通过看题解得知,该题有一个很优的解法:滑动窗口。
滑动窗口 - 无重复字符的最长子串 - 力扣(LeetCode)
定义两个指针,一个记录起始位置,一个记录结束位置。
**什么是滑动窗口?**其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列。
**如何移动?**我们只要把队列的左边的元素移出就行了,直到满足题目要求。
一直维持这样的队列,找出队列出现最长的长度时候,求出解。
可是这个题有一个点把我困住了:怎么判断起始位置和结束位置中间有无重复字符呢?
可以用集合!
常用的数据结构为 哈希集合(即C++ 中的std:: unordered_set,Java中的HashSet,Python中的set, JavaScript 中的 Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。
set基本函数
set的一些操作见《算法笔记》P197,下面只简单介绍一下用到的函数(需要《算法笔记》pdf 版的可以私聊我~):
set内的元素自动递增排序,且自动去除了重复元素。
insert(x):向集合中插入元素x,并自动去重。
find(value):返回set中对应值为value的迭代器。
erase():
①删除单个元素
st.erase(it),it是所需要删除元素的迭代器
st.erase(st.find(100));
st.erase(value),value为所需要删除元素的值
st.erase(100);
②删除一个区间里的所有元素
st.erase(first, last):first为所需要删除区间的起始迭代器,last为所需要删除区间的末尾迭代器的下一个地址,即删除[first, last)。
count():返回元素在集合中出现的次数。
该函数返回1或0,因为该集合仅包含唯一元素。如果设置的容器中存在该值,则返回1。如果容器中不存在它,则返回0。
set最主要的作用是自动去重并按升序排序,因此碰到需要去重但是不方便直接开数组的情况,可以尝试用set解决。
map基本函数
set的一些操作见《算法笔记》P213,后面一些题可能会用到map,所以写在这里:
定义map:
unordered_map<typename1, typename2> mp;
<key, value>映射
map可以直接通过对应下标访问
例:unordered_map<char, int> mp; mp['c']; 即为'c'对应的整数值。
代码
题解代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
// 哈希集合,记录每个字符是否出现过
unordered_set<char> occ;
int n = s.size();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
// 枚举左指针的位置,初始值隐性地表示为 -1
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.erase(s[i - 1]);
}
while (rk + 1 < n && !occ.count(s[rk + 1])) {
// 不断地移动右指针
occ.insert(s[rk + 1]);
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = max(ans, rk - i + 1);
}
return ans;
}
};
时间复杂度:O(n)
哭了。。原来我暴力用双重循环解决的问题只需要一个集合的count函数。。
我的代码:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
int rk = 0;//右指针
int mx=0;
unordered_set<char> st;
for(int i=0; i<n; i++){
while(rk<n && !st.count(s[rk])){
st.insert(s[rk++]);
}
if(st.size()>mx)
mx = st.size();
st.erase(s[i]);
}
return mx;
}
};
类似题目练手
根据题意,写下了如下代码:
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
/*
错误情况:
输入:"wordgoodgoodgoodbestword" ["word","good","best","good"]
输出:[]
预期结果:[8]
解决:原来是用集合存string,现在用集合存它们对应的index即可。
*/
int w_size = words[0].size();
int size = words.size();
int n = s.size();
unordered_set<int> occ;
vector<int> a;
int now;
int rk = -1;
for(int i=0;i<n-w_size*size+1;i++){ //滑动窗口
occ.clear();
rk=i;
for(int j=0;j<size;j++){ //取一个字符串
string temp = s.substr(rk,w_size);
vector<int> t;
for(int k=0;k<size;k++){ //在字符串数组中找到与之匹配的下标
if(temp.compare(words[k])==0){
t.push_back(k);
// cout<<temp<<" ";
}
}
for(int h=0;h<t.size();h++){ //如果下标不在集合里面,就把它加入
if(!occ.count(t[h])){
rk=rk+w_size;
occ.insert(t[h]);
break;
}
}
}
if(occ.size()==size) //如果集合中正好包括了字符串数组中的所有下标,就符合条件
a.push_back(i);
}
return a;
}
};
又超时了。。。
解法参照:详细通俗的思路分析,多解法 - 串联所有单词的子串 - 力扣(LeetCode)解法二
为了方便讨论,我们每次移动一个单词的长度,也就是w_size个字符,这样所有的移动被分成了w_size类。就不用一步一步移动i了
class Solution {
/*
基本思路:滑动窗口。
使用一个无序map,对应的int是该单词的频度。
初始:把初始滑动窗口中的字符串按单词长度划分后加入map中(频度+1),并把字符串数组中的字符串在map中的频度-1;
每当map中有频度=0的单词,就把该单词从map中删掉。
如果map为空,则记录下滑动窗口左边界;
如果不为空,滑动窗口右移,加入的单词在map中频度+1,减掉的单词在map中频度-1.
*/
public:
vector<int> findSubstring(string &s, vector<string> &words) {
vector<int> res;
int size = words.size(), w_size = words[0].size(), ls = s.size();
//为了方便讨论,我们每次移动一个单词的长度,也就是w_size个字符,这样所有的移动被分成了w_size类。
for (int i = 0; i < w_size && i + size * w_size <= ls; ++i) {
unordered_map<string, int> differ;
//先把滑动窗口内的单词存一下(单词在map中的频度+1)
int rk = i;
for (int j = 0; j < size; ++j) {
++differ[s.substr(rk, w_size)];
rk=rk+w_size;
}
//把单词在map中的频度-1,如果单词在map中的频度=0,就把它删掉
for (string &word: words) {
if (--differ[word] == 0) {
differ.erase(word);
}
}
//向后遍历,每次移动一个单词的长度
for (int start = i; start < ls - size * w_size + 1; start += w_size) {
//start相当于左边界,start!=i即滑动窗口需要后移了,此时需要多加入一个单词
if (start != i) {
// 新加入一个单词
string word = s.substr(start + (size - 1) * w_size, w_size);
if (++differ[word] == 0) {
//如果这个单词是之前不在map里,但是单词表中有,那么这个单词在map中的频度<0
//当它=0时,把它从map中删掉
differ.erase(word);
}
word = s.substr(start - w_size, w_size);
if (--differ[word] == 0) {
differ.erase(word);
}
}
if (differ.empty()) {
res.push_back(start);
}
}
}
return res;
}
};