力扣-3-无重复字符的最长字串
做之前看题目的印象是“动态规划”,因为好像和以前做的“最大子数和”看起来有点像
查看标签有“滑动窗口”,这个见过听过,但是不知道具体是什么,但是有印象,计网里的拥塞控制也用到了“滑动窗口”的概念
感觉题目降低难度了,只是要求返回长度不是要求返回子串
有了计网“滑动窗口”的概念,我想是需要两个指针来表示窗口的,头指针表示窗口起始,尾指针表示窗口末尾。另外拿一个变量来记录窗口长度
感觉难点在于:
- 如何高效地判重
我想起了刚刚做的题,用set,如果把字串中的字符放到set中,然后用count函数来判断
那么,试试吧
int lengthOfLongestSubstring(string s) { set<char> st; int begin = 0,ret =0; for(int end = begin;end<s.size();++end){ if(st.count(s[end])==0){ // set中没有该字符,说明没有重复 st.emplace(s[end]);// 放进去 }else{ // set中存在,有重复字符了 ret = max(ret,end-begin); begin = end;// 刷新窗口 } } return ret; }
第一版,三个示例通过了就迫不及待地提交,结果空字符就报错了
检查一遍健壮性
随手编了一个测试用例,发现一个问题:刷新窗口后set没清空
但还是不对,这是为什么?
问题应该出在最后遍历完成后没有再记录窗口长度,也就是说如果最长无重复子串在数组最后的情况
分析一下,遍历结束后,end指针应该在数组末尾,begin指针还在窗口开头
我把end改成类变量
有一个很复杂的问题,每次出现了重复字符不是把头指针与尾指针置通,而是头指针指向被重复字符的位置去,那我改map,刷新begin到被重复元素的后一个位置,然后这样又有了新问题,容器不能简单得清空了
class Solution { public: int lengthOfLongestSubstring(string s) { int ret = 0; if(s==""){ return ret; } map<char,int> mp; int begin = 0,end; for(end = begin;end<s.size();++end){ if(mp.count(s[end])==1){ // set中存在,有重复字符了 int newLocation = mp.at(s[end])+1; // 把begin到被重复字符之间的全踢掉 for(int i =begin;i<newLocation;++i){ mp.erase(s[i]); } begin = newLocation;// 刷新窗口 //放到被重复的字符位置去 } // set中没有该字符,说明没有重复 mp.emplace(s[end],end);// 放进去 ret = max(ret,end-begin+1); } return ret; } };
这一版,能把目前的四个用例全部跑过
通过!🎉不过效率奇低
理一下思路
个人思路
初始窗口指针begin、end均为0,end始终向前走,begin不定时刷新,两个指针中间为“窗口”,即“目前无重复的最大字串”
1.end指针遍历字符串,
- 若map中没有该字符(即当前窗口中字符无重复),将遇到的字符插入map作为键,字符的索引作为值
- 若map中已有该字符,这时候就需要更新窗口(1. 将begin更新为被重复的元素的后一索引 2. 将原bgin到新begin之间的元素从map中删除)
- 为了保证数组遍历完时不会丢失最新的数组长度,于是每一步遍历都更新了结果的值
接下来考虑怎么优化算法的时间和空间效率
优化
首先,end指针的一次遍历过程是没法优化的,这是必须的
我想到了优化更新begin的过程
这样,当我遇到相同的了,就从begin(到end)开始遍历,如果指向的字符不等于现在的重复的字符,就踢掉,那么就可以又回到set
class Solution { public: int lengthOfLongestSubstring(string s) { int ret = 0; if(s==""){ return ret; } set<char> st; int begin = 0,end; for(end = begin;end<s.size();++end){ if(st.count(s[end])==1){ while(s[begin]!=s[end]){ st.erase(s[begin]); begin++; } // 上面的循环结束后,begin指向了被重复的元素 begin++; } // set中没有该字符,说明没有重复 st.emplace(s[end]);// 放进去 ret = max(ret,end-begin+1); } return ret; } };
好像…时间提高了一点,空间没变化
继续优化,两个思路
- 不用STL容器能否实现?
我之所以会用容器,主要还是为了保存和判相同,但判相同不是能用位运算?但是保存地数据结构就不好解决了,或者要不,干脆不解决,不用额外的空间呢
……好像不太行,遍历只会导致效率更低 - map能否更巧妙地实现?
map的优点是能保存值和索引位置
我扫描字符串,键没有,就把键(字符)值(索引)放进去,键有了,就把返回值更新为字符长度。
不不不,思路有问题
看题解了看题解了
官方题解
滑动窗口,还是两个指针,有意思的是这里的右指针和左指针都是一直不回头地往右走的
左指针每一步的移动都会更新最长长度,并且从set中踢掉前一个元素
右指针遇到重复元素时,不再移动,左指针动就一直踢,直到把重复的元素踢掉为止
class Solution { public: int lengthOfLongestSubstring(string s) { // 哈希集合,记录每个字符是否出现过 unordered_set<char> occ; int n = s.size(); // 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动 int rk = 0, ans = 0; // 枚举左指针的位置,初始值隐性地表示为 -1 for (int i = 0; i < n; ++i) { // 左指针向右移动一格,移除前一个字符 if (i != 0) { occ.erase(s[i - 1]); } // rk的初始值为什么不设置为0? // 当右指针没有到头且没有出现重复字符时,就不断移动右指针并将元素插入set // 只要没有踢掉导致重复的元素,左指针就会从左往右一直踢 while (rk < n && !occ.count(s[rk])) { occ.insert(s[rk]); ++rk; } // 第 i 到 rk 个字符是一个极长的无重复字符子串 // 即是左右指针的距离 ans = max(ans, rk - i); } return ans; } };
C++中字符数组名和字符串变量名
这一插一段,因为写的时候遇到了s==nullptr
报错的情况,那么说明C++中字符串不是指针?不像是字符数组的数组名?
另外s==NULL
也报错,那么它也不是对象?
我用了s==""
,但究竟是怎么回事呢?
后记
哈哈哈,我还是很开心,毕竟是自己写出来的中等题(好吧,我看了标签提示)
2023.8.28
我还是觉得自己的代码思路好,本质上是和官解一样的,但是官解的循环结构实在是不好理解,右指针写在里面,左指针写在外面
int lengthOfLongestSubstring(string s) { int start = 0, end,res=0; unordered_set<char> set; for (end = start; end < s.size(); end++) { if (set.count(s[end])) { while (s[start] != s[end]) { set.erase(s[start++]); } start++; } set.emplace(s[end]); res = max(res, end - start); } return res; }
本文作者:YaosGHC
本文链接:https://www.cnblogs.com/yaocy/p/16329023.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步