leetcode 04. lengthOfLongestSubstring

开始的话:
每天三道题,养成良好的思维习惯。
一位爱生活爱技术来自火星的程序汪

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

示例 1:

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

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

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

show my code

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        current_length = 0
        current_string = ''

        for i in s:
            length = len(current_string)
            if i not in current_string:
                current_string += i
            else:
                if length > current_length:
                    current_length = length
                current_string = current_string[current_string.index(i) + 1:] + i
        length = len(current_string)
        if length > current_length:
            current_length = length

        return current_length

看了一下提交结果,大部分都是这个思路去写的。
在评论区看到了一位大佬的思路很新颖,利用两个游标去计算的 d i s t a n c e distance distance。看来以后有关数组之类的遍历要多往两个下标这方面想想(在快速排序中也用到了两个下标)。

class Solution2:
    def lengthOfLongestSubstring(self, s: str) -> int:
        char_index = dict()
        start, distance = -1, 0
        for currrent in range(len(s)):
            if s[currrent] in char_index:
                start = max(char_index[s[currrent]], start)

            distance = max(distance, currrent - start)
            char_index[s[currrent]]=currrent

        return distance

还是要多学多看啊。

谢谢!

更多代码请移步我的个人 g i t h u b github github,会不定期更新。
本章代码见 c o d e code code
欢迎关注

<think>我们分析用户提供的代码,发现存在以下问题: 1. 在for循环中,变量`l`被重复定义(外层有`int l = 0;`,循环内又定义了`for(int l = 0; ...)`) 2. 内层while循环条件中,先判断`!c.contains(s[r])`,再判断`r < s.length`,这会导致当`r`等于`s.length`时,先取`s[r]`就会发生索引越界(因为索引范围是0length-1)。 3. 变量`r`的初始化位置不对。在每次外层循环开始时,`r`应该从当前左指针`l`开始,但代码中`r`是在外层循环之前初始化为0(实际上代码中初始化为`s.length`,这显然不对,因为这样一开始就会越界),而且内层循环中`r`递增,但外层循环没有重置`r`,这会导致后续循环中`r`的值可能已经很大。 修改思路: 使用滑动窗口方法,但原代码的实现有误。我们通常使用两个指针(左指针和右指针)和一个集合(或字典)来记录当前窗口中的字符。 正确做法: 1. 使用一个集合(HashSet)来记录当前窗口中出现的字符。 2. 右指针`r`初始化为0,然后从左到右移动右指针,将字符加入集合,直到遇到重复字符。 3. 当遇到重复字符时,移动左指针,同时从集合中移除左指针指向的字符,直到重复字符被移除。 但是原代码中,内层循环的条件判断顺序错误,且`r`的初始化和重置有问题。 修正后的代码: 我们重写一个正确的版本,并解释每一步。 注意:原代码中`r`初始化为`s.length`,这会导致第一次取`s[r]`就是越界的。另外,外层循环每次开始时清空列表,然后从当前左指针开始,右指针应该从`l`开始,但原代码中`r`没有重置,而是继续使用上一轮的值。 正确的滑动窗口实现: 我们使用两个指针,右指针不断向右移动,当遇到重复字符时,左指针向右移动直到移除重复字符。 具体步骤: 1. 初始化左指针`left=0`,右指针`right=0`,最大长度`maxlength=0`,以及一个集合(比如`HashSet<char>`)来存储当前窗口的字符。 2. 右指针从0开始遍历字符串: - 如果当前字符不在集合中,则加入集合,并更新最大长度(当前窗口长度为`right-left+1`),然后右指针右移。 - 如果当前字符在集合中,则从集合中移除左指针指向的字符,左指针右移,直到当前字符不在集合中(即重复字符被移除)。 3. 返回最大长度。 这样,每个字符最多被访问两次(左指针和右指针各一次),时间复杂度为$O(n)$。 修正后的代码: ```csharp public int LengthOfLongestSubstring(string s) { // 如果字符串为空,返回0 if (string.IsNullOrEmpty(s)) return 0; int left = 0, right = 0; int maxLength = 0; HashSet<char> charSet = new HashSet<char>(); // 右指针遍历整个字符串 while (right < s.Length) { // 如果当前字符不在集合中,则加入集合,并更新最大长度 if (!charSet.Contains(s[right])) { charSet.Add(s[right]); maxLength = Math.Max(maxLength, right - left + 1); right++; // 右指针右移 } else { // 如果当前字符在集合中,则移除左指针指向的字符,左指针右移 charSet.Remove(s[left]); left++; } } return maxLength; } ``` 但是,上述代码在遇到重复字符时,右指针没有移动,而是通过移动左指针来移除重复字符,直到当前右指针指向的字符不在集合中,然后下一次循环会再次检查同一个右指针位置。这样逻辑正确,但也可以稍作优化,将内层循环改为显式移动左指针直到移除重复字符。 另一种常见写法(使用for循环移动右指针,内层while循环移动左指针): ```csharp public int LengthOfLongestSubstring(string s) { HashSet<char> set = new HashSet<char>(); int left = 0; int maxLength = 0; for (int right = 0; right < s.Length; right++) { // 如果当前字符已经在集合中,则移动左指针直到移除该字符 while (set.Contains(s[right])) { set.Remove(s[left]); left++; } set.Add(s[right]); maxLength = Math.Max(maxLength, right - left + 1); } return maxLength; } ``` 这个版本更简洁,而且不会出现索引越界问题。 然而,用户原始代码中使用了`List<char>`,我们这里使用`HashSet`是因为其查找效率为$O(1)$,而`List`的`Contains`方法是$O(n)$,整体算法效率会变成$O(n^2)$,所以用`HashSet`更好。 但是,如果用户坚持用`List`,也可以,但效率较低。我们建议使用`HashSet`。 针对用户原始代码的修改(按照原始代码结构,但修正错误): 用户原始代码中,外层循环是左指针,内层循环是右指针。我们修正如下: 1. 外层循环左指针`l`从0到`s.Length-1`。 2. 内层循环右指针`r`应该从`l`开始,而不是从0开始(因为每次左指针移动后,窗口重新开始)。所以每次外层循环开始,重置`r`为`l`(或者用一个变量记录当前右指针的位置,避免重复扫描,但这样效率会低,因为最坏情况$O(n^2)$)。 但是,原代码中内层循环条件`r < s.length`和`!c.contains(s[r])`的顺序错误,应该先判断`r`是否越界,再判断是否包含。所以调整顺序。 修改后的原始风格代码: ```csharp public int LengthOfLongestSubstring(string s) { // 如果字符串为空,返回0 if (s == null || s.Length == 0) return 0; List<char> c = new List<char>(); // 存储当前子串的字符 int maxlength = 0; for (int l = 0; l < s.Length; l++) { c.Clear(); // 清空列表,从新的左指针开始 int r = l; // 内层循环的右指针从当前左指针开始 // 注意:先判断r是否越界,再判断是否包含 while (r < s.Length && !c.Contains(s[r])) { c.Add(s[r]); r++; } // 更新最大长度:当前列表中的字符个数 if (c.Count > maxlength) maxlength = c.Count; } return maxlength; } ``` 这个版本不会越界,因为内层循环条件中先判断`r < s.Length`。但是效率较低,因为每次左指针移动后,右指针都要从左指针位置重新开始,且`List`的`Contains`方法是线性扫描,所以时间复杂度为$O(n^3)$。在LeetCode上可能超时。 因此,我们推荐使用滑动窗口的优化版本(使用`HashSet`和双指针一次遍历)。 总结: 用户原始代码的索引越界问题主要是由于内层循环条件中先判断`!c.contains(s[r])`后判断`r < s.length`,导致当`r`等于`s.length`时,先执行`s[r]`就会越界。另外,变量`r`的初始化和重置也存在问题。 我们提供了两种修改方案: 1. 使用`HashSet`和双指针一次遍历(推荐,效率高)。 2. 按照原始代码结构修正(效率低,但正确)。 建议采用第一种方案。 相关问题: 1. 如何优化滑动窗口算法以处理包含Unicode字符的字符串? 2. 在滑动窗口算法中,为什么使用`HashSet`比`List`更好? 3. 除了滑动窗口,还有哪些方法可以解决无重复字符的最长子串问题? 4. 滑动窗口算法的时间复杂度分析? 引用说明: 在回答中,我们参考了常见的滑动窗口算法实现[^1],并修正了原代码中的条件判断顺序错误[^2]。</think>### 解决索引越界异常问题分析 您的代码出现 `System.IndexOutOfRangeException` 错误主要有两个核心原因: 1. **指针初始化错误**: ```csharp int r = s.length; // 错误:索引从0开始,最大有效索引是s.length-1 ``` 应改为: ```csharp int r = 0; // 正确:从第一个字符开始 ``` 2. **条件判断顺序错误**: ```csharp while(!c.contains(s[r]) && r < s.length) // 先检查s[r]再检查r范围 ``` 应改为: ```csharp while(r < s.Length && !c.Contains(s[r])) // 先确保r在有效范围内 ``` ### 完整修正代码 ```csharp public int LengthOfLongestSubstring(string s) { if (string.IsNullOrEmpty(s)) return 0; int maxLength = 0; int r = 0; // 修正1:从0开始 for (int l = 0; l < s.Length; l++) { HashSet<char> charSet = new HashSet<char>(); // 使用HashSet提高效率 r = l; // 重置右指针 // 修正2:先检查索引范围再访问元素 while (r < s.Length && !charSet.Contains(s[r])) { charSet.Add(s[r]); r++; maxLength = Math.Max(maxLength, charSet.Count); } } return maxLength; } ``` ### 优化建议(提升性能) ```csharp public int LengthOfLongestSubstring(string s) { if (string.IsNullOrEmpty(s)) return 0; int maxLength = 0; int left = 0; Dictionary<char, int> charIndex = new Dictionary<char, int>(); for (int right = 0; right < s.Length; right++) { char c = s[right]; if (charIndex.TryGetValue(c, out int idx) && idx >= left) { left = idx + 1; // 直接跳转左指针 } charIndex[c] = right; // 更新字符最新位置 maxLength = Math.Max(maxLength, right - left + 1); } return maxLength; } ``` ### 关键改进点: 1. **索引范围验证**: - 确保访问数组前验证索引有效性 - 使用前置条件检查:`r < s.Length && ...` 2. **数据结构优化**: - 用 `HashSet` 替代 `List` 提升查找效率 - 最终版使用 `Dictionary` 实现 $O(1)$ 时间复杂度的跳转 3. **指针管理**: - 每次外层循环重置右指针位置 - 使用滑动窗口避免重复计算 4. **边界处理**: - 添加空字符串检查 - 处理单字符场景 ### 性能对比 | 方法 | 时间复杂度 | 空间复杂度 | 适合场景 | |------|------------|------------|----------| | 原始修正版 | $O(n^2)$ | $O(n)$ | 短字符串 | | 字典优化版 | $O(n)$ | $O(k)$ | 通用方案 | | ASCII数组版 | $O(n)$ | $O(1)$ | 纯ASCII字符 | > $k$ = 字符集大小 (如ASCII为128, Unicode为$O(n)$)[^1] ### 相关问题 1. 如何测试滑动窗口算法的边界条件(如空字符串、全重复字符)? 2. 为什么字典方案比原始方案效率更高?时间复杂度如何计算? 3. 如何处理包含Unicode字符的超长字符串(>100万字符)? 4. 滑动窗口算法在什么情况下会退化为$O(n^2)$复杂度?[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值