拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用HTML+CSS+JavaScript做冒泡排序可视化,24个数字动画演示

初学排序算法时很多人都有过这样的体验冒泡排序的代码不长逻辑看起来也直白可真要在脑子里模拟 24 个无序数字每轮交换后的样子很快就跟不上了。外层循环进入第 3 轮时数组的哪些区域已经稳定内层循环比较到哪一位就可以停下来如果某一轮一个交换都没发生是不是说明整个数组已经有序这些问题如果只看代码回答起来并不轻松。如果能把排序过程变成可视化的动画这些疑问会清晰得多。本文就用纯 HTML CSS JavaScript 实现一个 24 个数字的冒泡排序可视化页面整个页面不依赖任何框架和构建工具保存成一个 HTML 文件后浏览器双击就能运行。在动画里你会清楚地看到每一轮比较发生在哪两个数字之间哪些数字正在交换哪些数字已经排好并进入绿色区域以及理论上最多 276 次比较是如何一步步完成的。1. 冒泡排序一个“老牌但值得细看”的排序算法1.1 冒泡排序的基本思想冒泡排序Bubble Sort的核心思想是反复比较相邻的两个元素如果顺序不符合要求就把它们交换。以升序排序为例从数组的第一个元素开始依次比较arr[j]和arr[j 1]。如果arr[j] arr[j 1]说明前一个数字比后一个大交换它们。继续移动到下一对相邻元素重复上述比较与交换。当一轮比较结束后数组末尾一定是当前范围内最大的元素。下一轮比较时忽略已经排到末尾的元素重复整个过程。“冒泡”这个名字很形象较小的数字会像气泡一样逐渐往前移动较大的数字则会一层层“沉”到末尾。以数组[5, 1, 4, 2]为例第一轮比较结束后最大值 5 会被交换到索引3的位置之后 5 就不再参与后续比较。从算法性质来看冒泡排序有以下几个关键点时间复杂度最坏情况和平均情况都是O(n^2)如果数组已经有序并且代码加入了提前退出机制最好情况可以优化到O(n)。空间复杂度O(1)它只需要一个临时变量用来交换元素属于原地排序。稳定性相邻元素只有在前一个大于后一个时才交换相等元素不会交换相对顺序因此冒泡排序是稳定排序。1.2 为什么要把冒泡排序“可视化”学习排序算法时大部分人都会经历三个阶段能看懂代码、能手动模拟、能根据场景选择算法。手动模拟是很多人卡住的一步因为数据一旦多起来大脑的工作记忆会很快被占满。可视化最大的价值在于把时间维度拉进画面。你不再只是看到代码在反复跑循环而是能直观看到黄色指针在相邻元素之间移动代表正在进行比较红色闪烁的两个柱子代表正在发生交换绿色区域从右往左逐渐变长代表已排序部分不断增加如果某一轮完全没有发生交换动画可以提前结束而不是继续傻跑。很多同学在面试前临时背规范排序算法却很难说出优化点在哪里。通过可视化理解冒泡排序的“提前退出”和“有序区边界”之后这些细节会变成图形记忆比死记硬背牢固得多。1.3 为什么用 24 个数字来演示24 是一个非常合适的可视化规模。如果是 5 个数字排序过程太短还没来得及看清就结束了如果是 1000 个数字柱状图会被压缩得看不出细节动画时间也会变得太长。24 个数字既有足够的排列组合空间又能在普通屏幕上一根根清楚地展示。从计算量来看24 个数字最坏情况下需要比较23 22 21 ... 1 276 次如果在每一步之间等待约 80ms整体动画过程大概需要二十多秒这个时长对理解算法来说非常合适。你也可以通过速度滑块将其压缩到几秒或者放慢到半分钟观察每个细节。2. 环境准备只需要一个浏览器2.1 为什么选择原生 Web 技术排序可视化可以通过多种方案实现命令行输出、Python matplotlib 动画、Java Swing、JavaScript Canvas 等。本文选择HTML CSS JavaScript原因很实际零安装任何有浏览器的电脑都能运行不需要安装 Python、JDK 或任何包管理器。跨平台Windows、macOS、Linux 都能直接打开。交互自然按钮、滑块、暂停/继续功能用浏览器原生控件就能实现。绘图高效Canvas 2D 在 Web 端绘制几百根柱子非常流畅。算法逻辑本身和语言无关JavaScript 写的冒泡排序和其他语言并没有本质区别。如果你熟悉 C 或 Java下面的实现依然很容易看懂。2.2 目录结构与运行方式整个项目只需要一个文件例如bubble-sort-visualize.html。项目结构如下bubble-sort-visualize/ └── bubble-sort-visualize.html # 完整可视化页面运行方式有两种直接双击 HTML 文件用浏览器打开。在当前目录启动一个静态服务器例如python -m http.server 8080然后访问http://localhost:8080。后续代码都会围绕这一个文件展开。3. 先用 JavaScript 实现冒泡排序核心逻辑3.1 基础版冒泡排序先看最标准的冒泡排序写法这里用 JavaScript 来实现// 文件路径src/bubbleSort.js function bubbleSort(arr) { const n arr.length; for (let i 0; i n - 1; i) { for (let j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { // 交换相邻元素 [arr[j], arr[j 1]] [arr[j 1], arr[j]]; } } } return arr; } const data [64, 34, 25, 12, 22, 11, 90]; console.log(bubbleSort(data)); // 输出[11, 12, 22, 25, 34, 64, 90]代码中的[arr[j], arr[j 1]] [arr[j 1], arr[j]]是 ES6 的解构赋值交换技巧等价于let temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp;外层循环控制“总共需要跑多少轮”内层循环控制“当前轮需要比较到哪个位置”。因为每跑完一轮末尾就会多一个排好的元素所以内层循环上限是n - 1 - i。3.2 优化一提前退出如果原数组已经基本有序基础版依然会老老实实跑完所有外层循环造成大量无效比较。加入一个swapped标志就可以在某一轮完全没有交换时提前结束function bubbleSortOptimized(arr) { const n arr.length; for (let i 0; i n - 1; i) { let swapped false; for (let j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { [arr[j], arr[j 1]] [arr[j 1], arr[j]]; swapped true; } } // 如果这一轮没有发生任何交换说明数组已经有序 if (!swapped) { break; } } return arr; }在最好情况下也就是数组已经有序时这个版本只需要执行一轮比较比较n - 1次时间复杂度降到O(n)。可视化页面中每当一轮没有发生交换动画也会立即停止用户能直观看到“提前退出”的意义。3.3 优化二记录最后一次交换位置第二个优化思路是缩短下一轮的有序区边界。理论上如果最后一对交换发生在j和j 1之间那么索引j 1之后的所有元素已经在正确位置上下一轮只需要比较到j 1位置即可。function bubbleSortBoundary(arr) { let n arr.length; while (n 1) { let lastSwapIndex 0; for (let j 0; j n - 1; j) { if (arr[j] arr[j 1]) { [arr[j], arr[j 1]] [arr[j 1], arr[j]]; lastSwapIndex j 1; } } // 把下一轮的边界收缩到最后一次交换的位置 n lastSwapIndex; } return arr; }这种方式对“前半部分无序、后半部分已经有序”的数组非常友好。虽然本文的可视化页面还是采用“每轮末尾固定扩大一个绿色元素”的方式展示但你可以在扩展练习中尝试把这种优化也做进动画观察边界是如何一步步收缩的。如果大家习惯 C可以对比一下同样的逻辑// 文件路径bubbleSort.cpp void bubbleSort(vectorint arr) { int n arr.size(); for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; } }算法跨语言的核心逻辑是完全一致的差别只在语法层面。因此你在 HTML 页面里看到的动画过程同样适合解释 C 或 Java 中的冒泡排序行为。4. 可视化设计让 24 个数字“跑”起来4.1 可视化目标拆解在写代码之前先明确这个可视化页面需要满足哪些要求柱状图阵列24 个数字用 24 根柱子表示柱子的高度代表数字大小。颜色状态区分比较中的位置显示黄色交换中的位置显示红色已排序区域显示绿色普通位置显示蓝色。操作统计实时显示当前轮次、比较次数、交换次数。可控制性支持开始排序、暂停/继续、打乱数组以及调节动画速度。防误操作排序中不允许重复启动重置数组时能安全中止旧任务。4.2 使用 Canvas 绘制而不是操作 DOM有人可能会问为什么不直接创建 24 个div每次排序时改它的高度和颜色用 DOM 当然可以实现但存在两个问题频繁操作 DOM 开销大。排序每走一步可能要同时改多个 div 的height和background如果步数达到几百次页面很容易出现卡顿。逻辑分散。颜色、高度、位置都要分别更新代码可读性差。Canvas 的做法是每次状态变化后整帧重绘由clearRect fillRect完成绘制。24 个柱子的重绘量非常小性能完全不是问题而且绘制逻辑集中在一个draw()函数里数据状态与表现状态天然清晰分离。4.3 核心难点让排序过程可暂停、可继续这是整个可视化页面最容易踩坑的地方。如果直接调用同步的bubbleSort()JavaScript 会一口气把排序执行完。由于浏览器的主线程被占住页面无法重新绘制动画你只能看到最终结果。这正是很多初学者在写可视化时“动画一闪而过”的根本原因。解决思路是把排序函数改成异步函数在每次比较和交换之后await一个等待操作把控制权交还给浏览器让浏览器有机会绘制当前帧。function sleep(ms) { return new Promise(resolve { let remaining ms; const timer setInterval(() { if (stopSignal) { clearInterval(timer); resolve(); return; } if (paused) { return; // 暂停状态不减少 remaining } remaining - 20; if (remaining 0) { clearInterval(timer); resolve(); } }, 20); }); }在这个sleep实现里每次等待分成若干个 20ms 的小片段。如果暂停标志paused为true剩余时间不减少排序就“卡住”了点击继续后剩余时间继续减少。如果重置按钮设置了stopSignal等待会立即结束后续排序逻辑发现信号后主动退出避免旧任务继续操作新数组。4.4 状态字段设计为了让绘制函数知道当前怎么上色需要维护以下状态状态字段说明普通状态-默认蓝色比较中compareIndex黄色存储[j, j 1]交换中swapIndex红色存储[j, j 1]已排序区sortedCount绿色用于计算已就位的柱子数量排序标志isSorting防止重复启动暂停标志paused暂停状态下等待不结束停止信号stopSignal重置数组时中止排序任务每次排序循环推进时更新这些字段然后调用draw()页面就会立刻看到最新状态。5. 完整实战24 个数字冒泡排序可视化页面5.1 页面 HTML 结构页面分为四个区域标题、画布、统计信息、控制按钮。div classcontainer h2冒泡排序可视化24 个数字/h2 canvas idsortCanvas width720 height360/canvas div classstats span轮次b idroundIndex0/b/span span比较次数b idcompareCount0/b/span span交换次数b idswapCount0/b/span /div div classcontrols button idstartBtn开始排序/button button idpauseBtn disabled暂停/button button idresetBtn打乱数组/button label forspeedRange速度 input idspeedRange typerange min10 max300 value80 /label /div /div速度滑块的值单位是毫秒。值越小动画越快值越大动画越慢。默认 80ms在 24 个数字场景下整体节奏比较适中。5.2 CSS 样式样式采用深色背景重点突出画布区域和状态颜色。* { box-sizing: border-box; } body { margin: 0; padding: 20px; background: #1e1e2e; font-family: Microsoft YaHei, PingFang SC, sans-serif; color: #e0e0e0; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .container { background: #282a36; border-radius: 12px; padding: 24px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); width: fit-content; } h2 { margin: 0 0 16px; font-size: 20px; text-align: center; } canvas { display: block; background: #1e1e28; border-radius: 8px; margin-bottom: 16px; } .stats { display: flex; justify-content: space-around; margin-bottom: 16px; font-size: 14px; } .stats b { color: #f8f8f2; } .controls { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; align-items: center; } button { padding: 8px 18px; font-size: 14px; border: none; border-radius: 6px; background: #6272a4; color: #f8f8f2; cursor: pointer; transition: background 0.2s; } button:hover { background: #7085c0; } button:disabled { opacity: 0.5; cursor: not-allowed; } input[typerange] { vertical-align: middle; }5.3 JavaScript 核心代码首先是全局变量和初始化函数。为了生成不重复的 24 个数字使用 Fisher-Yates 洗牌算法生成 1 到 24 的随机排列。const canvas document.getElementById(sortCanvas); const ctx canvas.getContext(2d); const startBtn document.getElementById(startBtn); const pauseBtn document.getElementById(pauseBtn); const resetBtn document.getElementById(resetBtn); const speedInput document.getElementById(speedRange); const roundSpan document.getElementById(roundIndex); const compareSpan document.getElementById(compareCount); const swapSpan document.getElementById(swapCount); const SIZE 24; let arr []; let compareIndex null; let swapIndex null; let sortedCount 0; let compareCount 0; let swapCount 0; let roundIndex 0; let isSorting false; let paused false; let stopSignal false; function generateRandomArray(size) { const arr []; for (let i 1; i size; i) { arr.push(i); } for (let i arr.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [arr[i], arr[j]] [arr[j], arr[i]]; } return arr; } function updateStats() { roundSpan.textContent roundIndex; compareSpan.textContent compareCount; swapSpan.textContent swapCount; } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); const barWidth canvas.width / arr.length; const maxValue Math.max(...arr); for (let i 0; i arr.length; i) { const barHeight (arr[i] / maxValue) * (canvas.height - 20); const x i * barWidth; const y canvas.height - barHeight; let color #4a9eff; if (compareIndex compareIndex.includes(i)) { color #ffc53d; } if (swapIndex swapIndex.includes(i)) { color #ff4d4f; } if (i arr.length - sortedCount) { color #52c41a; } ctx.fillStyle color; ctx.fillRect(x 1, y, barWidth - 2, barHeight); } updateStats(); }接下来是sleep函数和排序主逻辑。function sleep(ms) { return new Promise(resolve { let remaining ms; const timer setInterval(() { if (stopSignal) { clearInterval(timer); resolve(); return; } if (paused) { return; } remaining - 20; if (remaining 0) { clearInterval(timer); resolve(); } }, 20); }); } async function runBubbleSort() { if (isSorting) return; isSorting true; stopSignal false; startBtn.disabled true; pauseBtn.disabled false; const n arr.length; for (let i 0; i n - 1; i) { if (stopSignal) break; roundIndex i 1; let swapped false; for (let j 0; j n - 1 - i; j) { if (stopSignal) break; compareIndex [j, j 1]; swapIndex null; draw(); await sleep(speedInput.value); if (stopSignal) return; compareCount; if (arr[j] arr[j 1]) { swapIndex [j, j 1]; [arr[j], arr[j 1]] [arr[j 1], arr[j]]; swapCount; draw(); await sleep(speedInput.value); if (stopSignal) return; swapped true; } } sortedCount; compareIndex null; swapIndex null; if (!swapped) break; } if (!stopSignal) { sortedCount n; draw(); } isSorting false; startBtn.disabled false; pauseBtn.disabled true; pauseBtn.textContent 暂停; paused false; }最后是按钮事件绑定和初始化。startBtn.addEventListener(click, runBubbleSort); pauseBtn.addEventListener(click, () { if (!isSorting) return; paused !paused; pauseBtn.textContent paused ? 继续 : 暂停; }); resetBtn.addEventListener(click, () { stopSignal true; init(); }); function init() { isSorting false; paused false; stopSignal false; compareIndex null; swapIndex null; sortedCount 0; compareCount 0; swapCount 0; roundIndex 0; arr generateRandomArray(SIZE); draw(); startBtn.disabled false; pauseBtn.disabled true; pauseBtn.textContent 暂停; } init();5.4 运行效果与关键交互把bubble-sort-visualize.html保存到本地并用浏览器打开页面会随机生成 24 根蓝色柱子。点击“开始排序”后你会看到黄色指针从左侧开始一步一步地在相邻两根柱子之间移动。如果黄色指针指向的左侧柱子比右侧高两根柱子会变红并在画面中完成一次高度交换。每当外层循环完成一轮右侧就会出现一根绿色柱子并且逐步向左蔓延。如果某一轮没有任何交换排序会立即停止此时绿色区域可能尚未覆盖所有柱子但数组已经有序。点击“暂停”可以随时中断动画再点击“继续”从当前位置恢复。点击“打乱数组”会清空排序状态重新生成一组随机数字。整个过程中统计区的“轮次、比较次数、交换次数”会同步刷新。你还能看到实际比较次数往往少于理论最大值的276次因为提前退出机制会在数组有序后立刻收工。6. 常见问题与排查思路在实际开发或调试可视化页面时比较容易遇到下面几个问题问题现象常见原因解决思路点击开始排序没有反应isSorting一直为true或按钮事件未绑定检查事件监听是否注册运行前打印isSorting状态动画一闪而过看不到过程排序函数是同步的主线程被阻塞将排序函数改为async每次比较和交换后await sleep()暂停后点击继续没有恢复sleep函数没有检查paused标志使用setInterval轮询paused状态暂停期间不减少剩余时间排序过程中打乱数组旧任务继续修改数据缺少停止信号添加stopSignal在sleep和循环中判断并主动退出多次点击开始按钮产生多个并发排序任务缺少互斥判断在排序函数开头判断isSorting为true时直接返回柱子底部没有对齐或高度越界计算高度时没有考虑画布边距使用canvas.height - 20作为最大高度留出底部安全距离绿色已排序区域显示错误sortedCount更新时机不对每一轮外层循环结束后再sortedCount最后强制设为n其中最隐蔽的是第五个问题。如果用户快速双击“开始排序”异步函数会同时启动多个任务它们共同修改同一个数组最终画面的颜色和统计数据都会乱掉。解决办法就是加一把简单的锁if (isSorting) return; isSorting true;排序结束时再把isSorting置回false。这个思路在很多异步场景里都能复用。7. 最佳实践与工程建议7.1 数据、状态与视图分离整个页面虽然只有一个 HTML 文件但代码结构上应该划分为三层数据层arr数组。状态层compareIndex、swapIndex、sortedCount、compareCount、swapCount、isSorting、paused、stopSignal。视图层draw()函数。draw()只负责根据当前数组和状态绘制画面不负责修改数组。排序算法只负责修改数组和状态不需要关心柱子的坐标。这种拆分方式在项目扩大后特别有用比如以后要加入插入排序可视化只需要实现新的排序函数复用同一套draw()。7.2 主动停止信号比强制刷新更安全很多人在处理“重置按钮”时会直接调用location.reload()来重载页面。这样虽然简单但用户体验很差而且会丢失排序统计信息。更好的做法是设置一个stopSignal让正在运行的异步函数在下一个安全位置主动退出然后调用init()重置页面状态。这种“协作式取消”的思路在异步编程中非常常见类似前端请求中的取消令牌CancelToken概念。7.3 合理控制动画性能在 24 个数字的场景下每帧绘制 24 个矩形性能完全不是问题。但如果以后要扩展到几百个数字可以考虑以下优化将draw()中的Math.max(...arr)改为排序前一次性计算避免每帧扫描整个数组。使用requestAnimationFrame统一控制绘制频率避免在极度快速的排序中重复绘制无效帧。对柱子填充颜色做缓存减少每次计算状态的开销。不过对于本文的规模保持代码简单更重要。7.4 让排序算法可测试可视化代码虽然好看但排序逻辑本身也值得单独验证。实际操作中可以把bubbleSort抽象成一个纯函数function bubbleSort(arr) { const result [...arr]; // 排序逻辑... return result; }然后写一段简单的测试代码验证排序结果和稳定性。这样当可视化页面出现异常时你能快速确认是算法问题还是渲染问题。7.5 后续扩展方向这个可视化页面完成后可以继续扩展让它的价值更大增加更多排序算法选择排序、插入排序、快速排序、归并排序共用同一套状态管理和绘制逻辑。增加单步模式点击一次按钮只执行一步比较或交换适合课堂演示。增加序列展示每一轮结束后的数组状态可以记录到日志区域方便回放。增加逆序度显示统计当前数组的逆序对数量观察排序过程中逆序对如何逐步减少。无论怎么扩展核心思想不变让算法过程可见、可交互、可解释。8. 自然收尾的实践建议冒泡排序在工业级项目中并不是首选算法因为平均时间复杂度较高
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门