3.无重复字符的最大字串

无重复字符的最大子串

暴力遍历

暴力的思路还是非常简单的,一个n^2的暴力循环。第一个循环是第一位是哪一位,第二个循环是后面满足条件的最大位数。
第一反应没能写出来是因为没有想到set。对于稍微有些特别的数据结构并不是特别敏感。

def lengthOfLongestSubstring(s: str) -> int:
    n = len(s)
    max_length = 0
    
    # 遍历每个可能的起始位置
    for i in range(n):
        # 使用集合来跟踪已经出现的字符
        char_set = set()
        current_length = 0
        
        # 从起始位置向后扩展
        for j in range(i, n):
            # 如果当前字符已经在集合中,表示遇到了重复字符
            if s[j] in char_set:
                break
            
            # 将当前字符添加到集合中,并增加当前子串长度
            char_set.add(s[j])
            current_length += 1
        
        # 更新最大长度
        max_length = max(max_length, current_length)
    
    return max_length

但是对于C++而言,这里又要用到stl标准库中的unordered_set<>容器。回想我当时学习C++的时候,几乎就没有怎么用过这些stl容器。。但是对于这种题目使用这种容器几乎是唯一解法,因为不使用stl会使其极度复杂,因为你需要自己定义这种结构体。
所以现在学习算法的时候我会顺带学习一下C++的常用stl。
https://www.runoob.com/cplusplus/cpp-libs-unordered_set.html

#include <string>
#include <unordered_set>
#include <algorithm>

class Solution {
public:
    int lengthOfLongestSubstring(std::string s) {
        int n = s.length();
        int maxLength = 0;
        
        // 遍历每个可能的起始位置
        for (int i = 0; i < n; i++) {
            // 使用unordered_set来跟踪已经出现的字符
            std::unordered_set<char> charSet;
            int currentLength = 0;
            
            // 从起始位置向后扩展
            for (int j = i; j < n; j++) {
                // 如果当前字符已经在集合中,表示遇到了重复字符
                if (charSet.find(s[j]) != charSet.end()) {
                    break;
                }
                
                // 将当前字符添加到集合中,并增加当前子串长度
                charSet.insert(s[j]);
                currentLength++;
            }
            
            // 更新最大长度
            maxLength = std::max(maxLength, currentLength);
        }
        
        return maxLength;
    }
};

滑动窗口

理解成一个小窗口在移动,一直划选着符合条件的内容。
窗口初始化为0,将第一个元素添加进窗口,随后检测第二个元素是否存在于窗口:
如果不存在则添加进窗口。如果存在则循环删除窗口内最左侧元素直到满足条件。
满足条件后记录目前的长度并和最大长度进行比较。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        if not s:return 0
        left = 0
        lookup = set()
        n = len(s)
        max_len = 0
        cur_len = 0
        for i in range(n):
            cur_len += 1
            while s[i] in lookup:
                lookup.remove(s[left])
                left += 1
                cur_len -= 1
            if cur_len > max_len:max_len = cur_len
            lookup.add(s[i])
        return max_len

作者:powcai
链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solutions/3982/hua-dong-chuang-kou-by-powcai/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。  

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.size() == 0) return 0;
        unordered_set<char> lookup;
        int maxStr = 0;
        int left = 0;
        for(int i = 0; i < s.size(); i++){
            while (lookup.find(s[i]) != lookup.end()){
                lookup.erase(s[left]);
                left ++;
            }
            maxStr = max(maxStr,i-left+1);
            lookup.insert(s[i]);
    }
        return maxStr;
        
    }
};

作者:powcai
链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solutions/3982/hua-dong-chuang-kou-by-powcai/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

时间复杂度分析:
乍一看,这段代码似乎有嵌套循环(外部for循环和内部while循环),可能让人觉得时间复杂度是O(n²)。但实际上,这个算法的总体时间复杂度是O(n),其中n是字符串的长度。
原因如下:

外部的for循环明确执行n次,每次处理字符串中的一个字符。
关键在于内部的while循环:虽然它看起来可能在最坏情况下对每个字符都执行多次,但实际上,字符串中的每个字符最多只会被处理两次:

一次是在外部for循环中被添加到窗口(lookup集合)
一次是在while循环中被移出窗口

左指针left在整个过程中最多移动n次(从0到n-1)。
每次执行while循环体,left指针都会向右移动一位。所以while循环体的总执行次数不会超过n次。

所以,虽然代码中有嵌套的循环结构,但内部while循环的总执行次数受到字符串长度n的限制,使得整体时间复杂度仍然是O(n)。
详细解释为什么while循环总执行次数不超过n:

每次执行while循环,left指针都会向右移动一位
left指针在整个算法执行过程中只会从0移动到最多n-1
因此,while循环的总执行次数不能超过n

空间复杂度:
空间复杂度是O(min(m, n)),其中m是字符集的大小,n是字符串长度。因为在最坏情况下,lookup集合最多包含min(m, n)个字符。
这就是为什么滑动窗口是解决这类问题的高效方法,它避免了暴力解法中的重复计算,将时间复杂度从O(n²)降低到O(n)。

此外,insert方法和erase方法分别是添加和删除。