Files
illustrated-algorithm/find-first-and-last-position-of-element-in-sorted-array/index.html
T
2026-08-24 04:35:13 +00:00

249 lines
11 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>065. 在排序数组中查找元素的第一个和最后一个位置 – 图解</title>
<link rel="stylesheet" href="../shared/style.css">
<style>
.vis-area { min-height: 120px; padding: 16px 0; }
.code-section { margin-top: 16px; }
.phase-tag { display:inline-block; padding:2px 10px; border-radius:999px; font-size:12px; font-weight:600; margin-bottom:8px; }
.phase-tag.left { background:#dbeafe; color:#1e40af; }
.phase-tag.right { background:#dcfce7; color:#166534; }
</style>
</head>
<body>
<div class="container">
<h1>🟡 065. 在排序数组中查找元素的第一个和最后一个位置 <span class="badge medium">中等</span></h1>
<p class="subtitle">分类:二分查找 | LeetCode Hot 100</p>
<div class="controls" id="controls">
<label for="inputArea">输入:</label>
<input type="text" id="inputArea" placeholder="nums=[5,7,7,8,8,10], target=8">
<button id="applyBtn" class="primary">生成图解</button>
<select id="exampleSelect"></select>
<span style="flex:1"></span>
<button id="prevBtn">◀ 上一步</button>
<button id="nextBtn">下一步 ▶</button>
<button id="jumpBtn">⏭ 跳到结果</button>
<button id="autoBtn">自动播放</button>
<button id="resetBtn">重置</button>
</div>
<div class="pipeline" id="pipeline"></div>
<div class="hint info" id="hintBox">
<span id="stepInfo"></span><br>
<span id="hintText"></span>
</div>
<div class="panels">
<div class="panel" id="mainPanel">
<h3>📊 可视化</h3>
<div class="vis-area" id="vizArea">点击「生成图解」开始</div>
</div>
<div class="panel-grid">
<div class="panel" id="detailPanel">
<h3>📝 当前步骤详情</h3>
<div id="detailContent">等待开始...</div>
</div>
<div class="panel" id="resultPanel">
<h3>✅ 结果</h3>
<div id="resultContent">等待完成...</div>
</div>
</div>
</div>
<div class="panel code-section">
<h3>💻 参考代码(Python)</h3>
<div id="codeArea"></div>
</div>
<footer>Powered by QwenPaw · 图解算法 · LeetCode Hot 100</footer>
</div>
<script src="../shared/algo-viz.js"></script>
<script>
"use strict";
(function() {
const examples = [
{nums:[5,7,7,8,8,10], target:8, label:'示例1: target=8'},
{nums:[5,7,7,8,8,10], target:6, label:'示例2: target=6'},
{nums:[], target:0, label:'示例3: 空数组'},
];
let numsArr, target, steps, stepCtrl;
function buildSteps(nums, tgt) {
numsArr = nums; target = tgt;
steps = [];
if (nums.length === 0) {
steps.push({phase:'done', stage:'empty', msg:'数组为空,返回 [-1, -1]', leftIdx:-1, rightIdx:-1});
return;
}
// Phase 1: find left boundary
steps.push({phase:'left', stage:'leftInit', msg:`第一步:查找左边界(第一个等于 ${tgt} 的位置)`, left:0, right:nums.length-1, mid:-1, leftIdx:-1, rightIdx:-1});
let lL = 0, lR = nums.length - 1, leftIdx = -1;
while (lL <= lR) {
const mid = Math.floor((lL + lR) / 2);
steps.push({phase:'left', stage:'leftCalc', msg:`左边界搜索:mid=${mid},nums[${mid}]=${nums[mid]}`, left:lL, right:lR, mid, leftIdx:-1, rightIdx:-1});
if (nums[mid] >= tgt) {
const action = nums[mid] === tgt ? `= ${tgt},可能不是最左,继续向左` : `> ${tgt},目标在左侧`;
steps.push({phase:'left', stage:'leftMove', msg:`nums[${mid}]=${nums[mid]} ${action},right = ${mid}-1 = ${mid-1}`, left:lL, right:mid-1, mid, leftIdx:-1, rightIdx:-1});
if (nums[mid] === tgt) leftIdx = mid;
lR = mid - 1;
} else {
steps.push({phase:'left', stage:'leftMove', msg:`nums[${mid}]=${nums[mid]} < ${tgt},目标在右侧,left = ${mid}+1 = ${mid+1}`, left:mid+1, right:lR, mid, leftIdx:-1, rightIdx:-1});
lL = mid + 1;
}
}
steps.push({phase:'left', stage:'leftDone', msg:`左边界搜索完成,leftIdx = ${leftIdx}`, left:lL, right:lR, mid:-1, leftIdx, rightIdx:-1});
if (leftIdx === -1) {
steps.push({phase:'done', stage:'notFound', msg:`未找到 ${tgt},返回 [-1, -1]`, leftIdx:-1, rightIdx:-1});
return;
}
// Phase 2: find right boundary
let rL = 0, rR = nums.length - 1, rightIdx = -1;
steps.push({phase:'right', stage:'rightInit', msg:`第二步:查找右边界(最后一个等于 ${tgt} 的位置)`, left:0, right:nums.length-1, mid:-1, leftIdx, rightIdx:-1});
while (rL <= rR) {
const mid = Math.floor((rL + rR) / 2);
steps.push({phase:'right', stage:'rightCalc', msg:`右边界搜索:mid=${mid},nums[${mid}]=${nums[mid]}`, left:rL, right:rR, mid, leftIdx, rightIdx:-1});
if (nums[mid] <= tgt) {
const action = nums[mid] === tgt ? `= ${tgt},可能不是最右,继续向右` : `< ${tgt},目标在右侧`;
steps.push({phase:'right', stage:'rightMove', msg:`nums[${mid}]=${nums[mid]} ${action},left = ${mid}+1 = ${mid+1}`, left:mid+1, right:rR, mid, leftIdx, rightIdx:-1});
if (nums[mid] === tgt) rightIdx = mid;
rL = mid + 1;
} else {
steps.push({phase:'right', stage:'rightMove', msg:`nums[${mid}]=${nums[mid]} > ${tgt},目标在左侧,right = ${mid}-1 = ${mid-1}`, left:rL, right:mid-1, mid, leftIdx, rightIdx:-1});
rR = mid - 1;
}
}
steps.push({phase:'right', stage:'rightDone', msg:`右边界搜索完成,rightIdx = ${rightIdx}`, left:rL, right:rR, mid:-1, leftIdx, rightIdx});
steps.push({phase:'done', stage:'done', msg:`结果:[${leftIdx}, ${rightIdx}]`, leftIdx, rightIdx});
}
function render(step) {
const s = steps[step];
const arr = numsArr;
if (!arr.length) {
$('vizArea').innerHTML = '<div class="formula-box">数组为空</div>';
$('detailContent').innerHTML = `<div class="calc-block">${s.msg}</div>`;
$('resultContent').innerHTML = '<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">返回 [-1, -1]</div>';
$('hintText').textContent = s.msg;
return;
}
const hl = highlightRange(arr, s.left, s.right, 'cyan');
if (s.mid >= 0 && s.mid < arr.length) hl[s.mid] = 'orange';
// highlight found boundaries
if (s.leftIdx >= 0) hl[s.leftIdx] = 'green';
if (s.rightIdx >= 0 && s.rightIdx !== s.leftIdx) hl[s.rightIdx] = 'green';
const pointers = {};
if (s.left >= 0 && s.left < arr.length) pointers['L'] = s.left;
if (s.right >= 0 && s.right < arr.length) pointers['R'] = s.right;
if (s.mid >= 0 && s.mid < arr.length) pointers['mid'] = s.mid;
let viz = `<div class="phase-tag ${s.phase==='left'?'left':'right'}">${s.phase==='left'?'阶段一:找左边界':s.phase==='right'?'阶段二:找右边界':'完成'}</div>`;
viz += renderArray(arr, {highlights: hl, pointers});
if (s.mid >= 0 && s.mid < arr.length) {
viz += `<div style="margin-top:6px;font-size:13px;">nums[${s.mid}] = ${arr[s.mid]} vs target = ${target} → ${arr[s.mid]===target?'相等':arr[s.mid]<target?'小于':'大于'}</div>`;
}
if (s.leftIdx >= 0 || s.rightIdx >= 0) {
viz += `<div class="current-answer">左边界 = ${s.leftIdx} 右边界 = ${s.rightIdx}</div>`;
}
$('vizArea').innerHTML = viz;
$('detailContent').innerHTML = `<div class="calc-block">${s.msg}</div>`;
if (s.stage === 'done') {
$('resultContent').innerHTML = `<div class="final-answer">返回 <b>[${s.leftIdx}, ${s.rightIdx}]</b><br>时间复杂度 O(log n)</div>`;
} else if (s.stage === 'notFound') {
$('resultContent').innerHTML = `<div class="final-answer" style="border-color:#f87171;background:#fef2f2;">返回 <b>[-1, -1]</b></div>`;
}
$('hintText').textContent = s.msg;
const stages = ['leftInit→左搜索','leftCalc→左计算','leftMove→左移动','leftDone→左完成','rightInit→右搜索','rightCalc→右计算','rightMove→右移动','rightDone→右完成','done→完成','notFound→未找到'];
$('pipeline').innerHTML = stages.map(st => {
const [k,l] = st.split('→');
return `<span class="pipe-step ${s.stage===k?'active':''}">${l}</span>`;
}).join('<i>→</i>');
}
function init() {
const sel = $('exampleSelect');
examples.forEach((e,i) => { sel.innerHTML += `<option value="${i}">${e.label}</option>`; });
$('inputArea').value = 'nums=[5,7,7,8,8,10], target=8';
buildSteps(examples[0].nums, examples[0].target);
stepCtrl = new StepController({onStep: render});
stepCtrl.setSteps(steps.map((_,i)=>i));
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
stepCtrl.onStep = (idx) => { render(idx); $('stepInfo').textContent = `步骤 ${idx+1} / ${steps.length}`; };
$('applyBtn').onclick = () => {
try {
const m = $('inputArea').value.match(/nums=\[([^\]]+)\].*target=(-?\d+)/);
if (!m) { alert('格式: nums=[5,7,7,8,8,10], target=8'); return; }
const arr = m[1].split(',').map(Number);
const tgt = parseInt(m[2]);
buildSteps(arr, tgt);
stepCtrl.setSteps(steps.map((_,i)=>i)); render(0);
$('stepInfo').textContent = `步骤 1 / ${steps.length}`;
} catch(e) { alert('输入格式错误'); }
};
$('exampleSelect').onchange = () => {
const e = examples[parseInt($('exampleSelect').value)];
$('inputArea').value = `nums=[${e.nums}], target=${e.target}`;
buildSteps(e.nums, e.target);
stepCtrl.setSteps(steps.map((_,i)=>i)); render(0);
};
$('prevBtn').onclick = () => stepCtrl.prev();
$('nextBtn').onclick = () => stepCtrl.next();
$('jumpBtn').onclick = () => stepCtrl.jumpToEnd();
$('autoBtn').onclick = () => { const on = stepCtrl.toggleAuto(); $('autoBtn').textContent = on ? '暂停' : '自动播放'; };
$('resetBtn').onclick = () => { stepCtrl.reset(); $('autoBtn').textContent = '自动播放'; };
}
init();
$('codeArea').innerHTML = renderCode(`def searchRange(nums, target):
def findLeft():
l, r = 0, len(nums) - 1
idx = -1
while l <= r:
mid = (l + r) // 2
if nums[mid] >= target:
if nums[mid] == target:
idx = mid
r = mid - 1
else:
l = mid + 1
return idx
def findRight():
l, r = 0, len(nums) - 1
idx = -1
while l <= r:
mid = (l + r) // 2
if nums[mid] <= target:
if nums[mid] == target:
idx = mid
l = mid + 1
else:
r = mid - 1
return idx
return [findLeft(), findRight()]`, {lang:'Python'});
})();
</script>
</body>
</html>