算法之滑动窗口寻找最长无重复字符串

本文探讨了如何使用滑动窗口算法解决LeetCode上的最长无重复字符子串问题。首先介绍了滑动窗口的概念,然后逐步优化代码,从白银解法到黄金解法,最后提出空间优化的解决方案。通过实例和代码展示了如何利用状态数组加速查找重复字符的位置,达到O(n)的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天无聊的时候刷了一道leetcode的题目,给定字符串,查找最长无重复字符串,具体题目信息如下:

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
示例 4:

输入: s = ""
输出: 0
 

提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

1 思路解析

本题是一道考察字符串匹配的题目,找出其中不含重复字符的字符串的最大长度,所使用的解题思路是滑动窗口的方法来求解

1.1 什么是滑动窗口?
就是从字符串srring中,取出一段连续的子串,滑动窗口就是给定原始字符串S,给定子串的起始位置start和结束位置end,win = S[start, end] 就是窗口,win的值随着start和end的变化不断的变化,因此,称为滑动窗口。因此,滑动窗口虽然是一个数组,由于原串S始终不变,所以实际上只需要定义下面两个整数就可以来表示滑动窗口了:

···
// 滑动窗口模板
int start
int end
···
这样有什么好处呢?可以充分利用空间,降低空间复杂度,避免无效的读写运算。

1.2 滑动窗口求解无重复子串的最大长度

思路:

设定一个全局变量来记录最大长度 maxLen = 0
初始化一个滑动窗口,start = 0, end = 0
开始for循环遍历S,设当前位置为 curId,每次移动一位
	if S[curId] 在滑动窗口中不存在,
		则将curId加入到滑动窗口中,即 end = curId 或者 end++
	else 
		从滑动窗口中找到与curId对应的字符相等的最后一个字符的位置 lastId,将lastId 之前的所有字符从滑动窗口中删除,即 start = lastId +1 
		则将curId加入到滑动窗口中,即 end = curId 或者 end++
	
	更新 maxLen = max( maxLen, end - start + 1)

2 实现代码

2.1 白银解法:

执行用时: 12 ms
内存消耗: 6.8 MB

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int len = 0;
        int start = 0, end = 0;                       //定义滑动窗口 
        for (int i = 0; i < s.size(); i++) {         //遍历所有的字符
            for (int j = start; j < end; j++) {     // 遍历滑动窗口
                if (s.at(j) == s.at(i)) {              
                    start = j + 1;                        // 如果当前字符和滑动窗口中的字符相同,则删除滑动窗口中前面的字符
                }
            }

            end++;
            len = (len < end - start) ? (end - start) : len;     // 更新最大长度
        }
        return len;
    }
};

上面是根据滑动窗口的思路,写出来的,耗时12ms,做完之后,开始思考,是否还可以优化?

for (int j = start; j < end; j++) {  
    if (s.at(j) == s.at(i)) {              
        start = j + 1;                    
    }
}

上面的代码总每个字符都需要全部遍历滑动窗口
我们的目的是找到最后一个与当前字符相同的位置,然后更新start的值,何不倒序遍历滑动窗口?

for (int j = end - 1; j >= start; j--) {  
    if (s.at(j) == s.at(i)) {              
        start = j + 1; 
        break;      // 满足条件就直接跳出for循环,减少不必要的计算   
    }
}
2.2 黄金求解

上面虽然优化之后有一点点的性能改进,但是,还不是最好的。
有一个问题,每次更新滑动窗口的时候,只需要遍历滑动窗口来更新start,是否可以避免掉这种遍历?
从算法的优化角度来考虑,最简单的优化思路如下

1 牺牲空间换时间
2 牺牲时间换空间

尝试使用空间来换时间,从解题思路可以看到,我们只需要找到当前字符在滑动窗口中的位置,是否可以用一个固定的数组来记录每个字符上次出现的位置,这样就可以不用遍历滑动窗口了。
由于字符串中的每个字符对应一个ASCII码,将字符串的ASCII码值作为下标,上次出现的下标作为数组的值,因此,设置一个记录状态的数组 int state[128].
因此,上面的for循环查找位置则变成了 判断 state[s[end]] >= start,实现代码如下:

执行用时: 4 ms
内存消耗: 6.7 MB

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int len = 0, start = 0, end = 0;
        int state[128];
        memset(state, -1, sizeof(state));
        for (end = 0; end < s.size(); end++) {
            if (state[s[end]] >= start) {
                start = state[s[end]] + 1;
            }
            state[s[end]] = end;
            len = (len < end - start + 1) ? (end - start + 1) : len;
        }
        return len;
    }
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值