add
This commit is contained in:
+54
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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 |
@@ -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 |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
// 关键字高亮
|
||||
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
|
||||
}
|
||||
@@ -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 || ''
|
||||
})
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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(n²) 最坏</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(n²) — 已有序数组且选择最值作为 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>
|
||||
Reference in New Issue
Block a user