3.无重复字符的最长子串
3.1 题目
给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。
示例:
示例 1:
- 输入:s = “abcabcbb”
- 输出:3
- 解释:因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
- 输入:s = “bbbbb”
- 输出:1
- 解释:因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
- 输入:s = “pwwkew”
- 输出:3
- 解释:因为无重复字符的最长子串是 “wke”,所以其长度为 3。
- 请注意,你的答案必须是 子串 的长度,”pwke” 是一个子序列,不是子串。
提示:
- 0 <= s.length <= 5 * 104
- s 由英文字母、数字、符号和空格组成
3.2 题解
方法一:双循环法
思路
用两层循环枚举所有可能的子串。外层循环确定子串起点,内层循环从起点向后扩展,遇到重复字符就停止。使用 Contains 判断当前字符是否已在子串中出现。
核心思想:暴力枚举所有子串,找到最长的无重复字符子串。
具体步骤:
- 外层循环遍历每个可能的起始位置
startIndex。 - 内层循环从
startIndex + 1开始向后扩展:- 如果当前字符已在子串中出现,停止扩展
- 否则更新最大长度
- 返回最大长度。
举例:对于 s = "abcabcbb":
startIndex=0:子串"ab"→"abc"→ 遇到a重复,长度 3startIndex=1:子串"bc"→"bca"→ 遇到b重复,长度 3startIndex=2:子串"ca"→"cab"→ 遇到c重复,长度 3- …
- 最长无重复子串长度为 3
复杂度分析
- 时间复杂度:
O(n^3),两层循环加上Contains操作。 - 空间复杂度:
O(n),存储子串的临时变量。
代码
// 方法一:双循环法
// 用两层循环,外层循环遍历字符串的每个字符作为子串的起始位置,内层循环遍历以当前字符为起始的所有子串,判断是否包含重复字符,从而找到最长的不含重复字符的子串。
static int LengthOfLongestSubstring1(string s)
{
// 如果输入字符串为空,直接返回0
if (s.Length == 0)
{
return 0;
}
// 如果输入字符串长度为1,直接返回1
if (s.Length == 1)
{
return 1;
}
// 初始化最大长度和对应的子串
int maxNum = 1; //至少有1个元素的话 子串至少都为1 所以初始化为1
string resultString = "";
// 外层循环,遍历所有可能的起始位置
for (int startIndex = 0; startIndex < s.Length; startIndex++)
{
// 临时变量,用于记录当前子串的长度
int tempMaxNum = 0;
// 内层循环,遍历以startIndex为起始位置的所有子串
for (int endIndex = startIndex + 1; endIndex < s.Length; endIndex++)
{
// 判断子串中是否包含当前字符 如果有重复的了往后继续遍历肯定也重复 没有意义 跳出循环
if (s.Substring(startIndex, endIndex - startIndex).Contains(s[endIndex]))
{
// 发现重复字符,结束内层循环
break;
}
// 计算当前子串的长度
tempMaxNum = endIndex - startIndex + 1;
// 如果当前临时最大长度大于全局最大长度,更新全局最大长度和对应子串
if (tempMaxNum > maxNum)
{
maxNum = tempMaxNum;
resultString = s.Substring(startIndex, endIndex - startIndex + 1);
}
}
}
//如果最长子串长度还是1 直接返回第一个元素算了
if (maxNum == 1)
{
resultString = s[0].ToString();
}
// 输出最长子串
Console.WriteLine(resultString);
// 返回最大长度
return maxNum;
}
方法二:滑动窗口单循环缓存字典法
思路
使用滑动窗口维护一个不含重复字符的子串。用字典记录每个字符的最新位置,当遇到重复字符时,将窗口起点跳转到重复字符的下一个位置。关键点:起点只能向后移动,不能回退。
核心思想:滑动窗口 + 字典记录位置,一次遍历完成。
具体步骤:
- 初始化窗口起点
startIndex = 0,字典charIndexDictionary。 - 遍历字符串,对于每个字符
s[endIndex]:- 如果字符在字典中且位置 >=
startIndex:更新startIndex为字典中位置 + 1 - 更新字典中该字符的位置为当前索引
- 更新最大长度
- 如果字符在字典中且位置 >=
- 返回最大长度。
举例:对于 s = "abcabcbb":
end=0, char='a':字典{a:0},长度 1end=1, char='b':字典{a:0, b:1},长度 2end=2, char='c':字典{a:0, b:1, c:2},长度 3end=3, char='a':a在字典中位置 0 >= 0,startIndex = 1,字典{a:3, b:1, c:2},长度 3end=4, char='b':b在字典中位置 1 >= 1,startIndex = 2,字典{a:3, b:4, c:2},长度 3end=5, char='c':c在字典中位置 2 >= 2,startIndex = 3,字典{a:3, b:4, c:5},长度 3end=6, char='b':b在字典中位置 4 >= 3,startIndex = 5,字典{a:3, b:6, c:5},长度 2end=7, char='b':b在字典中位置 6 >= 5,startIndex = 7,字典{a:3, b:7, c:5},长度 1- 最大长度为 3
复杂度分析
- 时间复杂度:
O(n),只需遍历一次字符串。 - 空间复杂度:
O(min(m, n)),其中m是字符集大小。
代码
// 方法二:滑动窗口单循环缓存字典法
// 使用滑动窗口的思想,维护两个指针startIndex和endIndex,以及一个字典charIndexDictionary来存储字符及其在字符串中的最新索引。
// 通过单循环遍历字符串,判断是否有重复字符,从而找到最长的不含重复字符的子串。
// 每次发现重复元素后,开始位置的变化规律是变为前方重复元素的后一位(关键规律)。注意其实位置不能往前走只能往后走,往后走要特殊处理。
static int LengthOfLongestSubstring2(string s)
{
// 如果输入字符串为空或为null,直接返回0
if (string.IsNullOrEmpty(s))
{
return 0;
}
// 如果输入字符串长度为1,直接返回1
if (s.Length == 1)
{
return 1;
}
string resultString = "";
// 初始化最大长度和起始位置
int maxNum = 0;
int startIndex = 0;
// 创建一个字典,用于记录字符及其在字符串中的索引
Dictionary<char, int> charIndexDictionary = new Dictionary<char, int>();
// 遍历字符串中的每个字符
for (int endIndex = 0; endIndex < s.Length; endIndex++)
{
// 如果当前字符在字典中存在,且其索引大于等于起始位置,要保证起始位置不能往回头
if (charIndexDictionary.ContainsKey(s[endIndex]) && charIndexDictionary[s[endIndex]] >= startIndex)
{
// 每次发现重复元素后,startIndex的变化规律是变为前方重复元素的后一位(关键规律)
// 如果是,更新起始位置为重复字符的下一个位置
startIndex = charIndexDictionary[s[endIndex]] + 1;
}
// 更新字典中字符的索引为当前位置
charIndexDictionary[s[endIndex]] = endIndex;
if (endIndex - startIndex + 1 > maxNum)
{
resultString = s.Substring(startIndex, endIndex - startIndex + 1);
}
// 计算当前不含重复字符的子串长度,并更新最大长度
maxNum = Math.Max(maxNum, endIndex - startIndex + 1);
}
// 输出最长子串
Console.WriteLine(resultString);
// 返回最大长度
return maxNum;
}
3.3 代码
class Program
{
static void Main()
{
#region 题目
// 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
// 示例 1:
// 输入: s = "abcabcbb"
// 输出: 3
// 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
// 示例 2:
// 输入: s = "bbbbb"
// 输出: 1
// 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
// 示例 3:
// 输入: s = "pwwkew"
// 输出: 3
// 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
// 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
// 提示:
// 0 <= s.length <= 5 * 104
// s 由英文字母、数字、符号和空格组成
#endregion
#region 测试
// 示例 1
string s1 = "abcabcbb";
int result11 = LengthOfLongestSubstring1(s1);
Console.WriteLine($"示例1 方法1 输出:{result11}");
int result12 = LengthOfLongestSubstring2(s1);
Console.WriteLine($"示例1 方法2 输出:{result12}");
// 示例 2
string s2 = "bbbbb";
int result21 = LengthOfLongestSubstring1(s2);
Console.WriteLine($"示例2 方法1 输出:{result21}");
int result22 = LengthOfLongestSubstring2(s2);
Console.WriteLine($"示例2 方法2 输出:{result22}");
// 示例 3
string s3 = "pwwkew";
int result31 = LengthOfLongestSubstring1(s3);
Console.WriteLine($"示例3 方法1 输出:{result31}");
int result32 = LengthOfLongestSubstring2(s3);
Console.WriteLine($"示例3 方法2 输出:{result32}");
#endregion
}
#region 答案
// 方法一:双循环法
// 用两层循环,外层循环遍历字符串的每个字符作为子串的起始位置,内层循环遍历以当前字符为起始的所有子串,判断是否包含重复字符,从而找到最长的不含重复字符的子串。
static int LengthOfLongestSubstring1(string s)
{
// 如果输入字符串为空,直接返回0
if (s.Length == 0)
{
return 0;
}
// 如果输入字符串长度为1,直接返回1
if (s.Length == 1)
{
return 1;
}
// 初始化最大长度和对应的子串
int maxNum = 1; //至少有1个元素的话 子串至少都为1 所以初始化为1
string resultString = "";
// 外层循环,遍历所有可能的起始位置
for (int startIndex = 0; startIndex < s.Length; startIndex++)
{
// 临时变量,用于记录当前子串的长度
int tempMaxNum = 0;
// 内层循环,遍历以startIndex为起始位置的所有子串
for (int endIndex = startIndex + 1; endIndex < s.Length; endIndex++)
{
// 判断子串中是否包含当前字符 如果有重复的了往后继续遍历肯定也重复 没有意义 跳出循环
if (s.Substring(startIndex, endIndex - startIndex).Contains(s[endIndex]))
{
// 发现重复字符,结束内层循环
break;
}
// 计算当前子串的长度
tempMaxNum = endIndex - startIndex + 1;
// 如果当前临时最大长度大于全局最大长度,更新全局最大长度和对应子串
if (tempMaxNum > maxNum)
{
maxNum = tempMaxNum;
resultString = s.Substring(startIndex, endIndex - startIndex + 1);
}
}
}
//如果最长子串长度还是1 直接返回第一个元素算了
if (maxNum == 1)
{
resultString = s[0].ToString();
}
// 输出最长子串
Console.WriteLine(resultString);
// 返回最大长度
return maxNum;
}
// 方法二:滑动窗口单循环缓存字典法
// 使用滑动窗口的思想,维护两个指针startIndex和endIndex,以及一个字典charIndexDictionary来存储字符及其在字符串中的最新索引。
// 通过单循环遍历字符串,判断是否有重复字符,从而找到最长的不含重复字符的子串。
// 每次发现重复元素后,开始位置的变化规律是变为前方重复元素的后一位(关键规律)。注意其实位置不能往前走只能往后走,往后走要特殊处理。
static int LengthOfLongestSubstring2(string s)
{
// 如果输入字符串为空或为null,直接返回0
if (string.IsNullOrEmpty(s))
{
return 0;
}
// 如果输入字符串长度为1,直接返回1
if (s.Length == 1)
{
return 1;
}
string resultString = "";
// 初始化最大长度和起始位置
int maxNum = 0;
int startIndex = 0;
// 创建一个字典,用于记录字符及其在字符串中的索引
Dictionary<char, int> charIndexDictionary = new Dictionary<char, int>();
// 遍历字符串中的每个字符
for (int endIndex = 0; endIndex < s.Length; endIndex++)
{
// 如果当前字符在字典中存在,且其索引大于等于起始位置,要保证起始位置不能王回头
if (charIndexDictionary.ContainsKey(s[endIndex]) && charIndexDictionary[s[endIndex]] >= startIndex)
{
// 每次发现重复元素后,startIndex的变化规律是变为前方重复元素的后一位(关键规律)
// 如果是,更新起始位置为重复字符的下一个位置
startIndex = charIndexDictionary[s[endIndex]] + 1;
}
// 更新字典中字符的索引为当前位置
charIndexDictionary[s[endIndex]] = endIndex;
if (endIndex - startIndex + 1 > maxNum)
{
resultString = s.Substring(startIndex, endIndex - startIndex + 1);
}
// 计算当前不含重复字符的子串长度,并更新最大长度
maxNum = Math.Max(maxNum, endIndex - startIndex + 1);
}
// 输出最长子串
Console.WriteLine(resultString);
// 返回最大长度
return maxNum;
}
#endregion
}
3.4 运行结果
abc
示例1 方法1 输出:3
abc
示例1 方法2 输出:3
b
示例2 方法1 输出:1
b
示例2 方法2 输出:1
wke
示例3 方法1 输出:3
wke
示例3 方法2 输出:3
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com