Files
suanfa/src/views/ComplexityDemo.vue
T
2026-06-16 10:23:48 +08:00

854 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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.
<template>
<div class="complexity-demo">
<header class="demo-header">
<div class="demo-icon">📊</div>
<div>
<h1 class="demo-title">时间复杂度动态演示</h1>
<p class="demo-subtitle">通过动画和计数直观理解不同算法的时间复杂度</p>
</div>
</header>
<!-- 控制面板 -->
<section class="ctrl-section">
<div class="ctrl-row">
<label class="ctrl-label">输入规模 n</label>
<input type="range" min="1" max="100" v-model.number="n" class="n-slider" />
<span class="n-value">{{ n }}</span>
</div>
<div class="ctrl-row">
<label class="ctrl-label">选择算法</label>
<select v-model="selectedAlgo" class="algo-select">
<option v-for="a in algorithms" :key="a.id" :value="a.id">
{{ a.label }} {{ a.complexity }}
</option>
</select>
</div>
<div class="ctrl-actions">
<button class="ctrl-btn" @click="run" :disabled="running"> 运行演示</button>
<button class="ctrl-btn" @click="stepForward" :disabled="running || stepIndex >= totalSteps - 1">下一步</button>
<button class="ctrl-btn" @click="resetDemo"> 重置</button>
</div>
</section>
<!-- 算法说明 -->
<section class="info-section" v-if="currentAlgo">
<div class="algo-info-card">
<h3>{{ currentAlgo.label }}</h3>
<div class="complexity-badges">
<span class="badge time-badge">时间复杂度{{ currentAlgo.complexity }}</span>
<span class="badge space-badge">空间复杂度{{ currentAlgo.space }}</span>
</div>
<p>{{ currentAlgo.description }}</p>
<div class="op-count" v-if="opCount !== null">
操作次数<strong>{{ opCount }}</strong>
<span class="op-formula"> {{ currentAlgo.formula.replace('n', n) }}</span>
</div>
</div>
</section>
<!-- 动态演示区域 -->
<section class="demo-section" v-if="currentAlgo">
<div class="visualization-area">
<!-- 数组可视化 -->
<div class="array-display" v-if="arrayData.length">
<div class="array-label">数据状态</div>
<div class="array-bars">
<div
v-for="(val, idx) in arrayData"
:key="idx"
class="array-bar-wrapper"
:style="{ height: barHeight(val) + 'px' }"
>
<div
class="array-bar"
:class="barClass(idx)"
:style="{ height: '100%' }"
>
<span class="bar-val">{{ val }}</span>
</div>
</div>
</div>
</div>
<!-- 日志/步骤说明 -->
<div class="step-log" v-if="steps.length">
<div class="log-header">📝 执行步骤</div>
<div class="log-body" ref="logRef">
<div
v-for="(step, idx) in visibleSteps"
:key="idx"
class="log-line"
:class="{ current: idx === stepIndex, done: idx < stepIndex }"
>
<span class="log-step-num">{{ idx + 1 }}</span>
<span class="log-text">{{ step }}</span>
</div>
</div>
</div>
<!-- 复杂度对比图表 -->
<div class="chart-section" v-if="showChart">
<div class="chart-header">📈 复杂度增长对比</div>
<div class="chart-canvas">
<div class="chart-row" v-for="item in chartData" :key="item.label">
<div class="chart-label">{{ item.label }}</div>
<div class="chart-bar-track">
<div
class="chart-bar"
:style="{ width: item.percent + '%', backgroundColor: item.color }"
>
<span class="chart-bar-text" v-if="item.percent > 15">{{ item.count }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="nav-back">
<router-link to="/chapter/ch1" class="back-link"> 返回第一章</router-link>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const n = ref(10)
const selectedAlgo = ref('linear')
const running = ref(false)
const opCount = ref(null)
const steps = ref([])
const stepIndex = ref(-1)
const arrayData = ref([])
const highlights = ref({})
const showChart = ref(false)
const logRef = ref(null)
const algorithms = [
{
id: 'constant',
label: '常数阶 — 数组随机访问',
complexity: 'O(1)',
space: 'O(1)',
formula: '1',
description: '直接通过索引访问数组元素,无论数组多大,执行时间恒定。这是最优的时间复杂度。'
},
{
id: 'logarithmic',
label: '对数阶 — 二分查找',
complexity: 'O(log n)',
space: 'O(1)',
formula: '⌈log₂(n)⌉ = ',
description: '每次将搜索范围缩小一半。即使 n 很大,需要的步数也增长很慢。例如 n=1024 时只需 10 步。'
},
{
id: 'linear',
label: '线性阶 — 线性查找',
complexity: 'O(n)',
space: 'O(1)',
formula: 'n = ',
description: '顺序遍历所有元素。执行时间与输入规模成正比,是最直观的算法复杂度。'
},
{
id: 'nlogn',
label: '线性对数阶 — 归并排序分解',
complexity: 'O(n log n)',
space: 'O(n)',
formula: 'n·⌈log₂(n)⌉ = ',
description: '常见的"分治"算法复杂度。每一层处理 n 个元素,共有 log n 层。'
},
{
id: 'quadratic',
label: '平方阶 — 冒泡排序',
complexity: 'O(n²)',
space: 'O(1)',
formula: 'n(n-1)/2 = ',
description: '双重循环嵌套,最常见于简单排序算法。当 n 翻倍时,运行时间变为原来的 4 倍。'
},
{
id: 'exponential',
label: '指数阶 — 递归斐波那契',
complexity: 'O(2ⁿ)',
space: 'O(n)',
formula: '2ⁿ = ',
description: '递归树呈指数增长。n 稍大时就会爆炸,因此实用中必须优化(如动态规划)。'
}
]
const currentAlgo = computed(() => algorithms.find(a => a.id === selectedAlgo.value))
const totalSteps = computed(() => steps.value.length)
const visibleSteps = computed(() => {
// 最多显示最近 50 步
if (stepIndex.value < 50) return steps.value.slice(0, Math.max(stepIndex.value + 1, 0))
return steps.value.slice(stepIndex.value - 49, stepIndex.value + 1)
})
const maxVal = computed(() => {
if (!arrayData.value.length) return 1
return Math.max(...arrayData.value, 1)
})
function barHeight(val) {
return Math.max(20, (val / maxVal.value) * 200)
}
function barClass(idx) {
const h = highlights.value || {}
const classes = []
if (h.sorted?.includes(idx)) classes.push('bar-sorted')
if (h.comparing?.includes(idx)) classes.push('bar-comparing')
if (h.current === idx) classes.push('bar-current')
return classes
}
function resetDemo() {
running.value = false
opCount.value = null
steps.value = []
stepIndex.value = -1
arrayData.value = []
highlights.value = {}
showChart.value = false
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
async function run() {
if (running.value) return
running.value = true
showChart.value = false
steps.value = []
stepIndex.value = -1
opCount.value = 0
highlights.value = {}
const algo = selectedAlgo.value
switch (algo) {
case 'constant': await runConstant(); break
case 'logarithmic': await runLogarithmic(); break
case 'linear': await runLinear(); break
case 'nlogn': await runNlogn(); break
case 'quadratic': await runQuadratic(); break
case 'exponential': await runExponential(); break
}
running.value = false
showChart.value = true
scrollToLogBottom()
}
function scrollToLogBottom() {
setTimeout(() => {
if (logRef.value) {
logRef.value.scrollTop = logRef.value.scrollHeight
}
}, 50)
}
function addStep(msg) {
steps.value.push(msg)
stepIndex.value = steps.value.length - 1
}
// O(1) — 数组随机访问
async function runConstant() {
const arr = Array.from({ length: n.value }, () => Math.floor(Math.random() * 100) + 1)
arrayData.value = arr
addStep(`生成包含 ${n.value} 个元素的数组`)
await sleep(300)
const idx = Math.floor(Math.random() * n.value)
highlights.value = { current: idx }
addStep(`直接通过索引 arr[${idx}] 访问元素,执行 1 次操作`)
await sleep(600)
opCount.value = 1
addStep(`✅ 无论 n 多大,始终只需 1 次操作 → O(1)`)
}
// O(log n) — 二分查找
async function runLogarithmic() {
const arr = Array.from({ length: n.value }, (_, i) => i + 1)
arrayData.value = arr
const target = Math.floor(Math.random() * n.value) + 1
addStep(`有序数组 [1..${n.value}],查找目标值 ${target}`)
await sleep(400)
let left = 0, right = n.value - 1
let count = 0
while (left <= right) {
count++
const mid = Math.floor((left + right) / 2)
highlights.value = { comparing: [left, right], current: mid }
addStep(`${count} 次:区间 [${left + 1}, ${right + 1}],中间索引 ${mid + 1} (值=${arr[mid]})`)
await sleep(500)
if (arr[mid] === target) {
highlights.value = { sorted: [mid], current: mid }
addStep(`✅ 找到目标 ${target} 在位置 ${mid + 1},共 ${count} 次比较`)
opCount.value = count
return
} else if (arr[mid] < target) {
left = mid + 1
addStep(` ${arr[mid]} < ${target},搜右侧`)
} else {
right = mid - 1
addStep(` ${arr[mid]} > ${target},搜左侧`)
}
await sleep(400)
}
addStep(`❌ 未找到,共 ${count} 次比较`)
opCount.value = count
}
// O(n) — 线性查找
async function runLinear() {
const arr = Array.from({ length: n.value }, () => Math.floor(Math.random() * 100))
arrayData.value = arr
const target = Math.floor(Math.random() * 100)
addStep(`数组长度=${n.value},查找目标值 ${target}`)
await sleep(400)
let count = 0
for (let i = 0; i < n.value; i++) {
count++
highlights.value = { current: i }
addStep(`${count} 次:比较 arr[${i}] = ${arr[i]} ${arr[i] === target ? '✅ 找到' : ''}`)
await sleep(200)
if (arr[i] === target) {
highlights.value = { sorted: [i] }
addStep(`✅ 找到目标 ${target} 在位置 ${i + 1},共 ${count} 次比较`)
opCount.value = count
return
}
}
addStep(`❌ 未找到,共 ${count} 次比较`)
opCount.value = count
}
// O(n log n) — 归并排序分解过程
async function runNlogn() {
const size = Math.min(n.value, 32)
const arr = Array.from({ length: size }, () => Math.floor(Math.random() * 100))
arrayData.value = arr
addStep(`数组 [${arr.join(', ')}] (n=${size})`)
await sleep(400)
let count = 0
async function mergeSort(arr, left, right, depth = 0) {
if (left >= right) return
const mid = Math.floor((left + right) / 2)
count++
highlights.value = { range: [left, right], current: mid }
addStep(` 分解 [${left + 1}..${right + 1}] → 左[${left + 1}..${mid + 1}] 右[${mid + 2}..${right + 1}]`)
await sleep(300)
await mergeSort(arr, left, mid, depth + 1)
await mergeSort(arr, mid + 1, right, depth + 1)
// 合并
const temp = []
let i = left, j = mid + 1
while (i <= mid && j <= right) {
count++
if (arr[i] <= arr[j]) temp.push(arr[i++])
else temp.push(arr[j++])
}
while (i <= mid) { temp.push(arr[i++]); count++ }
while (j <= right) { temp.push(arr[j++]); count++ }
for (let k = left; k <= right; k++) {
arr[k] = temp[k - left]
highlights.value = { sorted: Array.from({ length: right + 1 }, (_, i) => i).slice(0, k + 1) }
}
addStep(` 合并完成 [${left + 1}..${right + 1}]`)
await sleep(300)
}
await mergeSort(arr, 0, size - 1)
arrayData.value = [...arr]
const nlogn = Math.ceil(size * Math.log2(size))
opCount.value = count
addStep(`✅ 归并排序完成,总操作约 ${count} 次,理论值 O(n·log₂n) ≈ ${nlogn}`)
}
// O(n²) — 冒泡排序
async function runQuadratic() {
const size = Math.min(n.value, 20)
const arr = Array.from({ length: size }, () => Math.floor(Math.random() * 100))
arrayData.value = arr
addStep(`初始数组 [${arr.join(', ')}] (n=${size})`)
await sleep(400)
let count = 0
const len = arr.length
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
count++
highlights.value = { comparing: [j, j + 1] }
addStep(`比较 arr[${j}]=${arr[j]} 和 arr[${j + 1}]=${arr[j + 1]}`)
await sleep(200)
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
highlights.value = { swapping: [j, j + 1] }
addStep(` 交换 → [${arr.join(', ')}]`)
await sleep(200)
}
}
highlights.value = { sorted: Array.from({ length: len }, (_, k) => k).slice(len - 1 - i) }
}
arrayData.value = [...arr]
const expected = size * (size - 1) / 2
opCount.value = count
addStep(`✅ 冒泡排序完成,共 ${count} 次比较,理论值 n(n-1)/2 = ${expected}`)
}
// O(2ⁿ) — 递归斐波那契
async function runExponential() {
const size = Math.min(n.value, 10) // 限制,防止浏览器卡死
addStep(`递归计算 Fibonacci(${size}),展示递归调用树`)
await sleep(400)
let count = 0
let callStack = []
function fib(k) {
count++
if (k <= 1) {
addStep(` fib(${k}) = ${k} (基准情况)`)
return k
}
addStep(` fib(${k}) = fib(${k - 1}) + fib(${k - 2})`)
const a = fib(k - 1)
const b = fib(k - 2)
const result = a + b
addStep(` fib(${k}) = ${a} + ${b} = ${result}`)
return result
}
const result = fib(size)
const expected = Math.pow(2, size)
opCount.value = count
addStep(`✅ Fibonacci(${size}) = ${result},递归调用 ${count} 次,理论 O(2ⁿ) ≈ ${expected}`)
}
// 单步前进
function stepForward() {
if (stepIndex.value < totalSteps.value - 1) {
stepIndex.value++
scrollToLogBottom()
}
}
// 对比图表数据
const chartData = computed(() => {
const currentN = n.value
const items = [
{ label: 'O(1)', count: 1, color: '#22c55e' },
{ label: 'O(log n)', count: Math.ceil(Math.log2(currentN)), color: '#3b82f6' },
{ label: 'O(n)', count: currentN, color: '#f59e0b' },
{ label: 'O(n log n)', count: Math.ceil(currentN * Math.log2(currentN)), color: '#f97316' },
{ label: 'O(n²)', count: currentN * currentN, color: '#ef4444' },
{ label: 'O(2ⁿ)', count: Math.min(Math.pow(2, currentN), 999999), color: '#dc2626' }
]
const maxCount = Math.max(...items.map(i => i.count), 1)
return items.map(item => ({
...item,
percent: Math.min((item.count / maxCount) * 100, 100)
}))
})
</script>
<style scoped>
.complexity-demo {
max-width: 1000px;
margin: 0 auto;
padding: 0 24px;
}
.demo-header {
display: flex;
gap: 16px;
align-items: flex-start;
padding: 28px 0 20px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 24px;
}
.demo-icon {
font-size: 40px;
flex-shrink: 0;
}
.demo-title {
font-size: 26px;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 4px;
}
.demo-subtitle {
font-size: 14px;
color: var(--text-secondary);
}
/* 控制面板 */
.ctrl-section {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
}
.ctrl-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.ctrl-label {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
min-width: 100px;
}
.n-slider {
flex: 1;
max-width: 300px;
accent-color: var(--primary-color);
}
.n-value {
font-family: var(--mono);
font-size: 18px;
font-weight: 700;
color: var(--primary-color);
min-width: 40px;
}
.algo-select {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: var(--bg);
color: var(--text-primary);
font-size: 14px;
flex: 1;
max-width: 400px;
cursor: pointer;
}
.ctrl-actions {
display: flex;
gap: 10px;
}
.ctrl-btn {
padding: 8px 20px;
border-radius: 8px;
border: 1px solid var(--border-color);
background: var(--bg);
color: var(--text-primary);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.ctrl-btn:hover:not(:disabled) {
background: var(--hover-bg);
border-color: var(--primary-color);
}
.ctrl-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ctrl-btn:first-child {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.ctrl-btn:first-child:hover:not(:disabled) {
opacity: 0.9;
}
/* 算法说明 */
.info-section {
margin-bottom: 24px;
}
.algo-info-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
}
.algo-info-card h3 {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 10px;
}
.complexity-badges {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.badge {
font-size: 13px;
font-weight: 600;
padding: 4px 12px;
border-radius: 6px;
}
.time-badge {
background: #eff6ff;
color: #2563eb;
}
.space-badge {
background: #f0fdf4;
color: #16a34a;
}
.algo-info-card p {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 10px;
}
.op-count {
font-size: 15px;
color: var(--text-primary);
padding: 8px 14px;
background: var(--hover-bg);
border-radius: 8px;
}
.op-formula {
font-family: var(--mono);
font-size: 13px;
color: var(--text-tertiary);
margin-left: 8px;
}
/* 可视化区域 */
.visualization-area {
display: flex;
flex-direction: column;
gap: 20px;
}
.array-display {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
}
.array-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 12px;
}
.array-bars {
display: flex;
align-items: flex-end;
gap: 3px;
min-height: 220px;
}
.array-bar-wrapper {
flex: 1;
display: flex;
align-items: flex-end;
justify-content: center;
min-width: 12px;
}
.array-bar {
width: 100%;
background: var(--primary-color);
border-radius: 4px 4px 0 0;
transition: all 0.3s ease;
display: flex;
align-items: flex-end;
justify-content: center;
min-height: 20px;
}
.bar-val {
font-size: 10px;
color: white;
font-weight: 600;
padding: 2px 0;
}
.bar-current {
background: #f59e0b;
}
.bar-comparing {
background: #60a5fa;
}
.bar-sorted {
background: #22c55e;
}
/* 步骤日志 */
.step-log {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.log-header {
padding: 12px 16px;
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
background: var(--hover-bg);
}
.log-body {
max-height: 300px;
overflow-y: auto;
padding: 8px 0;
}
.log-line {
padding: 5px 16px;
font-size: 13px;
font-family: var(--mono);
color: var(--text-tertiary);
line-height: 1.5;
border-left: 3px solid transparent;
}
.log-line.current {
color: var(--text-primary);
background: var(--active-bg);
border-left-color: var(--primary-color);
}
.log-line.done {
color: var(--text-secondary);
}
.log-step-num {
display: inline-block;
min-width: 28px;
color: var(--text-tertiary);
font-size: 11px;
}
/* 对比图表 */
.chart-section {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
}
.chart-header {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
}
.chart-canvas {
display: flex;
flex-direction: column;
gap: 10px;
}
.chart-row {
display: flex;
align-items: center;
gap: 12px;
}
.chart-label {
min-width: 90px;
font-size: 13px;
font-weight: 600;
font-family: var(--mono);
color: var(--text-primary);
}
.chart-bar-track {
flex: 1;
height: 28px;
background: var(--hover-bg);
border-radius: 6px;
overflow: hidden;
}
.chart-bar {
height: 100%;
border-radius: 6px;
display: flex;
align-items: center;
padding-left: 8px;
transition: width 0.5s ease;
min-width: fit-content;
}
.chart-bar-text {
font-size: 12px;
font-weight: 600;
color: white;
white-space: nowrap;
}
.nav-back {
margin: 32px 0;
}
.back-link {
display: inline-block;
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
font-size: 14px;
}
.back-link:hover {
text-decoration: underline;
}
@media (prefers-color-scheme: dark) {
.time-badge { background: #1e3a5f; color: #60a5fa; }
.space-badge { background: #14532d; color: #4ade80; }
}
</style>