C++ 优先算法 —— 无重复字符的最长子串(滑动窗口)
目录
题目: 无重复字符的最长子串
1. 题目解析
2. 算法原理
Ⅰ. 暴力枚举
Ⅱ. 滑动窗口(同向双指针)
3. 代码实现
Ⅰ. 暴力枚举
Ⅱ. 滑动窗口
题目: 无重复字符的最长子串
1. 题目解析
题目截图:
此题所说的子串与长度最小的子数组题目中所说的子数组相当于一个概念,都是数组中连续的一段,区别在于子串是在一个字符串中,子数组是在一个整数数组中。
题目中要求找到一个没有重复字符的子串,并返回它的长度。
所以看到这里有三个长度为3的子串且是最长的,所以返回3。
这里可以看出全是b,只能找到一个字符,并返回长度1,因为再往后扩展也是重复的了。
所以,这道题的要求,并要求返回什么如上面所示。
2. 算法原理
这道题也有两种解法的:
- 暴力枚举
- 滑动窗口
Ⅰ. 暴力枚举
暴力枚举也就是把所有子串都枚举出来,枚举子串的时候,以某一个位置为起点,然后向后枚举,再接着以另一个某一个位置为起点,再向后枚举,这里注意的是枚举并不是全部枚举,而是当枚举一个位置时,继续再向后枚举的时候,如果发现有重复的字符出现,那么就停止枚举,也就是说,以这个位置开头的只能枚举到这里了,然后统计长度,接下来才是枚举第二个开头的,一直把所有情况枚举到,最后找一个长度的最大值。
这里的长度为2。
这里长度为3,通过上面题目解析中已经得知这里最长长度就是3.
固定每一个有可能的起始位置,然后依次向后扩展,直到扩展不能扩展为止,然后统计一下长度,把所有情况都枚举到,再统计一下最大值。
不过这里我们都是通过肉眼观察它们是否有重复的,但是程序中可不会直接就能观测到,这里就需要处理是否重复的细节问题了,可以借助开放定址法的hash表来解决重复问题,只需要把对应的字母映射到表上(遍历一个字符就让它映射到哈希表上),然后表里字母对应位置的数据统计它出现的次数,若是大于 1,那么它就重复。
所以这里的解法:暴力枚举+哈希表(表的数据存储的是字符出现的次数,为了判断字符是否重复出现)
这里时间复杂度:O(N²)。
所以,暴力枚举:
先判断 right 指向的字符在不在 hash表 里(也就是看表里对应的数据是不是大于 0),不在就放进去,然后 right 向后移动,再接着判断 right 指向的元素在不在,不在就放进去,right 再向后移动,重复上面的操作(不在就在表里对应的位置 +1,然后 right 后移)
再接着判断重复上面操作:
判断不在,重复上面操作:
此时判断不在,重复上面操作,这时right指向了第二个字符a:
当上面right 到 a 时,字符先前已经存在于哈希表里了,这时候就要停止枚举操作,"deabc"的长度就是以d开头的能得到无重复字符串的子串的最长长度。
接下来,让left换一个位置,left后移动一位,此时再让right回退至left的位置:
接下来继续重复上面的操作,直到把所有符合条件的情况枚举出来,统计长度,再获取最长的那个长度返回即可。
Ⅱ. 滑动窗口(同向双指针)
在上面暴力枚举种,我们发现有些情况是可以优化的,我们发现如下的情况:
这时left再往下一个移动的时候,这时到了字符a,让right回退到left,再继续枚举发现还是会到同一个a位置停止枚举,当left跳过了第一个出现的字符a之后,停止枚举的位置就发生变化了:
也就是说:
并且这里发现,在这个区间内依次往后枚举起始位置的话,因为终点是同样的,这时子串的长度就会递减,因此就可以让right不要拐回去了,让right在该位置先不动,先调整left,让left跳过有重复的字符:
暴力解法中,left移动到新的位置的时候,right就要回退至left的同位置,但这里也可以有个优化,也就是left跳过重复字符后,right是没有必要回退至left的:
因此,我们发现这里left和right的移动方向是一致的,也就是同向双指针:
这里的窗口就是left和right区间内维护的无重复字符的子串,让字符先进入窗口,然后判断,当有重复的字符的时候就让它出窗口:
这里的解法就是:利用规律,使用滑动窗口来解决问题。
注意:上面的情况每次都要更新结果,结果就是字符串的子串的长度。
规律:
- 当里面有重复的字符的时候,让left先向右移动,把出现的重复字符的位置之前的字符给跳过(因为它们的最后终点都会为这个重复的字符的第二次出现的位置)。
- 当left到符合要求的位置时候,right是不用回退的,可以继续向后移动,扩展该区间。
所以这里就可以同上一道题 长度最小的子数组 用滑动窗口的方法步骤解决:
- 先定义 left 和 right 并都初始化为0,充当窗口的左右端点。
- 进窗口(这里让字符串进入哈希表即可)。
- 判断:当窗口里有重复的字符时候,就要出窗口(就是从哈希表中删除该字符,注意:在删除之后,要再继续判断,直到没有重复字符为止)。
- 进出窗口都需要更新结果(符合要求的子串的长度,取新旧结果中最大的那一个即可)。
- 直到right指向最右边为止就就结束了。
这里时间复杂度情况也同于 长度最小的子数组 的情况,根据实际情况是每一步操作仅仅会让 right 向右移动 1 位或 left 向右移动 1 位,直到 right 移动到最后的位置。最坏的情况就是两个指针都遍历了一遍该数组,也就是2n次,所以时间复杂度为:O(N)。
接下来实现两种方法的代码:
3. 代码实现
题目链接:无重复字符的最长子串
Ⅰ. 暴力枚举
时间复杂度:O(N²)。
//暴力枚举
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
int len = 0;
for(int left = 0; left < n; left++) //先固定起始位置
{
int hash[128] = {0}; //将字符串中的字符出现次数映射到hash表
for(int right = left; right < n; right++)
//依次从left位置向后枚举扩展区间
{
hash[s[right]]++;
//让字符充当下标,该位置字符出现,就让它对应的hash值+1
if(hash[s[right]] > 1) //大于1说明出现重复字符了
{
break; //直接退出循环
}
len = max(len, right - left + 1); //更新结果
}
}
return len;
}
};
提交记录:
Ⅱ. 滑动窗口
时间复杂度:O(N)。
//滑动窗口
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.size();
int len = 0;
int hash[128] = { 0 }; //使用数组模拟hash表
//遇到重复的不需要让right回退了,让left跳过重复的字符之后,再处理right即可
for(int left = 0, right = 0; right < n; right++)
{
hash[s[right]]++; //进入窗口
while(hash[s[right]]>1) //判断
{
//大于1说明出现重复了
hash[s[left++]]--; //出窗口
}
len = max(len,right-left+1); //更新结果
}
return len;
}
};
提交记录:
制作不易,若有不足之处或出问题的地方,请各位大佬多多指教 ,感谢大家的阅读支持!!!