This commit is contained in:
2026-06-15 09:00:38 +08:00
parent fec66377d5
commit 4640c5e02b
191 changed files with 6046 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import Sidebar from './components/Sidebar.vue'
const route = useRoute()
const sidebarCollapsed = ref(false)
const currentChapterId = computed(() => {
if (route.name === 'Chapter') return route.params.id
return null
})
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
</script>
<template>
<div class="app-layout">
<Sidebar
:collapsed="sidebarCollapsed"
:currentId="currentChapterId"
@toggle="toggleSidebar"
/>
<main class="main-content">
<router-view />
</main>
<button class="back-to-top" @click="scrollToTop" v-show="showBackToTop"></button>
</div>
</template>
<script>
export default {
data() {
return {
showBackToTop: false
}
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
},
beforeUnmount() {
window.removeEventListener('scroll', this.handleScroll)
},
methods: {
handleScroll() {
this.showBackToTop = window.scrollY > 300
},
scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}
}
</script>
+53
View File
@@ -0,0 +1,53 @@
import { describe, it, expect } from 'vitest'
import { chapters, getAllFiles, getChapterById } from '../data/chapters.js'
describe('chapters data', () => {
it('should have 6 chapters', () => {
expect(chapters.length).toBe(6)
})
it('each chapter should have required fields', () => {
for (const ch of chapters) {
expect(ch.id).toBeTruthy()
expect(ch.title).toBeTruthy()
expect(ch.subtitle).toBeTruthy()
expect(ch.description).toBeTruthy()
expect(ch.icon).toBeTruthy()
expect(Array.isArray(ch.topics)).toBe(true)
}
})
it('chapter ids should be ch1 through ch6', () => {
const ids = chapters.map(ch => ch.id)
expect(ids).toEqual(['ch1', 'ch2', 'ch3', 'ch4', 'ch5', 'ch6'])
})
it('getChapterById should find correct chapter', () => {
const ch3 = getChapterById('ch3')
expect(ch3.title).toContain('动态规划')
expect(getChapterById('nonexistent')).toBeUndefined()
})
it('each chapter should have at least one topic', () => {
for (const ch of chapters) {
expect(ch.topics.length).toBeGreaterThan(0)
}
})
})
describe('getAllFiles', () => {
it('should return all files flattened', () => {
const files = getAllFiles()
expect(files.length).toBeGreaterThan(0)
})
it('each file should have path, name, label', () => {
const files = getAllFiles()
for (const f of files) {
expect(f.path).toBeTruthy()
expect(f.name).toBeTruthy()
expect(f.label).toBeTruthy()
expect(f.chapterId).toBeTruthy()
}
})
})
+118
View File
@@ -0,0 +1,118 @@
import { describe, it, expect } from 'vitest'
import { generateMergeSortSteps, generateQuickSortSteps, generateRandomArray } from '../utils/sortAnimations.js'
describe('generateMergeSortSteps', () => {
it('should produce steps for a small array', () => {
const arr = [3, 1, 2]
const steps = generateMergeSortSteps(arr)
expect(steps.length).toBeGreaterThan(0)
// 最后一步应标记全部已排序
const last = steps[steps.length - 1]
expect(last.highlights.sorted).toEqual([0, 1, 2])
expect(last.description).toContain('完成')
})
it('should sort the array correctly', () => {
const arr = [5, 3, 8, 1, 9, 2]
const steps = generateMergeSortSteps(arr)
const last = steps[steps.length - 1]
// 检查最终数组有序
const sorted = [...last.array].sort((a, b) => a - b)
expect(last.array).toEqual(sorted)
})
it('should handle single element array', () => {
const steps = generateMergeSortSteps([42])
expect(steps.length).toBeGreaterThan(0)
expect(steps[steps.length - 1].highlights.sorted).toEqual([0])
})
it('should handle empty array', () => {
const steps = generateMergeSortSteps([])
expect(steps.length).toBeGreaterThan(0)
})
it('should have description for every step', () => {
const steps = generateMergeSortSteps([4, 2, 7, 1])
for (const step of steps) {
expect(step.description).toBeTruthy()
expect(typeof step.description).toBe('string')
}
})
it('should record array state at each step', () => {
const arr = [4, 2, 7, 1]
const steps = generateMergeSortSteps(arr)
for (const step of steps) {
expect(Array.isArray(step.array)).toBe(true)
expect(step.array.length).toBe(arr.length)
}
})
})
describe('generateQuickSortSteps', () => {
it('should produce steps for a small array', () => {
const arr = [3, 1, 2]
const steps = generateQuickSortSteps(arr)
expect(steps.length).toBeGreaterThan(0)
const last = steps[steps.length - 1]
expect(last.highlights.sorted).toEqual([0, 1, 2])
expect(last.description).toContain('完成')
})
it('should sort the array correctly', () => {
const arr = [5, 3, 8, 1, 9, 2]
const steps = generateQuickSortSteps(arr)
const last = steps[steps.length - 1]
const sorted = [...last.array].sort((a, b) => a - b)
expect(last.array).toEqual(sorted)
})
it('should handle single element array', () => {
const steps = generateQuickSortSteps([42])
expect(steps.length).toBeGreaterThan(0)
expect(steps[steps.length - 1].highlights.sorted).toEqual([0])
})
it('should handle already sorted array', () => {
const arr = [1, 2, 3, 4, 5]
const steps = generateQuickSortSteps(arr)
const last = steps[steps.length - 1]
expect(last.array).toEqual([1, 2, 3, 4, 5])
})
it('should handle reverse sorted array', () => {
const arr = [5, 4, 3, 2, 1]
const steps = generateQuickSortSteps(arr)
const last = steps[steps.length - 1]
expect(last.array).toEqual([1, 2, 3, 4, 5])
})
it('should have description for every step', () => {
const steps = generateQuickSortSteps([4, 2, 7, 1])
for (const step of steps) {
expect(step.description).toBeTruthy()
}
})
})
describe('generateRandomArray', () => {
it('should generate array of correct size', () => {
const arr = generateRandomArray(10)
expect(arr.length).toBe(10)
})
it('should generate positive numbers', () => {
const arr = generateRandomArray(20)
for (const v of arr) {
expect(v).toBeGreaterThanOrEqual(1)
}
})
it('should respect maxValue', () => {
const arr = generateRandomArray(50, 10)
for (const v of arr) {
expect(v).toBeLessThanOrEqual(10)
}
})
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

+180
View File
@@ -0,0 +1,180 @@
<template>
<div class="code-viewer">
<div class="code-toolbar">
<div class="file-info">
<span class="file-icon">📄</span>
<span class="file-name">{{ fileName }}</span>
<span v-if="fileLabel" class="file-label">{{ fileLabel }}</span>
</div>
<div class="toolbar-actions">
<span v-if="description" class="file-desc-tip" :title="description"></span>
<button class="copy-btn" @click="copyCode" :title="copied ? '已复制!' : '复制代码'">
{{ copied ? '✅' : '📋' }}
</button>
</div>
</div>
<div class="code-content" ref="codeRef">
<pre><code v-html="highlightedCode"></code></pre>
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">加载中...</div>
</div>
<div v-if="error" class="error-overlay">
<p> {{ error }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { loadCodeFile, highlightC } from '../utils/codeUtils.js'
const props = defineProps({
filePath: String,
fileName: String,
fileLabel: String,
description: String
})
const codeContent = ref('')
const loading = ref(false)
const error = ref('')
const copied = ref(false)
const highlightedCode = computed(() => {
if (!codeContent.value) return ''
return highlightC(codeContent.value)
})
async function loadCode() {
if (!props.filePath) return
loading.value = true
error.value = ''
codeContent.value = ''
try {
codeContent.value = await loadCodeFile(props.filePath)
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
watch(() => props.filePath, loadCode, { immediate: true })
async function copyCode() {
try {
await navigator.clipboard.writeText(codeContent.value)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
} catch {
// fallback
const textarea = document.createElement('textarea')
textarea.value = codeContent.value
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
copied.value = true
setTimeout(() => { copied.value = false }, 2000)
}
}
</script>
<style scoped>
.code-viewer {
background: var(--code-bg);
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border-color);
margin-bottom: 20px;
}
.code-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: var(--code-header-bg);
border-bottom: 1px solid var(--border-color);
}
.file-info {
display: flex;
align-items: center;
gap: 8px;
}
.file-icon {
font-size: 16px;
}
.file-name {
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
font-size: 14px;
font-weight: 600;
color: var(--primary-color);
}
.file-label {
font-size: 12px;
color: var(--text-tertiary);
background: var(--tag-bg);
padding: 2px 8px;
border-radius: 4px;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.file-desc-tip {
cursor: help;
font-size: 16px;
}
.copy-btn {
background: var(--hover-bg);
border: 1px solid var(--border-color);
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.copy-btn:hover {
background: var(--active-bg);
}
.code-content {
position: relative;
overflow-x: auto;
}
.code-content pre {
padding: 16px;
margin: 0;
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.6;
color: var(--code-text);
}
.code-content code {
font-family: inherit;
}
.loading-overlay,
.error-overlay {
padding: 20px;
text-align: center;
color: var(--text-tertiary);
}
.error-overlay {
color: #e74c3c;
}
</style>
+95
View File
@@ -0,0 +1,95 @@
<script setup>
import { ref } from 'vue'
import viteLogo from '../assets/vite.svg'
import heroImg from '../assets/hero.png'
import vueLogo from '../assets/vue.svg'
const count = ref(0)
</script>
<template>
<section id="center">
<div class="hero">
<img :src="heroImg" class="base" width="170" height="179" alt="" />
<img :src="vueLogo" class="framework" alt="Vue logo" />
<img :src="viteLogo" class="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
</div>
<button type="button" class="counter" @click="count++">
Count is {{ count }}
</button>
</section>
<div class="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img class="logo" :src="viteLogo" alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://vuejs.org/" target="_blank">
<img class="button-icon" :src="vueLogo" alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg class="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg class="button-icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div class="ticks"></div>
<section id="spacer"></section>
</template>
+166
View File
@@ -0,0 +1,166 @@
<template>
<aside class="sidebar" :class="{ collapsed }">
<div class="sidebar-header">
<h2 class="sidebar-title" @click="$router.push('/')">
<span class="logo">📚</span>
<span v-show="!collapsed">算法分析教学</span>
</h2>
<button class="toggle-btn" @click="$emit('toggle')">
{{ collapsed ? '' : '' }}
</button>
</div>
<nav class="sidebar-nav">
<router-link
v-for="ch in chapters"
:key="ch.id"
:to="`/chapter/${ch.id}`"
class="nav-item"
:class="{ active: currentId === ch.id }"
>
<span class="nav-icon">{{ ch.icon }}</span>
<span class="nav-text" v-show="!collapsed">
<span class="nav-title">{{ ch.title.split('—')[0].trim() }}</span>
<span class="nav-subtitle">{{ ch.subtitle }}</span>
</span>
</router-link>
</nav>
<div class="sidebar-footer" v-show="!collapsed">
<p>算法分析与设计</p>
<p class="footer-sub">教学辅助平台</p>
</div>
</aside>
</template>
<script setup>
import { chapters } from '../data/chapters.js'
defineProps({
collapsed: Boolean,
currentId: String
})
defineEmits(['toggle'])
</script>
<style scoped>
.sidebar {
width: 280px;
min-width: 280px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: all 0.3s ease;
overflow: hidden;
}
.sidebar.collapsed {
width: 60px;
min-width: 60px;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-title {
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
white-space: nowrap;
}
.logo {
font-size: 20px;
}
.toggle-btn {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: var(--text-secondary);
padding: 4px 8px;
border-radius: 4px;
}
.toggle-btn:hover {
background: var(--hover-bg);
}
.sidebar-nav {
flex: 1;
padding: 12px 8px;
overflow-y: auto;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 12px;
margin-bottom: 4px;
border-radius: 8px;
color: var(--text-secondary);
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
}
.nav-item:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.nav-item.active {
background: var(--active-bg);
color: var(--primary-color);
font-weight: 600;
}
.nav-icon {
font-size: 20px;
flex-shrink: 0;
}
.nav-text {
display: flex;
flex-direction: column;
overflow: hidden;
}
.nav-title {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-subtitle {
font-size: 11px;
color: var(--text-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-footer {
padding: 16px;
border-top: 1px solid var(--border-color);
text-align: center;
font-size: 12px;
color: var(--text-tertiary);
}
.footer-sub {
font-size: 10px;
margin-top: 2px;
}
</style>
+463
View File
@@ -0,0 +1,463 @@
<template>
<div class="sort-visualizer">
<div class="viz-header">
<h3 class="viz-title">{{ title }}</h3>
<div class="viz-controls">
<button class="ctrl-btn" @click="randomize" title="随机生成新数组">🎲</button>
<button class="ctrl-btn" @click="reset" title="重置"></button>
<button class="ctrl-btn" @click="prevStep" :disabled="currentStep <= 0" title="上一步"></button>
<button class="ctrl-btn play-btn" @click="togglePlay" :title="isPlaying ? '暂停' : '播放'">
{{ isPlaying ? '⏸' : '▶' }}
</button>
<button class="ctrl-btn" @click="nextStep" :disabled="currentStep >= totalSteps - 1" title="下一步"></button>
<button class="ctrl-btn" @click="goToEnd" :disabled="currentStep >= totalSteps - 1" title="跳到结束"></button>
<span class="step-info">{{ currentStep + 1 }} / {{ totalSteps }}</span>
</div>
<div class="speed-control">
<label>速度</label>
<input type="range" min="1" max="10" v-model.number="speed" class="speed-slider" />
</div>
</div>
<div class="viz-canvas-wrapper">
<div class="viz-canvas" ref="canvasRef">
<div
v-for="(val, idx) in currentState.array"
:key="idx"
class="bar-wrapper"
:style="{ height: barHeight(val) + 'px' }"
>
<div
class="bar"
:class="barClass(idx)"
:style="{ height: '100%' }"
>
<span class="bar-value" v-if="showValues">{{ val }}</span>
</div>
</div>
</div>
</div>
<div class="viz-description">
<span class="desc-text">{{ currentState.description }}</span>
<div class="legend">
<span class="legend-item"><span class="legend-color legend-default"></span>未处理</span>
<span class="legend-item"><span class="legend-color legend-comparing"></span>比较中</span>
<span class="legend-item"><span class="legend-color legend-swapping"></span>交换</span>
<span class="legend-item"><span class="legend-color legend-pivot"></span>基准</span>
<span class="legend-item"><span class="legend-color legend-sorted"></span>已排序</span>
<span class="legend-item"><span class="legend-color legend-merging"></span>归并中</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onBeforeUnmount } from 'vue'
import { generateMergeSortSteps, generateQuickSortSteps, generateRandomArray } from '../utils/sortAnimations.js'
const props = defineProps({
title: { type: String, default: '排序可视化' },
algorithm: { type: String, default: 'merge' }, // 'merge' | 'quick'
arraySize: { type: Number, default: 12 },
showValues: { type: Boolean, default: true }
})
const canvasRef = ref(null)
const currentStep = ref(0)
const isPlaying = ref(false)
const speed = ref(5)
let timer = null
const rawArray = ref(generateRandomArray(props.arraySize))
const steps = ref([])
const totalSteps = computed(() => steps.value.length)
const currentState = computed(() => {
if (steps.value.length === 0) {
return {
array: rawArray.value,
highlights: {},
description: '点击 ▶ 开始演示'
}
}
return steps.value[currentStep.value] || steps.value[0]
})
// 计算最大值用于柱状图高度
const maxVal = computed(() => {
return Math.max(...currentState.value.array, 1)
})
function barHeight(val) {
// 最大高度约 280px
return Math.max(20, (val / maxVal.value) * 260)
}
function barClass(idx) {
const h = currentState.value.highlights || {}
const classes = []
// 已排序(最终位置)
if (h.sorted && h.sorted.includes(idx)) {
classes.push('bar-sorted')
return classes
}
if (h.sortedIndices && h.sortedIndices.includes(idx)) {
classes.push('bar-sorted')
return classes
}
// 基准元素
if (h.pivot === idx) {
classes.push('bar-pivot')
return classes
}
// 交换中
if (h.swapping && h.swapping.includes(idx)) {
classes.push('bar-swapping')
return classes
}
// 比较中
if (h.comparing && h.comparing.includes(idx)) {
classes.push('bar-comparing')
return classes
}
// 归并范围
if (h.merging) {
const l = h.merging.left
const r = h.merging.right
if (idx >= l[0] && idx <= l[1]) classes.push('bar-merging-left')
else if (idx >= r[0] && idx <= r[1]) classes.push('bar-merging-right')
}
// 范围高亮
if (h.range && !classes.length) {
if (idx >= h.range[0] && idx <= h.range[1]) {
classes.push('bar-in-range')
}
}
return classes
}
// 生成动画步骤
function generateSteps() {
if (props.algorithm === 'merge') {
steps.value = generateMergeSortSteps(rawArray.value)
} else {
steps.value = generateQuickSortSteps(rawArray.value)
}
currentStep.value = 0
}
function randomize() {
stopPlay()
rawArray.value = generateRandomArray(props.arraySize)
generateSteps()
}
function reset() {
stopPlay()
rawArray.value = generateRandomArray(props.arraySize)
generateSteps()
}
function nextStep() {
if (currentStep.value < steps.value.length - 1) {
currentStep.value++
} else {
stopPlay()
}
}
function prevStep() {
if (currentStep.value > 0) {
currentStep.value--
}
}
function goToEnd() {
stopPlay()
currentStep.value = steps.value.length - 1
}
function togglePlay() {
if (isPlaying.value) {
stopPlay()
} else {
startPlay()
}
}
function startPlay() {
if (currentStep.value >= steps.value.length - 1) {
currentStep.value = 0
}
isPlaying.value = true
const interval = Math.max(50, 600 - speed.value * 50)
timer = setInterval(() => {
if (currentStep.value < steps.value.length - 1) {
currentStep.value++
} else {
stopPlay()
}
}, interval)
}
function stopPlay() {
isPlaying.value = false
if (timer) {
clearInterval(timer)
timer = null
}
}
onBeforeUnmount(() => {
stopPlay()
})
// 初始化
generateSteps()
</script>
<style scoped>
.sort-visualizer {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
overflow: hidden;
margin-bottom: 24px;
box-shadow: var(--shadow-sm);
}
.viz-header {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 14px 20px;
background: var(--hover-bg);
border-bottom: 1px solid var(--border-color);
}
.viz-title {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin-right: auto;
}
.viz-controls {
display: flex;
align-items: center;
gap: 6px;
}
.ctrl-btn {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 6px 10px;
font-size: 16px;
cursor: pointer;
transition: all 0.15s;
line-height: 1;
}
.ctrl-btn:hover:not(:disabled) {
background: var(--active-bg);
border-color: var(--primary-color);
}
.ctrl-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.play-btn {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.play-btn:hover:not(:disabled) {
opacity: 0.9;
}
.step-info {
font-size: 12px;
color: var(--text-tertiary);
font-family: var(--mono);
min-width: 60px;
text-align: center;
}
.speed-control {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-tertiary);
}
.speed-slider {
width: 60px;
accent-color: var(--primary-color);
}
/* 画布 */
.viz-canvas-wrapper {
padding: 24px 20px 12px;
overflow-x: auto;
}
.viz-canvas {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 3px;
min-height: 300px;
padding: 0 10px;
}
.bar-wrapper {
flex: 1;
min-width: 18px;
max-width: 48px;
display: flex;
align-items: flex-end;
transition: height 0.2s ease;
}
.bar {
width: 100%;
border-radius: 4px 4px 0 0;
background: var(--primary-color);
opacity: 0.75;
display: flex;
align-items: flex-end;
justify-content: center;
transition: all 0.2s ease;
min-height: 4px;
position: relative;
}
.bar-value {
font-size: 10px;
color: white;
font-weight: 700;
position: absolute;
top: -18px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
font-family: var(--mono);
}
/* 状态颜色 */
.bar-comparing {
background: #f59e0b;
opacity: 1;
transform: scaleY(1.02);
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
}
.bar-swapping {
background: #ef4444;
opacity: 1;
transform: scaleY(1.05);
box-shadow: 0 0 12px rgba(239, 68, 68, 0.5);
animation: pulse 0.3s ease;
}
.bar-pivot {
background: #8b5cf6;
opacity: 1;
transform: scaleY(1.05);
box-shadow: 0 0 12px rgba(139, 92, 246, 0.5);
}
.bar-sorted {
background: #22c55e;
opacity: 0.9;
}
.bar-in-range {
opacity: 0.85;
}
.bar-merging-left {
background: #3b82f6;
opacity: 0.9;
}
.bar-merging-right {
background: #06b6d4;
opacity: 0.9;
}
@keyframes pulse {
0%, 100% { transform: scaleY(1.05); }
50% { transform: scaleY(1.15); }
}
/* 描述和图例 */
.viz-description {
padding: 10px 20px 14px;
border-top: 1px solid var(--border-color);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.desc-text {
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
flex: 1;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 11px;
color: var(--text-tertiary);
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 3px;
display: inline-block;
}
.legend-default {
background: var(--primary-color);
opacity: 0.75;
}
.legend-comparing {
background: #f59e0b;
}
.legend-swapping {
background: #ef4444;
}
.legend-pivot {
background: #8b5cf6;
}
.legend-sorted {
background: #22c55e;
}
.legend-merging {
background: #06b6d4;
}
</style>
+366
View File
@@ -0,0 +1,366 @@
/**
* 算法分析教学平台 - 章节与代码文件数据
* 对应教材章节:基础(复杂度分析)、分治法、动态规划、贪心、回溯、分支限界
*/
export const chapters = [
{
id: 'ch1',
title: '第一章 基础 — 复杂度分析',
subtitle: '算法时间复杂度与空间复杂度基础',
description: '本章介绍算法分析的基础知识,包括时间复杂度、空间复杂度的概念,以及通过实验比较不同算法的性能差异。',
icon: '📊',
topics: [
'时间复杂度概念(O、Ω、Θ)',
'空间复杂度分析',
'递归算法复杂度',
'实验对比分析方法'
],
files: [
{ path: 'c/ch1/first.c', name: 'first.c', label: '递归打印图案', description: '递归打印矩形/三角形图案,演示递归基本结构' },
{ path: 'c/ch1/compare.c', name: 'compare.c', label: '性能对比(累加 vs 高斯公式)', description: '比较循环累加与高斯求和公式的耗时差异,直观感受算法效率' },
{ path: 'c/ch1/compareall.c', name: 'compareall.c', label: '多项性能对比', description: '更多累加与高斯公式的计时对比实验' }
]
},
{
id: 'ch2',
title: '第二章 分治法',
subtitle: 'Divide and Conquer — 分而治之',
description: '分治法将大问题分解为若干规模较小的子问题,递归求解后再合并结果。本章涵盖归并排序、快速排序、二分查找、大整数乘法、矩阵乘法等经典分治算法。',
icon: '🔪',
topics: [
'分治策略基本思想',
'归并排序',
'快速排序(多种划分方式)',
'二分查找(递归与迭代)',
'大整数乘法',
'Strassen矩阵乘法',
'递归求最大值',
'全排列生成'
],
subfolders: [
{
name: 'mergesort',
label: '归并排序',
files: [
{ path: 'c/ch2/mergesort/guibing.c', name: 'guibing.c', label: '归并排序完整实现', description: '分治归并排序,包含 merge 与 mergeSort 函数' },
{ path: 'c/ch2/mergesort/merge.c', name: 'merge.c', label: '归并操作', description: '合并两个有序子数组的核心 merge 操作' }
],
demo: { path: '/sort-demo#merge', label: '🎬 动态演示', description: '观看归并排序的完整动画过程' }
},
{
name: 'quicksort',
label: '快速排序',
files: [
{ path: 'c/ch2/quicksort/danppt.c', name: 'danppt.c', label: '快速排序(单侧指针)', description: '单侧指针遍历划分的快速排序实现' },
{ path: 'c/ch2/quicksort/danleft.c', name: 'danleft.c', label: '快速排序(左指针法)', description: '基于左右指针移动的快速排序变体' },
{ path: 'c/ch2/quicksort/dantwo.c', name: 'dantwo.c', label: '快速排序(双指针法)', description: '另一种双指针快速排序实现' },
{ path: 'c/ch2/quicksort/shuangbian.c', name: 'shuangbian.c', label: '快速排序(双边扫描)', description: '经典的双边扫描分区快速排序' }
],
demo: { path: '/sort-demo#quick', label: '🎬 动态演示', description: '观看快速排序的完整动画过程' }
},
{
name: 'halfsearch',
label: '二分查找',
files: [
{ path: 'c/ch2/student/halfsearch.c', name: 'halfsearch.c', label: '二分查找(递归版)', description: '递归实现的二分查找算法' },
{ path: 'c/ch2/student/halfsearchnew.c', name: 'halfsearchnew.c', label: '二分查找(迭代版)', description: '迭代实现的二分查找算法' },
{ path: 'c/ch2/halfsearch/fenzhi.c', name: 'fenzhi.c', label: '分治分割示例', description: '二分分治分割示例' },
{ path: 'c/ch2/halfsearch/fenzhirec.c', name: 'fenzhirec.c', label: '递归查找', description: '递归结构的查找实现' }
]
},
{
name: 'bigcheng',
label: '大整数乘法',
files: [
{ path: 'c/ch2/bigcheng/bigchengold.c', name: 'bigchengold.c', label: '大整数乘法(基础版)', description: '低位进位数组模拟乘法' },
{ path: 'c/ch2/bigcheng/bigchengnew.c', name: 'bigchengnew.c', label: '大整数乘法(分治版)', description: '分治递归的大整数乘法实现' }
]
},
{
name: 'matrix',
label: '矩阵乘法',
files: [
{ path: 'c/ch2/juzhen/juzhenold.c', name: 'juzhenold.c', label: '矩阵乘法(朴素版)', description: '朴素三层循环矩阵乘法' },
{ path: 'c/ch2/juzhen/juzhennew.c', name: 'juzhennew.c', label: '矩阵乘法(分治版)', description: '分治/Strassen风格的矩阵乘法' },
{ path: 'c/ch2/matrix/macheng.c', name: 'macheng.c', label: '分治矩阵乘法', description: '分块递归矩阵乘法' }
]
},
{
name: 'digui',
label: '递归示例',
files: [
{ path: 'c/ch2/digui/printnumber.c', name: 'printnumber.c', label: '递归打印整数', description: '递归按位打印整数(高位到低位)' },
{ path: 'c/ch2/digui/tuxing.c', name: 'tuxing.c', label: '递归图形', description: '递归绘制图形示例' },
{ path: 'c/ch2/shangji/findarrmax.c', name: 'findarrmax.c', label: '递归求最大值', description: '递归查找数组最大值' },
{ path: 'c/ch2/shangji/sanjiao.c', name: 'sanjiao.c', label: '递归三角形', description: '递归打印星号三角形' }
]
},
{
name: 'maopao',
label: '冒泡排序',
files: [
{ path: 'c/ch2/maopao/mp.c', name: 'mp.c', label: '冒泡排序', description: '冒泡排序实现并打印中间状态' }
]
},
{
name: 'allpai',
label: '全排列',
files: [
{ path: 'c/ch2/ch2.allpai/allpai.c', name: 'allpai.c', label: '全排列生成', description: '递归交换法全排列生成' },
{ path: 'c/ch2/ch2.allpai/allpaichong.c', name: 'allpaichong.c', label: '全排列(去重)', description: '带去重逻辑的全排列生成' },
{ path: 'c/ch2/ch2.allpai/arrpaichong.c', name: 'arrpaichong.c', label: '数组排列去重', description: '数组排列生成与去重' },
{ path: 'c/ch2/student/quanpai.c', name: 'quanpai.c', label: '全排列(学生版)', description: '全排列/排列打印程序' },
{ path: 'c/ch2/student/ppp.c', name: 'ppp.c', label: '全排列(ppp', description: '递归交换并打印排列' },
{ path: 'c/ch2/student/paichongright.c', name: 'paichongright.c', label: '去重排列', description: '排除重复输出的排列逻辑' }
]
},
{
name: 'richeng',
label: '日程表',
files: [
{ path: 'c/ch2/richeng/bisan.c', name: 'bisan.c', label: '循环赛日程表', description: '使用2^k分组复制的赛程表逻辑' }
]
}
]
},
{
id: 'ch3',
title: '第三章 动态规划',
subtitle: 'Dynamic Programming — 最优子结构与重叠子问题',
description: '动态规划通过将问题分解为重叠子问题,并利用最优子结构性质,自底向上求解。本章涵盖0/1背包、最长公共子序列(LCS)、矩阵链乘、图像压缩、最少硬币等经典DP问题。',
icon: '🧩',
topics: [
'动态规划基本思想',
'最优子结构与重叠子问题',
'0/1背包问题',
'最长公共子序列(LCS)',
'矩阵链乘',
'图像压缩',
'最少硬币问题',
'杨辉三角'
],
subfolders: [
{
name: 'bag01',
label: '0/1背包问题',
files: [
{ path: 'c/ch3/bag01/bag01.c', name: 'bag01.c', label: '0/1背包 DP 实现', description: '经典二维DP矩阵解法' },
{ path: 'c/ch3/bag01/bagbag.c', name: 'bagbag.c', label: '背包状态追踪', description: '多种背包实现/状态追踪' },
{ path: 'c/ch3/bag01/bagevery.c', name: 'bagevery.c', label: '背包枚举', description: '背包问题枚举/遍历示例' },
{ path: 'c/ch3/bag01/yanghui.c', name: 'yanghui.c', label: '杨辉三角(二维数组)', description: '使用二维数组打印杨辉三角' },
{ path: 'c/ch3/bag01/yanghuiarr.c', name: 'yanghuiarr.c', label: '杨辉三角(动态分配)', description: '用malloc动态分配版杨辉三角' }
]
},
{
name: 'lcs',
label: '最长公共子序列(LCS)',
files: [
{ path: 'c/ch3/lcs/lcs1.c', name: 'lcs1.c', label: 'LCS 实现(一)', description: '构建LCS DP表' },
{ path: 'c/ch3/lcs/lcs2.c', name: 'lcs2.c', label: 'LCS 实现(二)', description: 'LCS动态规划实现,返回长度' },
{ path: 'c/ch3/lcs/printtable.c', name: 'printtable.c', label: 'LCS 路径回溯', description: '打印LCS路径表并回溯输出LCS' }
]
},
{
name: 'matrix',
label: '矩阵算法',
files: [
{ path: 'c/ch3/matrix/chengJia.c', name: 'chengJia.c', label: '矩阵乘法', description: '朴素矩阵乘法示例' },
{ path: 'c/ch3/matrix/matrixmul.c', name: 'matrixmul.c', label: '矩阵乘法(标准)', description: '标准矩阵乘法实现' },
{ path: 'c/ch3/matrix/duijiaoxian.c', name: 'duijiaoxian.c', label: '对角线处理', description: '矩阵对角线特性打印' },
{ path: 'c/ch3/matrix/kuohao.c', name: 'kuohao.c', label: '矩阵括号链乘', description: '矩阵链乘括号化DP实现' },
{ path: 'c/ch3/matrix/weishu.c', name: 'weishu.c', label: '位数计算', description: '数字位数/二进制位宽计算' }
]
},
{
name: 'image',
label: '图像压缩',
files: [
{ path: 'c/ch3/image/imgcompress.c', name: 'imgcompress.c', label: '图像压缩算法', description: '图像压缩段划分与最优分段' },
{ path: 'c/ch3/image/imgcompress0.c', name: 'imgcompress0.c', label: '图像压缩(基础版)', description: '图像压缩段编码示例' },
{ path: 'c/ch3/image/get2Len.c', name: 'get2Len.c', label: '位长计算', description: '计算二进制位长等辅助函数' }
]
},
{
name: 'homework',
label: '课后练习',
files: [
{ path: 'c/ch3/homework/lesscoin.c', name: 'lesscoin.c', label: '最少硬币问题', description: '典型背包/零钱兑换DP实现' }
]
}
]
},
{
id: 'ch4',
title: '第四章 贪心算法',
subtitle: 'Greedy Algorithm — 局部最优与全局最优',
description: '贪心算法在每一步选择中都采取当前最优的选择,希望最终得到全局最优解。本章涵盖活动选择、Huffman编码、背包问题(贪心版)、最优装载、最短路径、找零问题等。',
icon: '🎯',
topics: [
'贪心策略基本思想',
'活动选择问题',
'Huffman编码',
'贪心背包(部分背包)',
'最优装载问题',
'最短路径',
'找零问题'
],
subfolders: [
{
name: 'huodong',
label: '活动选择',
files: [
{ path: 'c/ch4/huodong/fenpei.c', name: 'fenpei.c', label: '活动分配', description: '活动选择问题的贪心实现' }
]
},
{
name: 'huffman',
label: 'Huffman编码',
files: [
{ path: 'c/ch4/huffman/halfall.c', name: 'halfall.c', label: 'Huffman编码主流程', description: '构建Huffman树并生成编码' },
{ path: 'c/ch4/huffman/halfcode.c', name: 'halfcode.c', label: 'Huffman编码生成', description: '从Huffman树回溯得到编码' },
{ path: 'c/ch4/huffman/halftree.c', name: 'halftree.c', label: 'Huffman树构建', description: '节点创建、合并、排序构建Huffman树' },
{ path: 'c/ch4/huffman/treelist.c', name: 'treelist.c', label: 'Huffman节点管理', description: '树节点创建和列表操作' }
]
},
{
name: 'bag',
label: '贪心背包',
files: [
{ path: 'c/ch4/bag/tanbag.c', name: 'tanbag.c', label: '贪心背包', description: '部分背包问题的贪心解法' },
{ path: 'c/ch4/bag/tanbagtest.c', name: 'tanbagtest.c', label: '贪心背包测试', description: '贪心背包的测试/驱动' }
]
},
{
name: 'bestload',
label: '最优装载',
files: [
{ path: 'c/ch4/bestload/loading.c', name: 'loading.c', label: '最优装载问题', description: '装载问题的贪心实现' },
{ path: 'c/ch4/bestload/sortAttr.c', name: 'sortAttr.c', label: '排序辅助', description: '装载问题排序比较器' }
]
},
{
name: 'money',
label: '找零问题',
files: [
{ path: 'c/ch4/money/getmoney.c', name: 'getmoney.c', label: '贪心找零', description: '按面额计算所需最少张数' },
{ path: 'c/ch4/money/moneytwowei.c', name: 'moneytwowei.c', label: '找零(二维)', description: '面额分解的另一版本' }
]
},
{
name: 'shortest',
label: '最短路径',
files: [
{ path: 'c/ch4/shortest/path.c', name: 'path.c', label: '最短路径', description: '最短路径算法实现' },
{ path: 'c/ch4/shortest/shortpath.c', name: 'shortpath.c', label: '最短路径(分支界)', description: '分支界/分治的最短路径' }
]
}
]
},
{
id: 'ch5',
title: '第五章 回溯法',
subtitle: 'Backtracking — 搜索与剪枝',
description: '回溯法通过深度优先搜索所有可能的解,并在搜索过程中用剪枝函数避免无效搜索。本章涵盖0/1背包的回溯解法、装载问题、N皇后问题等。',
icon: '🔙',
topics: [
'回溯法基本思想',
'深度优先搜索',
'剪枝函数(约束函数与限界函数)',
'0/1背包的回溯解法',
'装载问题的回溯解法',
'N皇后问题'
],
subfolders: [
{
name: 'hui01bag',
label: '回溯0/1背包',
files: [
{ path: 'c/ch5/hui01bag/huisu01bag.c', name: 'huisu01bag.c', label: '0/1背包回溯解法', description: '递归回溯搜索最优解' }
]
},
{
name: 'loading',
label: '装载问题',
files: [
{ path: 'c/ch5/loading/huiloading.c', name: 'huiloading.c', label: '回溯装载', description: '回溯搜索最佳放置方案' },
{ path: 'c/ch5/loading/loading2.c', name: 'loading2.c', label: '装载实现(二)', description: '装载问题的另一种回溯实现' }
]
},
{
name: 'queue',
label: 'N皇后',
files: [
{ path: 'c/ch5/queue/nqueue.c', name: 'nqueue.c', label: 'N皇后问题', description: '回溯法解N皇后问题' }
]
}
]
},
{
id: 'ch6',
title: '第六章 分支限界法',
subtitle: 'Branch and Bound — 剪枝搜索最优解',
description: '分支限界法以广度优先或最小耗费优先的方式搜索解空间树,并用限界函数剪去不可能产生最优解的分支。本章涵盖图的BFS/DFS、0/1背包的分支限界解法、TSP问题等。',
icon: '🌿',
topics: [
'分支限界法基本思想',
'广度优先搜索与优先队列',
'上界函数与剪枝',
'0/1背包的分支限界解法',
'旅行商问题(TSP)',
'图的BFS与DFS遍历'
],
subfolders: [
{
name: 'xianbag01',
label: '分支限界0/1背包',
files: [
{ path: 'c/ch6/xianbag01/bag01fifo.c', name: 'bag01fifo.c', label: '0/1背包(FIFO队列)', description: 'FIFO活结点队列的分支限界实现' },
{ path: 'c/ch6/xianbag01/bag01livevalue.c', name: 'bag01livevalue.c', label: '0/1背包(上界估计)', description: '用上界剪枝搜索最佳值的分支限界实现' }
]
},
{
name: 'tsp',
label: '旅行商问题(TSP)',
files: [
{ path: 'c/ch6/tsp/fenzhi/tsp2.c', name: 'tsp2.c', label: 'TSP(分支限界)', description: '分支限界法解旅行商问题' },
{ path: 'c/ch6/tsp/huisu/travelroute.c', name: 'travelroute.c', label: 'TSP(回溯法)', description: '回溯法求解旅行商路径' }
]
},
{
name: 'graph',
label: '图遍历',
files: [
{ path: 'c/ch6/graph/bfs.c', name: 'bfs.c', label: '广度优先遍历(BFS)', description: '队列实现的图广度优先遍历' },
{ path: 'c/ch6/graph/dfs.c', name: 'dfs.c', label: '深度优先遍历(DFS)', description: '邻接矩阵/递归的图深度优先遍历' }
]
}
]
}
]
/**
* 获取所有代码文件的扁平列表(含章节信息)
*/
export function getAllFiles() {
const allFiles = []
for (const chapter of chapters) {
if (chapter.files) {
for (const file of chapter.files) {
allFiles.push({ ...file, chapterId: chapter.id, chapterTitle: chapter.title })
}
}
if (chapter.subfolders) {
for (const folder of chapter.subfolders) {
for (const file of folder.files) {
allFiles.push({ ...file, chapterId: chapter.id, chapterTitle: chapter.title, folder: folder.label })
}
}
}
}
return allFiles
}
export function getChapterById(id) {
return chapters.find(ch => ch.id === id)
}
+8
View File
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index.js'
const app = createApp(App)
app.use(router)
app.mount('#app')
+30
View File
@@ -0,0 +1,30 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import ChapterView from '../views/ChapterView.vue'
import SortDemo from '../views/SortDemo.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/chapter/:id',
name: 'Chapter',
component: ChapterView,
props: true
},
{
path: '/sort-demo',
name: 'SortDemo',
component: SortDemo
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
+390
View File
@@ -0,0 +1,390 @@
/* ============================================
算法分析与设计 - 教学辅助平台 主题样式
============================================ */
:root {
/* 亮色主题 */
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--sidebar-bg: #f8fafc;
--bg: #ffffff;
--card-bg: #ffffff;
--code-bg: #1e1e2e;
--code-header-bg: #2d2d3f;
--code-text: #cdd6f4;
--border-color: #e2e8f0;
--hover-bg: #f1f5f9;
--active-bg: #eff6ff;
--tag-bg: #f1f5f9;
--text-primary: #1e293b;
--text-secondary: #475569;
--text-tertiary: #94a3b8;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
--sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: 'Fira Code', 'JetBrains Mono', 'Consolas', 'Cascadia Code', monospace;
color-scheme: light;
font: 15px/1.6 var(--sans);
color: var(--text-secondary);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (prefers-color-scheme: dark) {
:root {
--primary-color: #60a5fa;
--primary-hover: #3b82f6;
--sidebar-bg: #1e1e2e;
--bg: #18181b;
--card-bg: #27272a;
--code-bg: #1e1e2e;
--code-header-bg: #2d2d3f;
--code-text: #cdd6f4;
--border-color: #3f3f46;
--hover-bg: #2a2a2e;
--active-bg: #1e3a5f;
--tag-bg: #2a2a2e;
--text-primary: #f4f4f5;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.4);
color-scheme: dark;
}
}
/* 全局重置 */
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
background: var(--bg);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--sans);
color: var(--text-primary);
line-height: 1.3;
margin: 0;
}
p {
margin: 0;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* 布局 */
.app-layout {
display: flex;
min-height: 100vh;
}
.main-content {
flex: 1;
min-width: 0;
padding: 24px;
background: var(--bg);
overflow-x: hidden;
}
/* 滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* 回到顶部按钮 */
.back-to-top {
position: fixed;
bottom: 32px;
right: 32px;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--primary-color);
color: white;
border: none;
font-size: 20px;
cursor: pointer;
box-shadow: var(--shadow-md);
transition: all 0.2s;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
}
.back-to-top:hover {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
/* 代码语法高亮颜色 */
.hl-keyword {
color: #c678dd;
font-weight: 600;
}
.hl-comment {
color: #6a9955;
font-style: italic;
}
.hl-string {
color: #98c379;
}
.hl-number {
color: #d19a66;
}
.hl-preprocessor {
color: #61afef;
font-weight: 600;
}
/* 代码内容行号样式 */
.code-content code {
font-family: var(--mono);
font-size: 13px;
line-height: 1.6;
white-space: pre;
tab-size: 4;
}
/* 响应式 */
@media (max-width: 768px) {
.app-layout {
flex-direction: column;
}
.main-content {
padding: 16px;
}
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#app {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
+55
View File
@@ -0,0 +1,55 @@
/**
* 读取服务器上的代码文件内容
* 由于是静态站点,我们通过 fetch 请求加载 .c 文件
*/
export async function loadCodeFile(path) {
try {
const response = await fetch(`/${path}`)
if (!response.ok) {
throw new Error(`Failed to load ${path}: ${response.status}`)
}
return await response.text()
} catch (error) {
console.error('加载代码文件失败:', error)
return `// 无法加载文件: ${path}\n// 请确保文件存在且路径正确`
}
}
/**
* C语言关键字高亮
*/
export function highlightC(code) {
const keywords = [
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do',
'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if',
'int', 'long', 'register', 'return', 'short', 'signed', 'sizeof', 'static',
'struct', 'switch', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'while',
'include', 'define', 'ifdef', 'ifndef', 'endif', 'main', 'printf', 'scanf'
]
// 先转义 HTML
let escaped = code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 关键字高亮
const keywordPattern = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g')
escaped = escaped.replace(keywordPattern, '<span class="hl-keyword">$1</span>')
// 注释高亮 (// 和 /* */)
escaped = escaped.replace(/(\/\/.*)/g, '<span class="hl-comment">$1</span>')
escaped = escaped.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="hl-comment">$1</span>')
// 字符串高亮
escaped = escaped.replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, '<span class="hl-string">"$1"</span>')
escaped = escaped.replace(/'([^'\\]*(\\.[^'\\]*)*)'/g, '<span class="hl-string">\'$1\'</span>')
// 数字高亮
escaped = escaped.replace(/\b(\d+\.?\d*)\b/g, '<span class="hl-number">$1</span>')
// 预处理指令高亮
escaped = escaped.replace(/(^#\s*\w+.*)/gm, '<span class="hl-preprocessor">$1</span>')
return escaped
}
+291
View File
@@ -0,0 +1,291 @@
/**
* 排序算法可视化 - 动画步骤生成器
* 记录每一步的数组状态和操作描述,供可视化组件播放
*/
/**
* 生成归并排序的动画步骤
* @param {number[]} arr - 原始数组
* @returns {object[]} steps - 动画步骤数组
*/
export function generateMergeSortSteps(arr) {
const steps = []
const array = [...arr]
const n = array.length
// 辅助数组用于归并
const temp = new Array(n)
// 记录初始状态
recordStep(steps, array, { description: '初始数组' })
function mergeSort(lo, hi) {
if (lo >= hi) {
recordStep(steps, array, {
sorted: [lo],
range: [lo, hi],
description: `单个元素 [${array[lo]}], 无需排序`
})
return
}
const mid = Math.floor((lo + hi) / 2)
recordStep(steps, array, {
range: [lo, hi],
description: `拆分: [${lo}..${hi}] → [${lo}..${mid}] 和 [${mid+1}..${hi}]`
})
mergeSort(lo, mid)
mergeSort(mid + 1, hi)
// 归并
merge(lo, mid, hi)
}
function merge(lo, mid, hi) {
// 复制到辅助数组
for (let i = lo; i <= hi; i++) {
temp[i] = array[i]
}
recordStep(steps, array, {
range: [lo, hi],
merging: { left: [lo, mid], right: [mid + 1, hi] },
description: `开始归并 [${lo}..${mid}] 和 [${mid+1}..${hi}]`
})
let i = lo, j = mid + 1
for (let k = lo; k <= hi; k++) {
if (i > mid) {
// 左边已取完
recordStep(steps, array, {
range: [lo, hi],
comparing: [i > mid ? i - 1 : i, j],
merging: { left: [lo, mid], right: [mid + 1, hi] },
description: `左半部分已取完, 取右半部分元素 ${temp[j]}`
})
array[k] = temp[j++]
recordStep(steps, array, {
range: [lo, hi],
merging: { left: [lo, mid], right: [mid + 1, hi] },
sortedIndices: getSortedRange(lo, k),
description: `放置元素 ${array[k]} 到位置 ${k}`
})
} else if (j > hi) {
// 右边已取完
recordStep(steps, array, {
range: [lo, hi],
comparing: [i, j > hi ? j - 1 : j],
merging: { left: [lo, mid], right: [mid + 1, hi] },
description: `右半部分已取完, 取左半部分元素 ${temp[i]}`
})
array[k] = temp[i++]
recordStep(steps, array, {
range: [lo, hi],
merging: { left: [lo, mid], right: [mid + 1, hi] },
sortedIndices: getSortedRange(lo, k),
description: `放置元素 ${array[k]} 到位置 ${k}`
})
} else if (temp[i] <= temp[j]) {
// 左 <= 右, 取左边的
recordStep(steps, array, {
range: [lo, hi],
comparing: [i, j],
merging: { left: [lo, mid], right: [mid + 1, hi] },
description: `比较 ${temp[i]}${temp[j]}, 取左半部分元素 ${temp[i]}`
})
array[k] = temp[i++]
recordStep(steps, array, {
range: [lo, hi],
merging: { left: [lo, mid], right: [mid + 1, hi] },
sortedIndices: getSortedRange(lo, k),
description: `放置元素 ${array[k]} 到位置 ${k}`
})
} else {
// 左 > 右, 取右边的
recordStep(steps, array, {
range: [lo, hi],
comparing: [i, j],
merging: { left: [lo, mid], right: [mid + 1, hi] },
description: `比较 ${temp[i]} > ${temp[j]}, 取右半部分元素 ${temp[j]}`
})
array[k] = temp[j++]
recordStep(steps, array, {
range: [lo, hi],
merging: { left: [lo, mid], right: [mid + 1, hi] },
sortedIndices: getSortedRange(lo, k),
description: `放置元素 ${array[k]} 到位置 ${k}`
})
}
}
// 标记整个范围为已排好序
const sortedIndices = []
for (let k = lo; k <= hi; k++) sortedIndices.push(k)
recordStep(steps, array, {
sorted: sortedIndices,
range: [lo, hi],
description: `归并完成: [${lo}..${hi}] 已排好序`
})
}
function getSortedRange(lo, k) {
const indices = []
for (let i = lo; i <= k; i++) indices.push(i)
return indices
}
mergeSort(0, n - 1)
// 最终完成
const allSorted = array.map((_, i) => i)
recordStep(steps, array, {
sorted: allSorted,
description: '✅ 归并排序完成!'
})
return steps
}
/**
* 生成快速排序的动画步骤
* @param {number[]} arr - 原始数组
* @returns {object[]} steps - 动画步骤数组
*/
export function generateQuickSortSteps(arr) {
const steps = []
const array = [...arr]
const n = array.length
recordStep(steps, array, { description: '初始数组' })
function quickSort(lo, hi) {
if (lo >= hi) {
if (lo === hi) {
recordStep(steps, array, {
sorted: [lo],
pivot: lo,
description: `单个元素 [${array[lo]}], 已就位`
})
}
return
}
recordStep(steps, array, {
range: [lo, hi],
description: `对区间 [${lo}..${hi}] 执行快速排序`
})
const pivotIndex = partition(lo, hi)
// 标记 pivot 为已排好序
recordStep(steps, array, {
sorted: [pivotIndex],
range: [lo, hi],
description: `基准元素 ${array[pivotIndex]} 已就位于位置 ${pivotIndex}`
})
quickSort(lo, pivotIndex - 1)
quickSort(pivotIndex + 1, hi)
}
function partition(lo, hi) {
// 选择最右边的元素作为 pivot
const pivotValue = array[hi]
let i = lo - 1
recordStep(steps, array, {
range: [lo, hi],
pivot: hi,
description: `选择基准元素 pivot = ${pivotValue} (位置 ${hi})`
})
for (let j = lo; j < hi; j++) {
recordStep(steps, array, {
range: [lo, hi],
pivot: hi,
comparing: [j, hi],
description: `比较 array[${j}] = ${array[j]} 与 pivot = ${pivotValue}`
})
if (array[j] <= pivotValue) {
i++
if (i !== j) {
// 交换
;[array[i], array[j]] = [array[j], array[i]]
recordStep(steps, array, {
range: [lo, hi],
pivot: hi,
swapping: [i, j],
description: `${array[j]}${pivotValue}, 交换位置 ${i}${j}`
})
} else {
recordStep(steps, array, {
range: [lo, hi],
pivot: hi,
description: `${array[j]}${pivotValue}, i = j = ${i}, 无需交换`
})
}
}
}
// 将 pivot 放到正确位置
if (i + 1 !== hi) {
;[array[i + 1], array[hi]] = [array[hi], array[i + 1]]
recordStep(steps, array, {
range: [lo, hi],
swapping: [i + 1, hi],
pivot: i + 1,
description: `将基准元素 ${pivotValue} 放到正确位置 ${i + 1}`
})
} else {
recordStep(steps, array, {
range: [lo, hi],
pivot: hi,
description: `基准元素已在正确位置 ${hi}`
})
}
return i + 1
}
quickSort(0, n - 1)
const allSorted = array.map((_, i) => i)
recordStep(steps, array, {
sorted: allSorted,
description: '✅ 快速排序完成!'
})
return steps
}
/**
* 生成随机数组
* @param {number} size
* @param {number} maxValue
* @returns {number[]}
*/
export function generateRandomArray(size = 10, maxValue = 50) {
const arr = []
for (let i = 0; i < size; i++) {
arr.push(Math.floor(Math.random() * maxValue) + 1)
}
return arr
}
function recordStep(steps, array, info) {
steps.push({
array: [...array],
highlights: {
comparing: info.comparing || null,
swapping: info.swapping || null,
pivot: info.pivot ?? null,
sorted: info.sorted || null,
sortedIndices: info.sortedIndices || null,
range: info.range || null,
merging: info.merging || null
},
description: info.description || ''
})
}
+405
View File
@@ -0,0 +1,405 @@
<template>
<div class="chapter-view" v-if="chapter">
<header class="chapter-header">
<div class="chapter-icon">{{ chapter.icon }}</div>
<div>
<h1 class="chapter-title">{{ chapter.title }}</h1>
<p class="chapter-subtitle">{{ chapter.subtitle }}</p>
<p class="chapter-desc">{{ chapter.description }}</p>
</div>
</header>
<section class="topics-section">
<h2 class="section-label">📌 本章知识点</h2>
<div class="topics-list">
<span v-for="topic in chapter.topics" :key="topic" class="topic-pill">
{{ topic }}
</span>
</div>
</section>
<!-- 子文件夹分组显示 -->
<section v-for="folder in chapter.subfolders" :key="folder.name" class="folder-section">
<h2 class="folder-title">📂 {{ folder.label }}</h2>
<p class="folder-hint" v-if="getFolderDescription(folder.name)">{{ getFolderDescription(folder.name) }}</p>
<!-- 动态演示入口 -->
<div v-if="folder.demo" class="demo-entry">
<router-link :to="folder.demo.path" class="demo-link">
<span class="demo-link-icon">{{ folder.demo.label.split(' ')[0] }}</span>
<span class="demo-link-text">{{ folder.demo.label }}</span>
<span class="demo-link-desc">{{ folder.demo.description }}</span>
<span class="demo-link-arrow"></span>
</router-link>
</div>
<div class="file-list">
<div
v-for="file in folder.files"
:key="file.path"
class="file-item"
:class="{ active: currentFile?.path === file.path }"
@click="selectFile(file)"
>
<div class="file-item-header">
<span class="file-item-icon">📄</span>
<span class="file-item-name">{{ file.name }}</span>
<span class="file-item-label">{{ file.label }}</span>
</div>
<p class="file-item-desc">{{ file.description }}</p>
</div>
</div>
</section>
<!-- 根级文件没有子文件夹的文件 -->
<section v-if="chapter.files && chapter.files.length" class="folder-section">
<h2 class="folder-title">📂 其他示例</h2>
<div class="file-list">
<div
v-for="file in chapter.files"
:key="file.path"
class="file-item"
:class="{ active: currentFile?.path === file.path }"
@click="selectFile(file)"
>
<div class="file-item-header">
<span class="file-item-icon">📄</span>
<span class="file-item-name">{{ file.name }}</span>
<span class="file-item-label">{{ file.label }}</span>
</div>
<p class="file-item-desc">{{ file.description }}</p>
</div>
</div>
</section>
<!-- 代码查看区域 -->
<section v-if="currentFile" class="code-section">
<h2 class="section-label code-section-label">💻 代码查看</h2>
<CodeViewer
:filePath="currentFile.path"
:fileName="currentFile.name"
:fileLabel="currentFile.label"
:description="currentFile.description"
/>
</section>
<section v-else class="empty-section">
<div class="empty-state">
<span class="empty-icon">👆</span>
<p>请从上方选择一个代码文件查看其源码</p>
</div>
</section>
</div>
<div v-else class="not-found">
<h2>章节未找到</h2>
<p>请返回首页重新选择</p>
<router-link to="/" class="back-link"> 返回首页</router-link>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import { getChapterById } from '../data/chapters.js'
import CodeViewer from '../components/CodeViewer.vue'
const props = defineProps({
id: String
})
const route = useRoute()
const chapterId = computed(() => props.id || route.params.id)
const currentFile = ref(null)
const chapter = computed(() => getChapterById(chapterId.value))
// Reset current file when switching chapters
watch(chapterId, () => {
currentFile.value = null
})
function selectFile(file) {
currentFile.value = file
}
function getFolderDescription(folderName) {
const descriptions = {
mergesort: '归并排序是分治法的经典应用:将数组一分为二,分别排序后合并。时间复杂度 O(n log n)。',
quicksort: '快速排序通过选择一个基准元素,将数组分为左右两部分,递归排序。平均时间复杂度 O(n log n)。',
halfsearch: '二分查找在有序数组中通过每次将搜索范围缩小一半来查找目标值,时间复杂度 O(log n)。',
bigcheng: '大整数乘法通过分治策略将 n 位数乘法分解为更小规模的计算,降低时间复杂度。',
matrix: '矩阵乘法有多种实现方式:朴素算法 O(n³),Strassen 分治算法可优化至 O(n^2.81)。',
digui: '递归是分治法的基石,通过这些示例理解递归的基本结构与调用栈。',
maopao: '冒泡排序通过相邻元素比较交换将最大元素"冒泡"到末尾,时间复杂度 O(n²)。',
allpai: '全排列生成所有 n! 种排列方式,是回溯思想和分治策略的典型应用。',
richeng: '循环赛日程表使用分治策略为 2^k 个选手安排比赛日程。',
bag01: '0/1背包问题是动态规划的经典问题:给定容量和物品,求最大价值。',
lcs: '最长公共子序列(LCS)问题是比较两个序列相似度的经典DP问题。',
image: '图像压缩问题使用动态规划确定最优的像素段划分,平衡压缩比与存储空间。',
homework: '课后练习中的算法实现示例。',
huodong: '活动选择问题是贪心算法的经典案例:选择最多的不重叠活动。',
huffman: 'Huffman编码是数据压缩经典算法,通过构造最优前缀码实现无损压缩。',
bag: '贪心背包(部分背包)问题允许取物品的一部分,按单位价值贪心选择。',
bestload: '最优装载问题:在载重限制下尽可能多地装载物品。',
money: '找零问题:使用最少硬币凑出指定金额。',
shortest: '最短路径问题:寻找图中两点之间的最短路径。',
hui01bag: '0/1背包的回溯解法:通过深度优先搜索和剪枝寻找最优解。',
loading: '装载问题的回溯解法:搜索最优的装载方案。',
queue: 'N皇后问题的回溯解法:在 N×N 棋盘上放置 N 个皇后使其互不攻击。',
xianbag01: '0/1背包的分支限界解法:使用队列或优先队列进行广度优先搜索。',
tsp: '旅行商问题(TSP):寻找最短的环游所有城市的路径。',
graph: '图的两种基本遍历方式:广度优先(BFS)和深度优先(DFS)。'
}
return descriptions[folderName] || ''
}
</script>
<style scoped>
.chapter-view {
max-width: 1000px;
margin: 0 auto;
padding: 0 24px;
}
.chapter-header {
display: flex;
gap: 16px;
align-items: flex-start;
padding: 32px 0 20px;
border-bottom: 1px solid var(--border-color);
margin-bottom: 24px;
}
.chapter-icon {
font-size: 48px;
flex-shrink: 0;
}
.chapter-title {
font-size: 26px;
font-weight: 800;
color: var(--text-primary);
margin-bottom: 4px;
}
.chapter-subtitle {
font-size: 15px;
color: var(--primary-color);
font-weight: 500;
margin-bottom: 6px;
}
.chapter-desc {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.6;
}
.section-label {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.topics-section {
margin-bottom: 28px;
}
.topics-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.topic-pill {
font-size: 13px;
background: var(--active-bg);
color: var(--primary-color);
padding: 6px 16px;
border-radius: 20px;
font-weight: 500;
border: 1px solid var(--border-color);
}
.folder-section {
margin-bottom: 28px;
}
.folder-title {
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 6px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border-color);
}
.folder-hint {
font-size: 13px;
color: var(--text-tertiary);
margin-bottom: 12px;
line-height: 1.5;
}
/* 动态演示入口 */
.demo-entry {
margin-bottom: 12px;
}
.demo-link {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(139, 92, 246, 0.08));
border: 1px solid rgba(59, 130, 246, 0.25);
border-radius: 10px;
text-decoration: none;
transition: all 0.2s ease;
}
.demo-link:hover {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(139, 92, 246, 0.15));
border-color: var(--primary-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
text-decoration: none;
}
.demo-link-icon {
font-size: 24px;
flex-shrink: 0;
}
.demo-link-text {
font-size: 15px;
font-weight: 700;
color: var(--primary-color);
white-space: nowrap;
}
.demo-link-desc {
font-size: 13px;
color: var(--text-secondary);
flex: 1;
}
.demo-link-arrow {
font-size: 18px;
color: var(--primary-color);
font-weight: 700;
flex-shrink: 0;
}
.file-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 10px;
}
.file-item {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.file-item:hover {
border-color: var(--primary-color);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.file-item.active {
border-color: var(--primary-color);
background: var(--active-bg);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
}
.file-item-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.file-item-icon {
font-size: 14px;
}
.file-item-name {
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 13px;
font-weight: 600;
color: var(--primary-color);
}
.file-item-label {
font-size: 11px;
color: var(--text-tertiary);
background: var(--tag-bg);
padding: 1px 6px;
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-item-desc {
font-size: 12px;
color: var(--text-tertiary);
line-height: 1.4;
margin-left: 20px;
}
.code-section {
margin: 32px 0;
}
.code-section-label {
margin-bottom: 16px;
}
.empty-section {
padding: 60px 0;
text-align: center;
}
.empty-state {
color: var(--text-tertiary);
}
.empty-icon {
font-size: 40px;
display: block;
margin-bottom: 12px;
}
.empty-state p {
font-size: 15px;
}
.not-found {
text-align: center;
padding: 80px 20px;
}
.not-found h2 {
font-size: 24px;
margin-bottom: 8px;
color: var(--text-primary);
}
.back-link {
display: inline-block;
margin-top: 16px;
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
}
.back-link:hover {
text-decoration: underline;
}
</style>
+278
View File
@@ -0,0 +1,278 @@
<template>
<div class="home">
<header class="hero">
<h1 class="hero-title">📚 算法分析与设计</h1>
<p class="hero-subtitle">教学辅助平台 从基础到进阶的算法学习之旅</p>
<p class="hero-desc">
涵盖六大核心章节复杂度分析分治法动态规划贪心算法回溯法分支限界法
</p>
<div class="hero-actions">
<router-link to="/chapter/ch1" class="btn-primary">开始学习 </router-link>
<a href="#chapters" class="btn-secondary">浏览章节</a>
</div>
</header>
<section id="chapters" class="chapters-grid">
<router-link
v-for="ch in chapters"
:key="ch.id"
:to="`/chapter/${ch.id}`"
class="chapter-card"
>
<div class="card-icon">{{ ch.icon }}</div>
<h3 class="card-title">{{ ch.title.split('—')[0].trim() }}</h3>
<p class="card-subtitle">{{ ch.subtitle }}</p>
<p class="card-desc">{{ ch.description }}</p>
<div class="card-topics">
<span v-for="topic in ch.topics.slice(0, 3)" :key="topic" class="topic-tag">
{{ topic }}
</span>
<span v-if="ch.topics.length > 3" class="topic-tag more">+{{ ch.topics.length - 3 }}</span>
</div>
<div class="card-footer">
<span class="explore-link">查看详情 </span>
</div>
</router-link>
</section>
<section class="features-section">
<h2 class="section-title">平台功能</h2>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">📖</div>
<h3>代码阅览</h3>
<p>所有算法代码源文件在线展示支持语法高亮与一键复制</p>
</div>
<div class="feature-card">
<div class="feature-icon">📂</div>
<h3>分类导航</h3>
<p>按章节和子主题分类组织快速定位所需算法示例</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔍</div>
<h3>知识点梳理</h3>
<p>每个章节的关键知识点和算法思想概览</p>
</div>
<div class="feature-card">
<div class="feature-icon">💻</div>
<h3>C语言实现</h3>
<p>全部使用标准C语言实现适合教学演示与实验</p>
</div>
</div>
</section>
<footer class="footer">
<p>算法分析与设计 教学辅助平台</p>
</footer>
</div>
</template>
<script setup>
import { chapters } from '../data/chapters.js'
</script>
<style scoped>
.home {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
}
.hero {
text-align: center;
padding: 60px 20px 40px;
}
.hero-title {
font-size: 36px;
font-weight: 800;
background: linear-gradient(135deg, var(--primary-color), #8b5cf6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 12px;
}
.hero-subtitle {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.hero-desc {
font-size: 14px;
color: var(--text-tertiary);
margin-bottom: 24px;
}
.hero-actions {
display: flex;
gap: 12px;
justify-content: center;
}
.btn-primary, .btn-secondary {
padding: 12px 28px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-secondary {
background: var(--hover-bg);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--active-bg);
}
.chapters-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin: 40px 0;
}
.chapter-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
text-decoration: none;
color: inherit;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
}
.chapter-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
border-color: var(--primary-color);
}
.card-icon {
font-size: 40px;
margin-bottom: 12px;
}
.card-title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
}
.card-subtitle {
font-size: 13px;
color: var(--primary-color);
margin-bottom: 10px;
font-weight: 500;
}
.card-desc {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 14px;
flex: 1;
}
.card-topics {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 14px;
}
.topic-tag {
font-size: 11px;
background: var(--tag-bg);
color: var(--text-secondary);
padding: 3px 10px;
border-radius: 12px;
}
.topic-tag.more {
background: var(--active-bg);
color: var(--primary-color);
}
.card-footer {
border-top: 1px solid var(--border-color);
padding-top: 12px;
}
.explore-link {
font-size: 13px;
color: var(--primary-color);
font-weight: 600;
}
.section-title {
text-align: center;
font-size: 24px;
font-weight: 700;
margin-bottom: 24px;
color: var(--text-primary);
}
.features-section {
margin: 60px 0;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
}
.feature-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
text-align: center;
}
.feature-icon {
font-size: 32px;
margin-bottom: 12px;
}
.feature-card h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.feature-card p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
.footer {
text-align: center;
padding: 40px 0;
color: var(--text-tertiary);
font-size: 13px;
border-top: 1px solid var(--border-color);
margin-top: 40px;
}
</style>
+258
View File
@@ -0,0 +1,258 @@
<template>
<div class="sort-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="demo-section">
<h2 class="section-title">📖 算法说明</h2>
<div class="algo-info">
<div class="algo-card">
<h3>归并排序 (Merge Sort)</h3>
<div class="complexity">
<span><strong>时间复杂度:</strong> O(n log n)</span>
<span><strong>空间复杂度:</strong> O(n)</span>
<span><strong>稳定性:</strong> 稳定</span>
</div>
<p>归并排序采用<strong>分治策略</strong>将数组不断拆分为两半分别排序后再合并合并时通过比较两个有序子数组的头部元素依次选取较小的放入结果数组</p>
<ul class="algo-steps">
<li><strong>分解</strong>将数组从中间分为左右两个子数组</li>
<li><strong>解决</strong>递归地对左右子数组进行归并排序</li>
<li><strong>合并</strong>将两个有序子数组合并为一个有序数组</li>
</ul>
</div>
<div class="algo-card">
<h3>快速排序 (Quick Sort)</h3>
<div class="complexity">
<span><strong>时间复杂度:</strong> O(n log n) 平均 / O() 最坏</span>
<span><strong>空间复杂度:</strong> O(log n)</span>
<span><strong>稳定性:</strong> 不稳定</span>
</div>
<p>快速排序采用<strong>分治策略</strong>选择一个基准元素将数组分为小于基准和大于基准的两部分然后递归排序核心是 <strong>partition划分</strong> 操作</p>
<ul class="algo-steps">
<li><strong>选择基准</strong>选取数组最后一个元素作为 pivot</li>
<li><strong>划分</strong>将小于 pivot 的元素放到左边大于的放到右边</li>
<li><strong>递归</strong>对左右两个子区间递归进行快速排序</li>
</ul>
</div>
</div>
</section>
<section class="demo-section">
<h2 class="section-title">🎬 归并排序演示</h2>
<SortVisualizer
title="归并排序 (Merge Sort)"
algorithm="merge"
:arraySize="12"
/>
</section>
<section class="demo-section">
<h2 class="section-title">🎬 快速排序演示</h2>
<SortVisualizer
title="快速排序 (Quick Sort)"
algorithm="quick"
:arraySize="12"
/>
</section>
<section class="demo-section">
<h2 class="section-title">💡 对比总结</h2>
<div class="comparison-table-wrapper">
<table class="comparison-table">
<thead>
<tr>
<th>特性</th>
<th>归并排序</th>
<th>快速排序</th>
</tr>
</thead>
<tbody>
<tr>
<td>策略</td>
<td>先递归后合并后序遍历</td>
<td>先划分后递归前序遍历</td>
</tr>
<tr>
<td>额外空间</td>
<td>O(n) 需要辅助数组</td>
<td>O(log n) 递归栈空间</td>
</tr>
<tr>
<td>最坏情况</td>
<td>O(n log n) 始终稳定</td>
<td>O() 已有序数组且选择最值作为 pivot</td>
</tr>
<tr>
<td>稳定性</td>
<td> 稳定</td>
<td> 不稳定</td>
</tr>
<tr>
<td>适用场景</td>
<td>外部排序对稳定性有要求的场景</td>
<td>内部排序对平均性能要求高的场景</td>
</tr>
</tbody>
</table>
</div>
</section>
<div class="nav-back">
<router-link to="/chapter/ch2" class="back-link"> 返回第二章</router-link>
</div>
</div>
</template>
<script setup>
import SortVisualizer from '../components/SortVisualizer.vue'
</script>
<style scoped>
.sort-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: 28px;
}
.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);
}
.demo-section {
margin-bottom: 32px;
}
.section-title {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 2px solid var(--border-color);
}
.algo-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.algo-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
}
.algo-card h3 {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.complexity {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 12px;
padding: 8px 12px;
background: var(--hover-bg);
border-radius: 8px;
}
.algo-card p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 10px;
}
.algo-steps {
font-size: 13px;
color: var(--text-secondary);
padding-left: 20px;
line-height: 1.8;
}
.comparison-table-wrapper {
overflow-x: auto;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.comparison-table th,
.comparison-table td {
padding: 10px 16px;
border: 1px solid var(--border-color);
text-align: left;
}
.comparison-table th {
background: var(--hover-bg);
font-weight: 700;
color: var(--text-primary);
}
.comparison-table td {
color: var(--text-secondary);
}
.comparison-table tr:hover td {
background: var(--hover-bg);
}
.nav-back {
padding: 20px 0 40px;
}
.back-link {
color: var(--primary-color);
font-weight: 600;
text-decoration: none;
font-size: 14px;
}
.back-link:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.algo-info {
grid-template-columns: 1fr;
}
}
</style>