排序算法可视化器 - 实战案例
通过交互式动画直观理解8种经典排序算法的工作原理、时间复杂度差异和实际执行效率
你将学到
- 算法可视化实现 - 将抽象的算法逻辑转化为直观的动画演示
- 异步编程与动画控制 - 使用async/await控制算法执行节奏
- 用户交互设计 - 实现复杂的控制面板和实时反馈系统
- 性能优化技巧 - 在大量DOM操作中保持流畅的动画效果
- 状态管理 - 管理算法执行、暂停、回放等多状态交互
前置知识
- JavaScript基础语法与异步编程
- HTML5 Canvas或DOM操作基础
- 基本的排序算法原理
- 事件处理与用户交互设计
相关课程参考:数组基础、时间复杂度与空间复杂度、递归基础
架构设计
┌─────────────────────────────────────────────────────┐
│ 排序算法可视化器 │
├─────────────────────────────────────────────────────┤
│ 控制面板 │ 可视化区域 │ 算法信息面板 │
│ - 算法选择 │ - 数组可视化 │ - 算法描述 │
│ - 速度控制 │ - 比较动画 │ - 复杂度分析 │
│ - 数组操作 │ - 交换动画 │ - 执行统计 │
└─────────────────────────────────────────────────────┘
数据流:
[用户操作] → [控制逻辑] → [排序算法] → [可视化引擎] → [用户界面]
↑ ↓
└───────────[状态管理]────────┘
实现步骤
步骤1:项目结构搭建
首先创建基础的HTML结构和CSS样式:
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>排序算法可视化器</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-container">
<!-- 控制面板 -->
<div class="control-panel">
<h1>排序算法可视化器</h1>
<div class="control-group">
<label for="algorithm">选择算法:</label>
<select id="algorithm">
<option value="bubble">冒泡排序</option>
<option value="selection">选择排序</option>
<option value="insertion">插入排序</option>
<option value="shell">希尔排序</option>
<option value="merge">归并排序</option>
<option value="quick">快速排序</option>
<option value="heap">堆排序</option>
<option value="counting">计数排序</option>
</select>
</div>
<div class="control-group">
<label for="speed">动画速度:</label>
<input type="range" id="speed" min="1" max="100" value="50">
<span id="speed-value">50</span>
</div>
<div class="control-group">
<label for="array-size">数组大小:</label>
<input type="range" id="array-size" min="5" max="100" value="20">
<span id="array-size-value">20</span>
</div>
<div class="button-group">
<button id="generate">生成随机数组</button>
<button id="start">开始排序</button>
<button id="pause" disabled>暂停</button>
<button id="resume" disabled>继续</button>
<button id="reset">重置</button>
</div>
<div class="stats-container">
<div class="stat">
<span class="stat-label">比较次数:</span>
<span id="comparisons" class="stat-value">0</span>
</div>
<div class="stat">
<span class="stat-label">交换次数:</span>
<span id="swaps" class="stat-value">0</span>
</div>
<div class="stat">
<span class="stat-label">当前状态:</span>
<span id="status" class="stat-value">就绪</span>
</div>
</div>
</div>
<!-- 可视化区域 -->
<div class="visualization-container">
<div class="array-container" id="array-container">
<!-- 数组柱状图将通过JavaScript动态生成 -->
</div>
<!-- 算法说明 -->
<div class="algorithm-info" id="algorithm-info">
<h3 id="algorithm-name">冒泡排序</h3>
<p id="algorithm-description">通过重复遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。</p>
<div class="complexity">
<div>时间复杂度: O(n²)</div>
<div>空间复杂度: O(1)</div>
</div>
</div>
</div>
</div>
<script src="sorters.js"></script>
<script src="visualizer.js"></script>
<script src="app.js"></script>
</body>
</html>
步骤2:CSS样式设计
/* styles.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
}
.control-panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.control-panel h1 {
color: #333;
margin-bottom: 20px;
text-align: center;
font-size: 1.4em;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #555;
}
.control-group select,
.control-group input[type="range"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 6px;
}
.button-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin: 20px 0;
}
.button-group button {
padding: 10px;
border: none;
border-radius: 6px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.button-group button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.button-group button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.stats-container {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-top: 20px;
}
.stat {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.stat-label {
font-weight: 600;
color: #666;
}
.stat-value {
color: #764ba2;
font-weight: 700;
}
.visualization-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.array-container {
display: flex;
align-items: flex-end;
justify-content: center;
height: 400px;
padding: 20px 0;
border-bottom: 2px solid #eee;
margin-bottom: 20px;
}
.array-bar {
flex: 1;
margin: 0 2px;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
border-radius: 4px 4px 0 0;
transition: height 0.1s ease;
position: relative;
}
.array-bar.comparing {
background: linear-gradient(180deg, #ff6b6b 0%, #ee5a24 100%);
}
.array-bar.swapping {
background: linear-gradient(180deg, #f9ca24 0%, #f0932b 100%);
}
.array-bar.sorted {
background: linear-gradient(180deg, #00b894 0%, #00cec9 100%);
}
.array-bar .value {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
font-weight: bold;
color: #333;
}
.algorithm-info {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
}
.algorithm-info h3 {
color: #764ba2;
margin-bottom: 10px;
font-size: 1.2em;
}
.algorithm-info p {
color: #666;
line-height: 1.6;
margin-bottom: 15px;
}
.complexity {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
background: white;
padding: 10px;
border-radius: 6px;
}
.complexity div {
text-align: center;
font-weight: 600;
color: #764ba2;
}
/* 响应式设计 */
@media (max-width: 768px) {
.app-container {
grid-template-columns: 1fr;
}
.array-container {
height: 300px;
}
}
步骤3:排序算法实现
// sorters.js - 排序算法集合
const Sorters = {
// 冒泡排序
bubbleSort: async (array, callbacks) => {
const arr = [...array];
let swapped;
for (let i = 0; i < arr.length; i++) {
swapped = false;
for (let j = 0; j < arr.length - i - 1; j++) {
// 记录比较操作
callbacks.onCompare(j, j + 1);
await callbacks.wait();
if (arr[j] > arr[j + 1]) {
// 记录交换操作
callbacks.onSwap(j, j + 1);
await callbacks.wait();
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swapped = true;
}
}
// 标记已排序
callbacks.onSorted(arr.length - i - 1);
if (!swapped) break;
}
return arr;
},
// 选择排序
selectionSort: async (array, callbacks) => {
const arr = [...array];
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < arr.length; j++) {
callbacks.onCompare(minIndex, j);
await callbacks.wait();
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex !== i) {
callbacks.onSwap(i, minIndex);
await callbacks.wait();
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
callbacks.onSorted(i);
}
callbacks.onSorted(arr.length - 1);
return arr;
},
// 插入排序
insertionSort: async (array, callbacks) => {
const arr = [...array];
for (let i = 1; i < arr.length; i++) {
let j = i;
while (j > 0) {
callbacks.onCompare(j - 1, j);
await callbacks.wait();
if (arr[j - 1] > arr[j]) {
callbacks.onSwap(j - 1, j);
await callbacks.wait();
[arr[j - 1], arr[j]] = [arr[j], arr[j - 1]];
j--;
} else {
break;
}
}
// 标记当前位置已插入
callbacks.onSorted(i);
}
callbacks.onSorted(0);
return arr;
},
// 希尔排序
shellSort: async (array, callbacks) => {
const arr = [...array];
let gap = Math.floor(arr.length / 2);
while (gap > 0) {
for (let i = gap; i < arr.length; i++) {
let j = i;
while (j >= gap) {
callbacks.onCompare(j - gap, j);
await callbacks.wait();
if (arr[j - gap] > arr[j]) {
callbacks.onSwap(j - gap, j);
await callbacks.wait();
[arr[j - gap], arr[j]] = [arr[j], arr[j - gap]];
j -= gap;
} else {
break;
}
}
}
gap = Math.floor(gap / 2);
}
// 标记所有元素已排序
for (let i = 0; i < arr.length; i++) {
callbacks.onSorted(i);
}
return arr;
},
// 归并排序
mergeSort: async (array, callbacks, start = 0, end = array.length - 1) => {
const arr = [...array];
if (start >= end) return [arr[start]];
const mid = Math.floor((start + end) / 2);
// 递归排序左右两部分
const left = await Sorters.mergeSort(arr, callbacks, start, mid);
const right = await Sorters.mergeSort(arr, callbacks, mid + 1, end);
// 合并两个有序数组
return await Sorters.merge(left, right, callbacks, start);
},
merge: async (left, right, callbacks, startIndex) => {
const result = [];
let i = 0, j = 0;
const originalArray = [...left, ...right];
while (i < left.length && j < right.length) {
// 记录比较操作
callbacks.onCompare(startIndex + i, startIndex + left.length + j);
await callbacks.wait();
if (left[i] <= right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
while (i < left.length) {
result.push(left[i++]);
}
while (j < right.length) {
result.push(right[j++]);
}
// 更新可视化
for (let k = 0; k < result.length; k++) {
callbacks.onUpdate(startIndex + k, result[k]);
await callbacks.wait();
callbacks.onSorted(startIndex + k);
}
return result;
},
// 快速排序
quickSort: async (array, callbacks, low = 0, high = array.length - 1) => {
const arr = [...array];
if (low < high) {
const pivotIndex = await Sorters.partition(arr, callbacks, low, high);
await Sorters.quickSort(arr, callbacks, low, pivotIndex - 1);
await Sorters.quickSort(arr, callbacks, pivotIndex + 1, high);
}
// 标记所有元素已排序
for (let i = low; i <= high; i++) {
callbacks.onSorted(i);
}
return arr;
},
partition: async (array, callbacks, low, high) => {
const pivot = array[high];
let i = low - 1;
for (let j = low; j < high; j++) {
callbacks.onCompare(j, high);
await callbacks.wait();
if (array[j] < pivot) {
i++;
callbacks.onSwap(i, j);
await callbacks.wait();
[array[i], array[j]] = [array[j], array[i]];
}
}
callbacks.onSwap(i + 1, high);
await callbacks.wait();
[array[i + 1], array[high]] = [array[high], array[i + 1]];
return i + 1;
},
// 堆排序
heapSort: async (array, callbacks) => {
const arr = [...array];
const n = arr.length;
// 构建最大堆
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
await Sorters.heapify(arr, n, i, callbacks);
}
// 逐个提取元素
for (let i = n - 1; i > 0; i--) {
callbacks.onSwap(0, i);
await callbacks.wait();
[arr[0], arr[i]] = [arr[i], arr[0]];
callbacks.onSorted(i);
await Sorters.heapify(arr, i, 0, callbacks);
}
callbacks.onSorted(0);
return arr;
},
heapify: async (array, n, i, callbacks) => {
let largest = i;
const left = 2 * i + 1;
const right = 2 * i + 2;
if (left < n) {
callbacks.onCompare(left, largest);
await callbacks.wait();
if (array[left] > array[largest]) {
largest = left;
}
}
if (right < n) {
callbacks.onCompare(right, largest);
await callbacks.wait();
if (array[right] > array[largest]) {
largest = right;
}
}
if (largest !== i) {
callbacks.onSwap(i, largest);
await callbacks.wait();
[array[i], array[largest]] = [array[largest], array[i]];
await Sorters.heapify(array, n, largest, callbacks);
}
},
// 计数排序
countingSort: async (array, callbacks) => {
const arr = [...array];
const max = Math.max(...arr);
const min = Math.min(...arr);
const range = max - min + 1;
const count = new Array(range).fill(0);
const output = new Array(arr.length);
// 统计每个元素的出现次数
for (let i = 0; i < arr.length; i++) {
count[arr[i] - min]++;
}
// 计算累积计数
for (let i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// 从后向前遍历,构建输出数组
for (let i = arr.length - 1; i >= 0; i--) {
callbacks.onCompare(i, i); // 简化展示
await callbacks.wait();
output[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}
// 将输出数组复制回原数组
for (let i = 0; i < arr.length; i++) {
callbacks.onUpdate(i, output[i]);
await callbacks.wait();
callbacks.onSorted(i);
}
return output;
}
};
// 导出排序器
if (typeof module !== 'undefined' && module.exports) {
module.exports = Sorters;
}
步骤4:可视化引擎实现
// visualizer.js - 可视化引擎
class Visualizer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.bars = [];
this.array = [];
this.isRunning = false;
this.isPaused = false;
this.animationSpeed = 50; // 1-100
this.delay = this.calculateDelay(this.animationSpeed);
}
// 初始化数组可视化
initialize(array) {
this.array = [...array];
this.container.innerHTML = '';
this.bars = [];
const maxValue = Math.max(...this.array);
this.array.forEach((value, index) => {
const bar = document.createElement('div');
bar.className = 'array-bar';
bar.style.height = `${(value / maxValue) * 100}%`;
bar.style.transition = `height 0.1s ease`;
// 添加数值标签
const valueLabel = document.createElement('div');
valueLabel.className = 'value';
valueLabel.textContent = value;
bar.appendChild(valueLabel);
this.container.appendChild(bar);
this.bars.push(bar);
});
}
// 计算动画延迟
calculateDelay(speed) {
// 速度值范围1-100,映射到延迟500ms-5ms
return Math.max(5, 500 - (speed * 5));
}
// 设置动画速度
setAnimationSpeed(speed) {
this.animationSpeed = speed;
this.delay = this.calculateDelay(speed);
}
// 等待函数(支持暂停)
async wait() {
if (this.isPaused) {
await new Promise(resolve => {
this.resumeResolve = resolve;
});
}
return new Promise(resolve => {
setTimeout(resolve, this.delay);
});
}
// 高亮比较的元素
highlightCompare(index1, index2) {
this.clearHighlights();
if (this.bars[index1]) {
this.bars[index1].classList.add('comparing');
}
if (this.bars[index2]) {
this.bars[index2].classList.add('comparing');
}
}
// 标记交换
highlightSwap(index1, index2) {
this.clearHighlights();
if (this.bars[index1]) {
this.bars[index1].classList.add('swapping');
}
if (this.bars[index2]) {
this.bars[index2].classList.add('swapping');
}
// 交换高度
const height1 = this.bars[index1].style.height;
const height2 = this.bars[index2].style.height;
this.bars[index1].style.height = height2;
this.bars[index2].style.height = height1;
// 交换数值标签
const value1 = this.bars[index1].querySelector('.value');
const value2 = this.bars[index2].querySelector('.value');
if (value1 && value2) {
const temp = value1.textContent;
value1.textContent = value2.textContent;
value2.textContent = temp;
}
}
// 更新元素值
updateValue(index, value) {
if (this.bars[index]) {
const maxValue = Math.max(...this.array);
this.bars[index].style.height = `${(value / maxValue) * 100}%`;
const valueLabel = this.bars[index].querySelector('.value');
if (valueLabel) {
valueLabel.textContent = value;
}
this.array[index] = value;
}
}
// 标记已排序
markSorted(index) {
if (this.bars[index]) {
this.bars[index].classList.remove('comparing', 'swapping');
this.bars[index].classList.add('sorted');
}
}
// 清除所有高亮
clearHighlights() {
this.bars.forEach(bar => {
bar.classList.remove('comparing', 'swapping');
});
}
// 重置所有状态
reset() {
this.isRunning = false;
this.isPaused = false;
this.clearHighlights();
this.bars.forEach(bar => {
bar.classList.remove('sorted', 'comparing', 'swapping');
});
}
// 暂停动画
pause() {
this.isPaused = true;
}
// 恢复动画
resume() {
this.isPaused = false;
if (this.resumeResolve) {
this.resumeResolve();
this.resumeResolve = null;
}
}
// 获取回调函数
getCallbacks() {
return {
onCompare: (i, j) => {
this.highlightCompare(i, j);
this.comparisonCount++;
},
onSwap: (i, j) => {
this.highlightSwap(i, j);
this.swapCount++;
},
onUpdate: (i, value) => {
this.updateValue(i, value);
},
onSorted: (i) => {
this.markSorted(i);
},
wait: () => this.wait()
};
}
// 重置统计计数
resetCounts() {
this.comparisonCount = 0;
this.swapCount = 0;
}
}
// 导出可视化器
if (typeof module !== 'undefined' && module.exports) {
module.exports = Visualizer;
}
步骤5:主应用逻辑
// app.js - 主应用逻辑
document.addEventListener('DOMContentLoaded', () => {
// DOM元素引用
const algorithmSelect = document.getElementById('algorithm');
const speedSlider = document.getElementById('speed');
const speedValue = document.getElementById('speed-value');
const arraySizeSlider = document.getElementById('array-size');
const arraySizeValue = document.getElementById('array-size-value');
const generateBtn = document.getElementById('generate');
const startBtn = document.getElementById('start');
const pauseBtn = document.getElementById('pause');
const resumeBtn = document.getElementById('resume');
const resetBtn = document.getElementById('reset');
const comparisonsDisplay = document.getElementById('comparisons');
const swapsDisplay = document.getElementById('swaps');
const statusDisplay = document.getElementById('status');
const algorithmNameDisplay = document.getElementById('algorithm-name');
const algorithmDescriptionDisplay = document.getElementById('algorithm-description');
// 初始化可视化器
const visualizer = new Visualizer('array-container');
// 算法信息
const algorithmInfo = {
bubble: {
name: '冒泡排序',
description: '通过重复遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。',
timeComplexity: 'O(n²)',
spaceComplexity: 'O(1)'
},
selection: {
name: '选择排序',
description: '首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。',
timeComplexity: 'O(n²)',
spaceComplexity: 'O(1)'
},
insertion: {
name: '插入排序',
description: '通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序)。',
timeComplexity: 'O(n²)',
spaceComplexity: 'O(1)'
},
shell: {
name: '希尔排序',
description: '也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。',
timeComplexity: 'O(n log n)',
spaceComplexity: 'O(1)'
},
merge: {
name: '归并排序',
description: '采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。',
timeComplexity: 'O(n log n)',
spaceComplexity: 'O(n)'
},
quick: {
name: '快速排序',
description: '通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。',
timeComplexity: 'O(n log n)',
spaceComplexity: 'O(log n)'
},
heap: {
name: '堆排序',
description: '利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。',
timeComplexity: 'O(n log n)',
spaceComplexity: 'O(1)'
},
counting: {
name: '计数排序',
description: '一个非基于比较的排序算法,该算法于1954年由Harold H. Seward提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序。',
timeComplexity: 'O(n + k)',
spaceComplexity: 'O(n + k)'
}
};
// 当前状态
let state = {
array: [],
isSorting: false,
currentAlgorithm: null,
arraySize: 20
};
// 生成随机数组
function generateRandomArray(size) {
const array = [];
for (let i = 0; i < size; i++) {
array.push(Math.floor(Math.random() * 100) + 1);
}
return array;
}
// 更新显示信息
function updateDisplay() {
comparisonsDisplay.textContent = visualizer.comparisonCount || 0;
swapsDisplay.textContent = visualizer.swapCount || 0;
}
// 更新算法信息
function updateAlgorithmInfo(algorithm) {
const info = algorithmInfo[algorithm];
algorithmNameDisplay.textContent = info.name;
algorithmDescriptionDisplay.textContent = info.description;
const complexityDiv = document.querySelector('.complexity');
complexityDiv.innerHTML = `
<div>时间复杂度: ${info.timeComplexity}</div>
<div>空间复杂度: ${info.spaceComplexity}</div>
`;
}
// 重置应用状态
function resetState() {
state.isSorting = false;
visualizer.reset();
visualizer.resetCounts();
updateDisplay();
startBtn.disabled = false;
pauseBtn.disabled = true;
resumeBtn.disabled = true;
statusDisplay.textContent = '就绪';
}
// 开始排序
async function startSorting() {
if (state.isSorting) return;
state.isSorting = true;
state.currentAlgorithm = algorithmSelect.value;
// 更新UI状态
startBtn.disabled = true;
pauseBtn.disabled = false;
resumeBtn.disabled = true;
statusDisplay.textContent = '排序中...';
// 更新算法信息
updateAlgorithmInfo(state