JavaScript 查找字符串中最长无重复字符子串的长度

本文将详细介绍如何使用 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("----------------------");
});

边界情况处理

  1. 空字符串:应返回0
  2. 全相同字符:如"bbbbb"应返回1
  3. 包含空格的字符串:如" "应返回1
  4. 重复字符出现在窗口外:如"abba"应正确处理
  5. Unicode字符:JavaScript字符串支持Unicode,但上述方法已经支持

算法可视化

遍历结束
开始
初始化窗口起始位置和最大长度
遍历字符串
当前字符是否在窗口中
移动窗口起始位置到重复字符后
更新字符位置
计算当前窗口长度
更新最大长度
返回最大长度
结束

实际应用场景

  1. 密码强度检查:检查密码中连续不重复字符的长度
  2. DNA序列分析:查找基因序列中的独特片段
  3. 文本编辑器:实现查找最长不重复单词序列
  4. 数据压缩:识别可压缩的重复模式

总结

本文介绍了5种不同的JavaScript实现方法来查找字符串中最长无重复字符子串的长度:

  1. 暴力解法:简单但效率低,仅适用于小字符串
  2. 滑动窗口(Map):推荐的标准解法,平衡了性能和可读性
  3. 滑动窗口(Set):另一种实现方式,逻辑清晰
  4. 滑动窗口(Map优化):处理边界情况更完善
  5. 滑动窗口(Array):性能最优,适用于已知字符集的情况

最佳实践建议:

  • 对于一般情况,使用方法二或方法四
  • 对于性能要求极高且字符集有限的情况,使用方法五
  • 避免使用方法一处理长字符串

所有实现的核心思想都是滑动窗口技术,通过动态调整窗口的起始和结束位置来高效地查找最优解。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北辰alk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值