LeetCode 3. Longest Substring Without Repeating Characters 无重复字符的最长子串 C#

26 篇文章 3 订阅
16 篇文章 0 订阅
本文详细介绍了LeetCode第3题——无重复字符的最长子串问题,提供三种C#解决方案,包括暴力法、滑动窗口法及优化的滑动窗口法,并分析了它们的时间复杂度和空间复杂度。
摘要由CSDN通过智能技术生成

前言

本文介绍了 LeetCode 第 3 题 , “Longest Substring Without Repeating Characters”, 也就是 “无重复字符的最长子串” 的问题.

本文使用 C# 语言完成题目,用到了 C# 的哈希表 HashSet 和 Dictionary ( 不用 HashTable 而是使用 Dictionary,官方推荐使用 Dictionary,详情见 https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.hashtable?view=netframework-4.8)。

题目

English

LeetCode 3. Longest Substring Without Repeating Characters

Given a string, find the length of the longest substring without repeating characters.

Example 1:

Input: “abcabcbb”
Output: 3
Explanation: The answer is “abc”, with the length of 3.
Example 2:

Input: “bbbbb”
Output: 1
Explanation: The answer is “b”, with the length of 1.
Example 3:

Input: “pwwkew”
Output: 3
Explanation: The answer is “wke”, with the length of 3.
Note that the answer must be a substring, “pwke” is a subsequence and not a substring.

中文

LeetCode 3. 无重复字符的最长子串

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

示例 1:

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

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

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

解决方案

按照一般逻辑来思考,可能需要3层循环来实现暴力破解,时间复杂度为O(n^3) ;除了暴力破解外,还可以借助哈希表实现O(n)的解决方案。

笔者为大家整理了一下容易卡住的测试集 供大家参考。

1.  "a"
2. ""
3. "pwwkew"
4. "dvdf"
5. "abcd" 
6. "abcabcbb"

方法一 : 暴力法

逐个检查所有的子字符串,看它是否不含有重复的字符。

需要用到3层循环计算,时间复杂度为O(n^3).

C# 代码 :

    public int LengthOfLongestSubstring(string s)
    {
        int result = 0;
        int i = 0;
        int j = 0;
        for (; i < s.Length; i++)
        {
            bool breakJ = false;
            for (j = i + 1; j < s.Length; j++)
            {
                for (int k = i; k < j; k++)
                {
                    if (s[k] == s[j])
                    {
                        int subResult = j - i;
                        result = subResult > result ? subResult : result;
                        breakJ = true;
                        i = k;
                        break;
                    }
                }
                if (breakJ) break;
            }
            if (!breakJ)
            {
                var subResult = j - i;
                result = subResult > result ? subResult : result;
                break;
            }
        }
        return result;
    }
执行结果

执行结果 通过,执行用时 520ms,内存消耗 27.3MB .

复杂度分析

时间复杂度:O(n^3)

空间复杂度:O(1)

思路图解:

以 pwwkew 为例:
在这里插入图片描述

时间复杂度的计算

在这里插入图片描述

方法二 : 滑动窗口

暴力法非常简单,但它太慢了。那么我们该如何优化它呢?

在暴力法中,我们会反复检查一个子字符串是否含有有重复的字符,但这是没有必要的。如果从索引 i 到 j - 1 之间的子字符串 Sij 已经被检查为没有重复字符。我们只需要检查 s[j] 对应的字符是否已经存在于子字符串 Sij中。

要检查一个字符是否已经在子字符串中,我们可以检查整个子字符串,这将产生一个复杂度为 O(n^2)的算法,但我们可以做得更好。

通过使用 HashSet 作为滑动窗口,我们可以用 O(1) 的时间来完成对字符是否在当前的子字符串中的检查。

滑动窗口是数组/字符串问题中常用的抽象概念。 窗口通常是在数组/字符串中由开始和结束索引定义的一系列元素的集合,即 [i, j)(左闭,右开)。而滑动窗口是可以将两个边界向某一方向“滑动”的窗口。例如,我们将 [i, j) 向右滑动 11 个元素,则它将变为 [i+1, j+1)(左闭,右开)。

回到我们的问题,我们使用 HashSet 将字符存储在当前窗口 [i, j)(最初 j = i)中。 然后我们向右侧滑动索引 j,如果它不在 HashSet 中,我们会继续滑动 j。直到 s[j] 已经存在于 HashSet 中。此时,得到一个局部解为 s[i] 到 s[j-1] ,然后我们将 滑动窗口的左侧向右移动,也就是 增加i,直到 HashSet 中没有重复值位置。之后继续将滑动窗口像右侧滑动,增加 j 。循环以上步骤,取所有局部解的最大值,就得到我们的答案。

    public int LengthOfLongestSubstring(string s)
    {
        int n = s.Length;
        HashSet<char> set = new HashSet<char>();
        int result = 0;
        int i = 0;
        int j = 0;
        while (i < n && j < n)
        {
            if (set.Contains(s[j]))
            {
                set.Remove(s[i]);
                i++;
            }
            else
            {
                set.Add(s[j]);
                j++;
                result = Math.Max(result, j - i);
            }
        }
        return result;
    }
执行结果

执行结果 通过,执行用时 104ms,内存消耗 25.1MB .

复杂度分析

时间复杂度:O(2n) = O(n) ,最糟的情况下,每个字符将同时被 i 和 j 访问,即每个字符被访问2次。

空间复杂度:O(min(m,n)) ,与之前的方法相同。滑动窗口法需要 O(k) 的空间,其中 k 表示 Set 的大小。而 Set 的大小取决于字符串 n 的大小以及字符集 / 字母 m 的大小。

思路图解:

以 pwwkew 为例:

在这里插入图片描述

方法三 : 优化的滑动窗口

对于方法二,可以发现有一个地方可以优化的。方法二图解的第三步到第四步,i 只向右移动了一格;即发生冲突时,i每次只会向右移动一格,这导致了在最糟糕的情况下,I 和 j 几乎将 s 遍历了一遍,共2n个步骤。

事实上,它可以被优化为仅需要n个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。 当我们找到重复的字符时,我们可以立即跳过该窗口。即对于方法二图解的第四步来说,可以先找到与 s[j]重复的字符的下标index,让窗口左侧移动到 index+1 的位置即可。

C# 代码中,我们不用之前的 HashSet , 而是改用 Dictionary:

    public int LengthOfLongestSubstring(string s)
    {
        int n = s.Length;
        Dictionary<char, int> map = new Dictionary<char, int>();
        int result = 0;
        int i = 0;
        int j = 0;
        while (j < n)
        {
            if (map.ContainsKey(s[j]))
            {
                var oldJ = map[s[j]];
                i = Math.Max(oldJ + 1, i);
                map[s[j]] = j;
            }
            else
            {
                map.Add(s[j], j);
            }
            j++;
            result = Math.Max(result, j - i);
        }
        return result;
    }
执行结果

执行结果 通过,执行用时 88ms,内存消耗 25.1MB .

复杂度分析

时间复杂度:O(n) , 索引 j 将会迭代 n 次,而索引 i 会在发生冲突时,立即跳到无冲突的位置。

空间复杂度:O(min(m,n)) ,与之前的方法相同。

参考资料汇总

https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/wu-zhong-fu-zi-fu-de-zui-chang-zi-chuan-by-leetcod/

https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.generic.hashset-1?view=netframework-4.8

https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.generic.dictionary-2?view=netframework-4.8

https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.hashtable?view=netframework-4.8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值