加时间复杂度

This commit is contained in:
2026-06-16 10:23:48 +08:00
parent daf9e50938
commit 0255ceba02
5 changed files with 878 additions and 5 deletions
+12
View File
@@ -16,11 +16,23 @@ export const chapters = [
'递归算法复杂度',
'实验对比分析方法'
],
subfolders: [
{
name: 'complexity-demo',
label: '🧪 时间复杂度实验',
demo: { path: '/complexity-demo', label: '📊 动态演示', description: '通过动画直观理解 O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(2ⁿ) 的增长差异' },
files: []
},
{
name: 'code-examples',
label: '代码示例',
files: [
{ path: 'c/ch1/first.c', name: 'first.c', label: '递归打印图案', description: '递归打印矩形/三角形图案,演示递归基本结构', pyPath: 'py/ch1/first.py' },
{ path: 'c/ch1/compare.c', name: 'compare.c', label: '性能对比(累加 vs 高斯公式)', description: '比较循环累加与高斯求和公式的耗时差异,直观感受算法效率', pyPath: 'py/ch1/compare.py' },
{ path: 'c/ch1/compareall.c', name: 'compareall.c', label: '多项性能对比', description: '更多累加与高斯公式的计时对比实验' }
]
}
]
},
{
id: 'ch2',
+6
View File
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import ChapterView from '../views/ChapterView.vue'
import SortDemo from '../views/SortDemo.vue'
import ComplexityDemo from '../views/ComplexityDemo.vue'
const routes = [
{
@@ -19,6 +20,11 @@ const routes = [
path: '/sort-demo',
name: 'SortDemo',
component: SortDemo
},
{
path: '/complexity-demo',
name: 'ComplexityDemo',
component: ComplexityDemo
}
]
+2
View File
@@ -170,6 +170,8 @@ function closeDrawer() {
function getFolderDescription(folderName) {
const descriptions = {
'complexity-demo': '通过交互式动画,直观体验不同时间复杂度算法的执行过程与操作次数差异。',
'code-examples': '本章相关的 C 语言和 Python 代码示例。',
mergesort: '归并排序是分治法的经典应用:将数组一分为二,分别排序后合并。时间复杂度 O(n log n)。',
quicksort: '快速排序通过选择一个基准元素,将数组分为左右两部分,递归排序。平均时间复杂度 O(n log n)。',
halfsearch: '二分查找在有序数组中通过每次将搜索范围缩小一半来查找目标值,时间复杂度 O(log n)。',
+853
View File
@@ -0,0 +1,853 @@
<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>
+1 -1
View File
@@ -8,7 +8,7 @@ export default defineConfig({
port: 1025,
proxy: {
'/api': {
target: 'http://localhost:3001',
target: 'http://localhost:3002',
changeOrigin: true,
},
},