595 lines
14 KiB
Vue
595 lines
14 KiB
Vue
<template>
|
||
<div class="algorithm-demo">
|
||
<div class="demo-header">
|
||
<span class="title">算法思维:解决问题的方法</span>
|
||
<span class="subtitle">不同策略解决不同类型的问题</span>
|
||
</div>
|
||
|
||
<div class="demo-content">
|
||
<div class="algorithm-tabs">
|
||
<button
|
||
v-for="algo in algorithms"
|
||
:key="algo.name"
|
||
:class="['tab-btn', { active: activeAlgo === algo.name }]"
|
||
@click="activeAlgo = algo.name"
|
||
>
|
||
{{ algo.name }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="algorithm-visual">
|
||
<div class="visual-header">
|
||
<span class="algo-name">{{ currentAlgo.name }}</span>
|
||
<span class="algo-desc">{{ currentAlgo.desc }}</span>
|
||
</div>
|
||
|
||
<div class="visual-content">
|
||
<div v-if="activeAlgo === '二分查找'" class="binary-search">
|
||
<div class="search-input">
|
||
<span>在有序数组中查找:</span>
|
||
<input
|
||
v-model.number="searchTarget"
|
||
type="number"
|
||
class="num-input"
|
||
placeholder="输入数字"
|
||
/>
|
||
<button class="search-btn" @click="runBinarySearch">查找</button>
|
||
</div>
|
||
<div class="array-display">
|
||
<div
|
||
v-for="(num, i) in sortedArray"
|
||
:key="i"
|
||
class="array-cell"
|
||
:class="{
|
||
highlight: i >= searchRange.left && i <= searchRange.right,
|
||
found: i === foundIndex,
|
||
mid: i === midIndex
|
||
}"
|
||
>
|
||
{{ num }}
|
||
</div>
|
||
</div>
|
||
<div v-if="searchSteps.length" class="search-info">
|
||
<div v-for="(step, i) in searchSteps" :key="i" class="step">
|
||
{{ step }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="activeAlgo === '排序'" class="sorting">
|
||
<div class="sort-controls">
|
||
<button class="sort-btn" @click="resetArray">重置数组</button>
|
||
<button class="sort-btn" @click="runSort">开始排序</button>
|
||
</div>
|
||
<div class="array-display">
|
||
<div
|
||
v-for="(num, i) in sortArray"
|
||
:key="i"
|
||
class="array-cell"
|
||
:class="{
|
||
comparing: comparingIndices.includes(i),
|
||
sorted: sortedIndices.includes(i)
|
||
}"
|
||
>
|
||
{{ num }}
|
||
</div>
|
||
</div>
|
||
<div class="sort-info">
|
||
{{ sortStatus }}
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="activeAlgo === '递归'" class="recursion">
|
||
<div class="recursion-input">
|
||
<span>计算斐波那契数列第</span>
|
||
<input
|
||
v-model.number="fibN"
|
||
type="number"
|
||
min="1"
|
||
max="15"
|
||
class="num-input"
|
||
/>
|
||
<span>项</span>
|
||
<button class="calc-btn" @click="calcFib">计算</button>
|
||
</div>
|
||
<div v-if="fibResult !== null" class="fib-result">
|
||
<span class="result-value">F({{ fibN }}) = {{ fibResult }}</span>
|
||
</div>
|
||
<div v-if="fibSteps.length" class="recursion-tree">
|
||
<div class="tree-title">递归调用过程</div>
|
||
<div class="tree-content">
|
||
<div
|
||
v-for="(step, i) in fibSteps.slice(0, 8)"
|
||
:key="i"
|
||
class="tree-node"
|
||
>
|
||
{{ step }}
|
||
</div>
|
||
<div v-if="fibSteps.length > 8" class="tree-more">
|
||
... 共 {{ fibSteps.length }} 次调用
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="activeAlgo === '贪心'" class="greedy">
|
||
<div class="greedy-desc">
|
||
硬币找零问题:用最少的硬币凑出指定金额
|
||
</div>
|
||
<div class="greedy-input">
|
||
<span>目标金额:</span>
|
||
<input
|
||
v-model.number="coinTarget"
|
||
type="number"
|
||
min="1"
|
||
class="num-input"
|
||
/>
|
||
<button class="calc-btn" @click="calcCoins">计算</button>
|
||
</div>
|
||
<div class="coins-available">
|
||
可用硬币:{{ coins.join(', ') }} 元
|
||
</div>
|
||
<div v-if="coinResult.length" class="coin-result">
|
||
<div class="result-title">找零方案:</div>
|
||
<div class="coin-list">
|
||
<span v-for="(c, i) in coinResult" :key="i" class="coin">{{ c }}元</span>
|
||
</div>
|
||
<div class="result-summary">
|
||
共 {{ coinResult.length }} 枚硬币
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="complexity-info">
|
||
<div class="info-title">时间复杂度速查</div>
|
||
<div class="complexity-list">
|
||
<div v-for="c in complexities" :key="c.name" class="complexity-item">
|
||
<span class="c-name">{{ c.name }}</span>
|
||
<span class="c-value" :class="c.class">{{ c.value }}</span>
|
||
<span class="c-desc">{{ c.desc }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-box">
|
||
<strong>核心思想:</strong>算法是解决问题的方法。好的算法能让程序效率提升几个数量级。理解算法思维,比记住具体算法更重要。
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, reactive } from 'vue'
|
||
|
||
const activeAlgo = ref('二分查找')
|
||
|
||
const algorithms = [
|
||
{ name: '二分查找', desc: '每次排除一半,O(log n)' },
|
||
{ name: '排序', desc: '将无序变有序' },
|
||
{ name: '递归', desc: '自己调用自己' },
|
||
{ name: '贪心', desc: '每步选最优' }
|
||
]
|
||
|
||
const currentAlgo = computed(() => {
|
||
return algorithms.find((a) => a.name === activeAlgo.value)
|
||
})
|
||
|
||
const sortedArray = ref([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25])
|
||
const searchTarget = ref(13)
|
||
const searchRange = reactive({ left: 0, right: 12 })
|
||
const foundIndex = ref(-1)
|
||
const midIndex = ref(-1)
|
||
const searchSteps = ref([])
|
||
|
||
const runBinarySearch = () => {
|
||
searchSteps.value = []
|
||
foundIndex.value = -1
|
||
midIndex.value = -1
|
||
let left = 0
|
||
let right = sortedArray.value.length - 1
|
||
|
||
while (left <= right) {
|
||
const mid = Math.floor((left + right) / 2)
|
||
midIndex.value = mid
|
||
searchRange.left = left
|
||
searchRange.right = right
|
||
|
||
searchSteps.value.push(
|
||
`查找范围 [${left}, ${right}],中间位置 ${mid},值 ${sortedArray.value[mid]}`
|
||
)
|
||
|
||
if (sortedArray.value[mid] === searchTarget.value) {
|
||
foundIndex.value = mid
|
||
searchSteps.value.push(`找到目标 ${searchTarget.value} 在位置 ${mid}`)
|
||
return
|
||
} else if (sortedArray.value[mid] < searchTarget.value) {
|
||
left = mid + 1
|
||
searchSteps.value.push(
|
||
`${sortedArray.value[mid]} < ${searchTarget.value},在右半部分继续查找`
|
||
)
|
||
} else {
|
||
right = mid - 1
|
||
searchSteps.value.push(
|
||
`${sortedArray.value[mid]} > ${searchTarget.value},在左半部分继续查找`
|
||
)
|
||
}
|
||
}
|
||
searchSteps.value.push(`未找到目标 ${searchTarget.value}`)
|
||
}
|
||
|
||
const sortArray = ref([64, 34, 25, 12, 22, 11, 90, 45])
|
||
const comparingIndices = ref([])
|
||
const sortedIndices = ref([])
|
||
const sortStatus = ref('点击"开始排序"观察冒泡排序过程')
|
||
|
||
const resetArray = () => {
|
||
sortArray.value = [64, 34, 25, 12, 22, 11, 90, 45]
|
||
comparingIndices.value = []
|
||
sortedIndices.value = []
|
||
sortStatus.value = '数组已重置'
|
||
}
|
||
|
||
const runSort = async () => {
|
||
sortedIndices.value = []
|
||
const arr = [...sortArray.value]
|
||
const n = arr.length
|
||
|
||
for (let i = 0; i < n - 1; i++) {
|
||
for (let j = 0; j < n - i - 1; j++) {
|
||
comparingIndices.value = [j, j + 1]
|
||
sortStatus.value = `比较 ${arr[j]} 和 ${arr[j + 1]}`
|
||
await new Promise((r) => setTimeout(r, 300))
|
||
|
||
if (arr[j] > arr[j + 1]) {
|
||
;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
|
||
sortArray.value = [...arr]
|
||
sortStatus.value = `交换 ${arr[j + 1]} 和 ${arr[j]}`
|
||
await new Promise((r) => setTimeout(r, 200))
|
||
}
|
||
}
|
||
sortedIndices.value.push(n - i - 1)
|
||
}
|
||
sortedIndices.value.push(0)
|
||
comparingIndices.value = []
|
||
sortStatus.value = '排序完成!'
|
||
}
|
||
|
||
const fibN = ref(8)
|
||
const fibResult = ref(null)
|
||
const fibSteps = ref([])
|
||
|
||
const calcFib = () => {
|
||
fibSteps.value = []
|
||
const fib = (n) => {
|
||
fibSteps.value.push(`fib(${n})`)
|
||
if (n <= 1) return n
|
||
return fib(n - 1) + fib(n - 2)
|
||
}
|
||
fibResult.value = fib(fibN.value)
|
||
}
|
||
|
||
const coinTarget = ref(67)
|
||
const coins = [100, 50, 20, 10, 5, 1]
|
||
const coinResult = ref([])
|
||
|
||
const calcCoins = () => {
|
||
coinResult.value = []
|
||
let remaining = coinTarget.value
|
||
for (const coin of coins) {
|
||
while (remaining >= coin) {
|
||
coinResult.value.push(coin)
|
||
remaining -= coin
|
||
}
|
||
}
|
||
}
|
||
|
||
const complexities = [
|
||
{ name: 'O(1)', value: '常数', desc: '最优,如数组访问', class: 'good' },
|
||
{ name: 'O(log n)', value: '对数', desc: '很好,如二分查找', class: 'good' },
|
||
{ name: 'O(n)', value: '线性', desc: '一般,如遍历', class: 'mid' },
|
||
{
|
||
name: 'O(n log n)',
|
||
value: '线性对数',
|
||
desc: '可接受,如快速排序',
|
||
class: 'mid'
|
||
},
|
||
{ name: 'O(n²)', value: '平方', desc: '较慢,如冒泡排序', class: 'bad' },
|
||
{ name: 'O(2ⁿ)', value: '指数', desc: '很慢,如暴力递归', class: 'bad' }
|
||
]
|
||
</script>
|
||
|
||
<style scoped>
|
||
.algorithm-demo {
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 8px;
|
||
background: var(--vp-c-bg-soft);
|
||
padding: 1rem;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
.demo-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.demo-header .title {
|
||
font-weight: bold;
|
||
font-size: 1rem;
|
||
}
|
||
.demo-header .subtitle {
|
||
color: var(--vp-c-text-2);
|
||
font-size: 0.85rem;
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.algorithm-tabs {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tab-btn {
|
||
padding: 0.4rem 0.8rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
background: var(--vp-c-bg);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.tab-btn.active {
|
||
background: var(--vp-c-brand);
|
||
color: white;
|
||
border-color: var(--vp-c-brand);
|
||
}
|
||
|
||
.algorithm-visual {
|
||
background: var(--vp-c-bg);
|
||
padding: 0.75rem;
|
||
border-radius: 6px;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.visual-header {
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.algo-name {
|
||
font-weight: bold;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.algo-desc {
|
||
font-size: 0.85rem;
|
||
color: var(--vp-c-text-2);
|
||
margin-left: 0.5rem;
|
||
}
|
||
|
||
.search-input,
|
||
.greedy-input,
|
||
.recursion-input {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.num-input {
|
||
width: 60px;
|
||
padding: 0.25rem;
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 4px;
|
||
background: var(--vp-c-bg-alt);
|
||
}
|
||
|
||
.search-btn,
|
||
.sort-btn,
|
||
.calc-btn {
|
||
padding: 0.25rem 0.75rem;
|
||
background: var(--vp-c-brand);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.array-display {
|
||
display: flex;
|
||
gap: 2px;
|
||
margin-bottom: 0.75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.array-cell {
|
||
padding: 0.5rem;
|
||
background: var(--vp-c-bg-alt);
|
||
border: 1px solid var(--vp-c-divider);
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
font-size: 0.85rem;
|
||
min-width: 32px;
|
||
text-align: center;
|
||
}
|
||
|
||
.array-cell.highlight {
|
||
background: var(--vp-c-brand-soft);
|
||
}
|
||
|
||
.array-cell.found {
|
||
background: var(--vp-c-success);
|
||
color: white;
|
||
}
|
||
|
||
.array-cell.mid {
|
||
border: 2px solid var(--vp-c-brand);
|
||
}
|
||
|
||
.array-cell.comparing {
|
||
background: var(--vp-c-warning-soft);
|
||
}
|
||
|
||
.array-cell.sorted {
|
||
background: var(--vp-c-success-soft);
|
||
}
|
||
|
||
.search-info,
|
||
.sort-info {
|
||
font-size: 0.8rem;
|
||
color: var(--vp-c-text-2);
|
||
}
|
||
|
||
.step {
|
||
padding: 0.15rem 0;
|
||
}
|
||
|
||
.sort-controls {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.fib-result {
|
||
font-size: 1.1rem;
|
||
font-weight: bold;
|
||
color: var(--vp-c-brand);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.recursion-tree {
|
||
background: var(--vp-c-bg-alt);
|
||
padding: 0.5rem;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.tree-title {
|
||
font-weight: bold;
|
||
font-size: 0.85rem;
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.tree-content {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.tree-node {
|
||
font-size: 0.75rem;
|
||
font-family: monospace;
|
||
background: var(--vp-c-bg);
|
||
padding: 0.15rem 0.4rem;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.tree-more {
|
||
font-size: 0.75rem;
|
||
color: var(--vp-c-text-2);
|
||
}
|
||
|
||
.greedy-desc {
|
||
font-size: 0.85rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.coins-available {
|
||
font-size: 0.8rem;
|
||
color: var(--vp-c-text-2);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.coin-result {
|
||
margin-top: 0.5rem;
|
||
}
|
||
|
||
.result-title {
|
||
font-weight: bold;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.coin-list {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
flex-wrap: wrap;
|
||
margin: 0.25rem 0;
|
||
}
|
||
|
||
.coin {
|
||
padding: 0.25rem 0.5rem;
|
||
background: var(--vp-c-brand-soft);
|
||
border-radius: 4px;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.result-summary {
|
||
font-size: 0.85rem;
|
||
color: var(--vp-c-success);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.complexity-info {
|
||
background: var(--vp-c-bg-alt);
|
||
padding: 0.75rem;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.info-title {
|
||
font-weight: bold;
|
||
font-size: 0.85rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.complexity-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.25rem;
|
||
}
|
||
|
||
.complexity-item {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.c-name {
|
||
font-family: monospace;
|
||
font-weight: bold;
|
||
min-width: 60px;
|
||
}
|
||
|
||
.c-value {
|
||
min-width: 50px;
|
||
}
|
||
|
||
.c-value.good {
|
||
color: var(--vp-c-success);
|
||
}
|
||
.c-value.mid {
|
||
color: var(--vp-c-warning);
|
||
}
|
||
.c-value.bad {
|
||
color: var(--vp-c-danger);
|
||
}
|
||
|
||
.c-desc {
|
||
color: var(--vp-c-text-2);
|
||
}
|
||
|
||
.info-box {
|
||
background: var(--vp-c-bg-alt);
|
||
padding: 0.75rem;
|
||
border-radius: 6px;
|
||
font-size: 0.85rem;
|
||
color: var(--vp-c-text-2);
|
||
margin-top: 0.75rem;
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
}
|
||
</style>
|