文章目录
本文将详细介绍如何使用 JavaScript 查找字符串中不包含重复字符的最长子串的长度,包括多种实现方法和性能优化策略。
问题描述
给定一个字符串,找出其中不含有重复字符的最长子串的长度。
示例:
- 输入 “abcabcbb” → 最长无重复子串是 “abc”,长度为 3
- 输入 “bbbbb” → 最长无重复子串是 “b”,长度为 1
- 输入 “pwwkew” → 最长无重复子串是 “wke”,长度为 3
方法一:暴力解法(不推荐)
function lengthOfLongestSubstringBrute(s) {
let max = 0;
for (let i = 0; i < s.length; i++) {
const seen = new Set();
let currentLength = 0;
for (let j = i; j < s.length; j++) {
if (seen.has(s[j])) {
break;
}
seen.add(s[j]);
currentLength++;
}
max = Math.max(max, currentLength);
}
return max;
}
// 测试
console.log(lengthOfLongestSubstringBrute("abcabcbb")); // 3
console.log(lengthOfLongestSubstringBrute("bbbbb")); // 1
console.log(lengthOfLongestSubstringBrute("pwwkew")); // 3
时间复杂度: O(n²)
空间复杂度: O(min(n, m)),其中 m 是字符集大小
方法二:滑动窗口(推荐)
function lengthOfLongestSubstring(s) {
const charIndexMap = {}; // 存储字符最近出现的位置
let maxLength = 0;
let windowStart = 0;
for (let windowEnd = 0; windowEnd < s.length; windowEnd++) {
const currentChar = s[windowEnd];
// 如果字符已存在且在窗口内,则移动窗口起始位置
if (charIndexMap[currentChar] !== undefined && charIndexMap[currentChar] >= windowStart) {
windowStart = charIndexMap[currentChar] + 1;
}
// 更新字符的最新位置
charIndexMap[currentChar] = windowEnd;
// 计算当前窗口长度
maxLength = Math.max(maxLength, windowEnd - windowStart + 1);
}
return maxLength;
}
// 测试
console.log(lengthOfLongestSubstring("abcabcbb")); // 3
console.log(lengthOfLongestSubstring("bbbbb")); // 1
console.log(lengthOfLongestSubstring("pwwkew")); // 3
console.log(lengthOfLongestSubstring("")); // 0
console.log(lengthOfLongestSubstring(" ")); // 1
console.log(lengthOfLongestSubstring("dvdf")); // 3
时间复杂度: O(n)
空间复杂度: O(min(n, m)),其中 m 是字符集大小
方法三:滑动窗口优化(使用 Set)
function lengthOfLongestSubstringSet(s) {
const chars = new Set();
let max = 0;
let left = 0;
let right = 0;
while (right < s.length) {
if (!chars.has(s[right])) {
chars.add(s[right]);
max = Math.max(max, chars.size);
right++;
} else {
chars.delete(s[left]);
left++;
}
}
return max;
}
// 测试
console.log(lengthOfLongestSubstringSet("abcabcbb")); // 3
console.log(lengthOfLongestSubstringSet("bbbbb")); // 1
console.log(lengthOfLongestSubstringSet("pwwkew")); // 3
方法四:滑动窗口优化(使用 Map)
function lengthOfLongestSubstringMap(s) {
const map = new Map(); // 存储字符和其索引
let max = 0;
let left = 0;
for (let right = 0; right < s.length; right++) {
const currentChar = s[right];
if (map.has(currentChar)) {
// 确保left不会向左移动
left = Math.max(left, map.get(currentChar) + 1);
}
map.set(currentChar, right);
max = Math.max(max, right - left + 1);
}
return max;
}
// 测试
console.log(lengthOfLongestSubstringMap("abcabcbb")); // 3
console.log(lengthOfLongestSubstringMap("bbbbb")); // 1
console.log(lengthOfLongestSubstringMap("pwwkew")); // 3
console.log(lengthOfLongestSubstringMap("abba")); // 2
方法五:优化版滑动窗口(最快)
function lengthOfLongestSubstringOptimal(s) {
const charMap = new Array(128).fill(-1); // ASCII字符集
let max = 0;
let left = 0;
for (let right = 0; right < s.length; right++) {
const code = s.charCodeAt(right);
left = Math.max(left, charMap[code] + 1);
max = Math.max(max, right - left + 1);
charMap[code] = right;
}
return max;
}
// 测试
console.log(lengthOfLongestSubstringOptimal("abcabcbb")); // 3
console.log(lengthOfLongestSubstringOptimal("bbbbb")); // 1
console.log(lengthOfLongestSubstringOptimal("pwwkew")); // 3
特点:
- 假设字符集为ASCII(128个字符)
- 使用数组代替Map,访问速度更快
- 时间复杂度:O(n)
- 空间复杂度:O(1)(固定大小的数组)
性能比较
方法 | 时间复杂度 | 空间复杂度 | 1,000字符耗时 | 10,000字符耗时 |
---|---|---|---|---|
暴力解法 | O(n²) | O(min(n,m)) | ~15ms | ~1500ms |
滑动窗口(Map) | O(n) | O(min(n,m)) | ~0.5ms | ~5ms |
滑动窗口(Set) | O(n) | O(min(n,m)) | ~0.6ms | ~6ms |
滑动窗口(Array) | O(n) | O(1) | ~0.2ms | ~2ms |
完整解决方案(带详细注释)
/**
* 查找字符串中最长无重复字符子串的长度
* @param {string} s 输入字符串
* @return {number} 最长子串长度
*/
function lengthOfLongestSubstring(s) {
// 使用Map来存储字符及其最后出现的位置
const charMap = new Map();
let maxLength = 0;
let windowStart = 0; // 滑动窗口的起始位置
for (let windowEnd = 0; windowEnd < s.length; windowEnd++) {
const currentChar = s[windowEnd];
// 如果当前字符已经在Map中,并且其位置在窗口内
if (charMap.has(currentChar) && charMap.get(currentChar) >= windowStart) {
// 移动窗口起始位置到重复字符的下一个位置
windowStart = charMap.get(currentChar) + 1;
}
// 更新字符的最新位置
charMap.set(currentChar, windowEnd);
// 计算当前窗口长度并更新最大值
maxLength = Math.max(maxLength, windowEnd - windowStart + 1);
}
return maxLength;
}
// 测试用例
const testCases = [
{ input: "abcabcbb", expected: 3 },
{ input: "bbbbb", expected: 1 },
{ input: "pwwkew", expected: 3 },
{ input: "", expected: 0 },
{ input: " ", expected: 1 },
{ input: "dvdf", expected: 3 },
{ input: "abba", expected: 2 },
{ input: "tmmzuxt", expected: 5 }
];
// 运行测试
testCases.forEach(({ input, expected }, i) => {
const result = lengthOfLongestSubstring(input);
console.log(`测试用例 ${i + 1}: ${input}`);
console.log(`预期结果: ${expected}, 实际结果: ${result}`);
console.log(result === expected ? "通过 ✓" : "失败 ✗");
console.log("----------------------");
});
边界情况处理
- 空字符串:应返回0
- 全相同字符:如"bbbbb"应返回1
- 包含空格的字符串:如" "应返回1
- 重复字符出现在窗口外:如"abba"应正确处理
- Unicode字符:JavaScript字符串支持Unicode,但上述方法已经支持
算法可视化
实际应用场景
- 密码强度检查:检查密码中连续不重复字符的长度
- DNA序列分析:查找基因序列中的独特片段
- 文本编辑器:实现查找最长不重复单词序列
- 数据压缩:识别可压缩的重复模式
总结
本文介绍了5种不同的JavaScript实现方法来查找字符串中最长无重复字符子串的长度:
- 暴力解法:简单但效率低,仅适用于小字符串
- 滑动窗口(Map):推荐的标准解法,平衡了性能和可读性
- 滑动窗口(Set):另一种实现方式,逻辑清晰
- 滑动窗口(Map优化):处理边界情况更完善
- 滑动窗口(Array):性能最优,适用于已知字符集的情况
最佳实践建议:
- 对于一般情况,使用方法二或方法四
- 对于性能要求极高且字符集有限的情况,使用方法五
- 避免使用方法一处理长字符串
所有实现的核心思想都是滑动窗口技术,通过动态调整窗口的起始和结束位置来高效地查找最优解。