废话不多说,直接上题目
题目------无重复字符的最长字串问题
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串的长度。
示例 1:
ini
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
ini
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
makefile
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列, 不是子串。
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
二、我的解法
js
var lengthOfLongestSubstring = function(s) {
let big=0;
for(let i=0;i<s.length;i++){
let c=a(s,i)
big=big>c? big:c;
}
return big;
};
function a(s,n){
list={};
let count=0;
for(let i=n;i<s.length;i++){
count++;
if(s[i] in list){
return count-1;
}
list[s[i]]=i;
}
return count;
}
我的解法是使用类哈希表把遍历过的字符存起来,以便于以 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ( O ) N(O) </math>N(O)的时间复杂度去遍历某一趟 ,但是最终的时间复杂度还是 <math xmlns="http://www.w3.org/1998/Math/MathML"> N ( O 2 ) N(O2) </math>N(O2),不过没关系,这不是重点
三、最优解法------ 滑动窗口(Sliding Window)算法
js
function lengthOfLongestSubstring(s) {
// 使用Map来存储字符及其最新索引
const charIndexMap = new Map();
let maxLength = 0;
let left = 0; // 滑动窗口左边界
for (let right = 0; right < s.length; right++) {
const currentChar = s[right];
// 如果字符已存在且在窗口内,则移动左边界
if (charIndexMap.has(currentChar) {
// 取最大值是为了防止左边界回退
left = Math.max(left, charIndexMap.get(currentChar) + 1);
}
// 更新字符的最新索引
charIndexMap.set(currentChar, right);
// 计算当前窗口大小并更新最大值
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
题解要点
1. 初始化
const charIndexMap = new Map();
: 创建一个 Map 对象charIndexMap
,用于存储字符及其在字符串s
中最新出现的索引。 Map 对于快速查找字符的索引非常有用。let maxLength = 0;
: 初始化变量maxLength
为 0。 这将用于存储找到的最长无重复子字符串的长度。let left = 0;
: 初始化滑动窗口的左边界left
为 0。
2. 滑动窗口循环
for (let right = 0; right < s.length; right++) { ... }
: 使用right
变量作为滑动窗口的右边界,从字符串的第一个字符开始遍历到最后一个字符。
3. 处理当前字符
-
const currentChar = s[right];
: 获取当前字符。 -
if (charIndexMap.has(currentChar)) { ... }
: 检查当前字符currentChar
是否已经存在于charIndexMap
中。 如果存在,说明当前字符在当前窗口中已经出现过重复。-
left = Math.max(left, charIndexMap.get(currentChar) + 1);
: 如果currentChar
已经存在于charIndexMap
中,则需要移动左边界left
。charIndexMap.get(currentChar)
获取currentChar
上一次出现的索引。charIndexMap.get(currentChar) + 1
是新的左边界的候选位置,即重复字符上次出现位置的下一个位置。Math.max(left, charIndexMap.get(currentChar) + 1)
取left
的当前值和新候选位置中的较大值。 这是至关重要的,可以防止left
回退。 例如:字符串为 "abba",当 right 指向第二个 'b' 时,left 会更新为 2。接下来,当 right 指向 'a' 时,如果没有Math.max
,left 会错误地回退到 1。
-
4. 更新字符索引和最大长度
charIndexMap.set(currentChar, right);
: 更新currentChar
在charIndexMap
中的索引为当前的right
。 无论字符是否重复,都需要更新其最新位置。maxLength = Math.max(maxLength, right - left + 1);
: 计算当前滑动窗口的大小(right - left + 1
),并将其与当前的maxLength
进行比较,更新maxLength
为较大的值。
5. 返回结果
return maxLength;
: 循环结束后,返回maxLength
,即找到的最长无重复子字符串的长度。
实例与展示









四、动态图解---(复制在编译器运行即可)
js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滑动窗口算法图解</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.container {
margin-bottom: 30px;
}
.string-display {
font-size: 24px;
letter-spacing: 5px;
margin: 20px 0;
position: relative;
height: 60px;
}
.char {
display: inline-block;
width: 30px;
text-align: center;
position: relative;
}
.index {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: #666;
}
.window {
position: absolute;
height: 40px;
background-color: rgba(100, 200, 100, 0.3);
top: 25px;
border-radius: 5px;
transition: all 0.5s ease;
}
.pointer {
position: absolute;
top: -15px;
font-size: 12px;
color: red;
}
.left-pointer {
left: 0;
}
.right-pointer {
right: 0;
}
.controls {
margin: 20px 0;
}
button {
padding: 8px 15px;
margin-right: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.explanation {
background-color: #f8f8f8;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
</head>
<body>
<h1>无重复字符的最长子串 - 滑动窗口算法图解</h1>
<div class="container">
<h2>示例字符串: "abcabcbb"</h2>
<div class="controls">
<button id="prevBtn">上一步</button>
<button id="nextBtn">下一步</button>
<button id="resetBtn">重置</button>
<span id="stepCounter">步骤: 0/8</span>
</div>
<div class="string-display" id="stringDisplay">
<!-- 字符将通过JS动态生成 -->
</div>
<div class="explanation" id="explanation">
<p>初始化: left = 0, right = 0, maxLen = 0</p>
<p>当前窗口: []</p>
<p>字符位置记录: {}</p>
</div>
<table id="stepsTable">
<thead>
<tr>
<th>步骤</th>
<th>right</th>
<th>字符</th>
<th>窗口</th>
<th>字符位置</th>
<th>操作</th>
<th>maxLen</th>
</tr>
</thead>
<tbody>
<!-- 表格内容将通过JS动态生成 -->
</tbody>
</table>
</div>
<script>
const s = "abcabcbb";
let currentStep = 0;
const maxSteps = s.length;
const charMap = {};
let left = 0;
let maxLen = 0;
const stepsData = [];
// 初始化字符串显示
function initStringDisplay() {
const stringDisplay = document.getElementById('stringDisplay');
stringDisplay.innerHTML = '';
for (let i = 0; i < s.length; i++) {
const charElement = document.createElement('div');
charElement.className = 'char';
charElement.innerHTML = `
<span class="index">${i}</span>
${s[i]}
`;
stringDisplay.appendChild(charElement);
}
updateWindowDisplay();
}
// 更新窗口显示
function updateWindowDisplay() {
// 移除旧的窗口和指针
const oldWindow = document.querySelector('.window');
if (oldWindow) oldWindow.remove();
const oldPointers = document.querySelectorAll('.pointer');
oldPointers.forEach(p => p.remove());
const stringDisplay = document.getElementById('stringDisplay');
const chars = document.querySelectorAll('.char');
// 添加窗口
if (currentStep > 0) {
const windowElement = document.createElement('div');
windowElement.className = 'window';
const firstChar = chars[left];
const lastChar = chars[currentStep - 1];
const leftPos = firstChar.offsetLeft;
const rightPos = lastChar.offsetLeft + lastChar.offsetWidth;
windowElement.style.left = `${leftPos}px`;
windowElement.style.width = `${rightPos - leftPos}px`;
stringDisplay.appendChild(windowElement);
// 添加指针
const leftPointer = document.createElement('div');
leftPointer.className = 'pointer left-pointer';
leftPointer.textContent = 'left';
leftPointer.style.left = `${leftPos}px`;
stringDisplay.appendChild(leftPointer);
const rightPointer = document.createElement('div');
rightPointer.className = 'pointer right-pointer';
rightPointer.textContent = 'right';
rightPointer.style.left = `${rightPos - 15}px`;
stringDisplay.appendChild(rightPointer);
}
}
// 更新解释文本
function updateExplanation() {
const explanation = document.getElementById('explanation');
if (currentStep === 0) {
explanation.innerHTML = `
<p>初始化: left = 0, right = 0, maxLen = 0</p>
<p>当前窗口: []</p>
<p>字符位置记录: {}</p>
`;
return;
}
const currentChar = s[currentStep - 1];
const windowStr = s.slice(left, currentStep);
const charMapStr = JSON.stringify(charMap).replace(/"/g, '');
let operation = `添加字符 '${currentChar}'`;
if (charMap[currentChar] !== undefined && charMap[currentChar] >= left) {
operation = `发现重复字符 '${currentChar}',移动 left 从 ${left} 到 ${charMap[currentChar] + 1}`;
}
explanation.innerHTML = `
<p>步骤 ${currentStep}: right = ${currentStep - 1}, 字符 = '${currentChar}'</p>
<p>${operation}</p>
<p>当前窗口: [${windowStr}] (长度: ${windowStr.length})</p>
<p>字符位置记录: ${charMapStr}</p>
<p>最大长度更新为: ${maxLen}</p>
`;
}
// 更新步骤表格
function updateStepsTable() {
const tbody = document.querySelector('#stepsTable tbody');
tbody.innerHTML = '';
for (let i = 0; i < stepsData.length; i++) {
const step = stepsData[i];
const row = document.createElement('tr');
if (i === currentStep - 1) {
row.style.backgroundColor = '#ffffcc';
}
row.innerHTML = `
<td>${i + 1}</td>
<td>${step.right}</td>
<td>'${step.char}'</td>
<td>${step.window}</td>
<td>${step.charMap}</td>
<td>${step.operation}</td>
<td>${step.maxLen}</td>
`;
tbody.appendChild(row);
}
}
// 执行下一步
function nextStep() {
if (currentStep >= maxSteps) return;
const right = currentStep;
const currentChar = s[right];
// 记录步骤前的状态
const prevLeft = left;
const prevMaxLen = maxLen;
// 更新字符位置
if (charMap[currentChar] !== undefined && charMap[currentChar] >= left) {
left = charMap[currentChar] + 1;
}
charMap[currentChar] = right;
maxLen = Math.max(maxLen, right - left + 1);
// 保存步骤数据
stepsData.push({
right: right,
char: currentChar,
window: s.slice(left, right + 1),
charMap: JSON.parse(JSON.stringify(charMap)),
operation: charMap[currentChar] !== undefined && charMap[currentChar] >= prevLeft ?
`重复: 移动 left 从 ${prevLeft} 到 ${left}` :
`扩展窗口`,
maxLen: maxLen
});
currentStep++;
updateDisplay();
}
// 执行上一步
function prevStep() {
if (currentStep <= 0) return;
currentStep--;
// 恢复状态
if (currentStep > 0) {
const stepData = stepsData[currentStep - 1];
left = stepData.window.length > 0 ?
s.indexOf(stepData.window[0]) : 0;
maxLen = stepData.maxLen;
// 恢复字符位置记录
Object.keys(charMap).forEach(k => delete charMap[k]);
Object.assign(charMap, stepData.charMap);
} else {
left = 0;
maxLen = 0;
Object.keys(charMap).forEach(k => delete charMap[k]);
}
updateDisplay();
}
// 重置
function reset() {
currentStep = 0;
left = 0;
maxLen = 0;
Object.keys(charMap).forEach(k => delete charMap[k]);
stepsData.length = 0;
updateDisplay();
}
// 更新所有显示
function updateDisplay() {
document.getElementById('stepCounter').textContent = `步骤: ${currentStep}/${maxSteps}`;
updateWindowDisplay();
updateExplanation();
updateStepsTable();
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
initStringDisplay();
document.getElementById('nextBtn').addEventListener('click', nextStep);
document.getElementById('prevBtn').addEventListener('click', prevStep);
document.getElementById('resetBtn').addEventListener('click', reset);
// 添加键盘控制
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight') nextStep();
if (e.key === 'ArrowLeft') prevStep();
});
});
</script>
</body>
</html>
五、结语
再见!