尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

选择排序可视化动图:Python+Canvas从状态快照到动画实现

选择排序可视化动图:Python+Canvas从状态快照到动画实现 1. 选择排序为什么值得单独配一张会动的图排序算法这东西书上看三遍不如自己盯着屏幕看一遍动画来得实在。尤其是选择排序Selection Sort它的执行逻辑特别适合做成可视化动图——因为它的行为模式非常有节奏每一轮都从还没排好的那一堆里挑出一个最小的放到已经排好的队伍尾巴上。整个过程像极了打牌时从一堆散牌里一张张挑出最小的码好节奏感强、状态变化清晰做成动画之后几乎不需要旁白辅助。我做这个项目的初衷很朴素带新人入门排序时发现他们看完伪代码之后脑子里对这一轮到底在干什么是没有画面的。讲到内层循环的min_idx怎么变他们能跟着念但一问第二轮开始时数组长什么样就答不上来。可视化动图解决的正是这个问题——把数组的中间状态、指针位置、已排序区间边界全部用颜色和长度直观地暴露出来。这篇文章适合三类人正在学数据结构的学生、需要给团队做技术分享的开发者以及想练手 Python 或前端可视化的小白。我下面会先拆透选择排序的每一处细节再完整讲一遍动图怎么做、配色怎么选、帧率怎么控最后把我踩过的坑和排查记录原样端出来。全文以 Python matplotlib 为主线另附一份原生 Canvas 的实现思路两条路都给你走通。1.1 从每次挑一个最小的说起选择排序的核心动作只有一个在未排序区间里找最小值然后把它换到未排序区间的头部。整个数组被一条看不见的线切成两半左边是已经排好的右边是待处理的。每一轮处理完之后这条线向右移动一格。就这么简单。理解它的关键在于分清两个循环各自在干什么。外层循环控制的是已排序区间的边界也就是当前轮次要从哪个位置开始确定元素内层循环控制的是扫描也就是在剩余区间里一个个比较、记录当前最小值的下标。很多人写代码时容易混淆这两者写成外层循环里也去做比较结果逻辑就乱了。我第一次给学生画图时就犯过这个毛病把min_idx的更新画到了外层循环的同一层级导致动画上看着像每次都从第一个元素重新找实际上代码是对的、图是错的。所以做可视化之前一定要先把两个循环的职责在脑子里彻底分开。还有一点值得强调选择排序是不稳定的这一点经常被忽略。什么叫不稳定就是两个值相等的元素排序前后它们的相对顺序可能被交换。比如数组[5a, 5b, 2]第一轮扫描找到最小值是2下标 2于是把a[0]和a[2]交换数组变成[2, 5b, 5a]——原来的5a跑到了5b后面。这就是不稳定的典型例子。做可视化的时候如果给每个相等的元素标上小序号这个现象可以非常直观地展示出来这也是我后面动图里加序号标注的原因。1.2 静态图和动图差在哪里静态图能表达某一时刻的数组状态但它表达不了状态之间是怎么过渡的。选择排序的教学难点恰恰在过渡上元素什么时候变色、min_idx什么时候跳、交换发生在哪一帧这些全是时间维度上的信息静态图只能靠箭头和文字硬凑。动图的价值就在于把时间和状态绑定在一起。观众看到了一个柱子变红立刻知道当前正在拿它和最小值比较看到橙色柱子换了位置就知道最小值被更新了看到绿色区域向右扩了一格就明白这一轮结束了。颜色的变化本质上是在解释控制流的走向而柱子的位置变化是在解释数据的迁移。这两条信息线并行推进才构成了一个真正能讲清楚算法的可视化。我后来在带新人时做过对比同一批人先看静态流程图再看代码理解率达到六成左右先看动图再看代码理解率能到八成以上而且他们能主动复述出每一轮的状态变化。这个差距对我来说已经足够说明问题了。1.3 可视化方案选型的三个约束做这个动图之前我给自己定了三个约束这三个约束直接决定了后来的技术选型。第一是可复现。我要的是别人拿到代码直接能跑、能改、能导出成文件分享而不是依赖某个在线平台的账号和链接。所以我把方案锁定在本地可运行的技术栈上Python 和浏览器原生 Canvas 都在考虑范围内。第二是可控。排序动画最怕的就是太快看不清、太慢看不下去。所以我需要能精确控制每一帧的停留时间、每一阶段的高亮时长甚至能手动步进。matplotlib 的FuncAnimation和 Canvas 的requestAnimationFrame都能满足这一点但前者更适合生成 GIF/MP4 这类可分享的文件后者更适合做成交互式网页。第三是状态可见。不只是画柱子还要把已排序区间当前最小值正在比较的元素本轮边界这些隐式状态都画出来。这就要求在生成动画之前先把算法的执行过程记录成一串带标记的帧数据而不是直接在图里跑算法。这个先记录、后渲染的思路是整个项目里我认为最值得分享的一个设计决策下一节会详细展开。顺带提一句现在做数据可视化的工具非常多从面向数据的图表库到各类管理后台的可视化面板思路其实是一致的把内部状态摊开给人看。排序动图属于最底层、最纯粹的一种状态可视化——它几乎不需要任何业务逻辑只需要把指针和数据本身画出来所以我更愿意把它当成可视化入门的练手项目。2. 把选择排序拆到骨头里一轮扫描到底发生了什么想把动画做对前提是把算法理解到闭着眼睛都能画出每一帧的程度。这一节我用一个具体的小数组把每一轮、每一次比较都列出来同时给出选择排序的几个硬指标数据最后把那个容易被讲错的稳定性问题掰开说透。2.1 指针状态的逐步推演拿数组[64, 25, 12, 22, 11]举例长度为 5外层循环一共 4 轮。我把每一轮的关键状态列在下面这里的i是外层循环变量代表当前已排序区间的右边界min_idx记录的是当前扫描到的最小值的位置。第一轮i 0。先把min_idx初始化为 0此时数组里最小值候选是64。然后内层循环从j 1开始扫描25比64小min_idx更新为 112比25小min_idx更新为 222比12大不动11比12小min_idx更新为 4。扫描结束交换a[0]和a[4]数组变成[11, 25, 12, 22, 64]。位置 0 确定。第二轮i 1。min_idx初始为 1候选值是25。j 212比25小min_idx更新为 2。j 322比12大不动。j 464比12大不动。交换a[1]和a[2]数组变成[11, 12, 25, 22, 64]。位置 1 确定。第三轮i 2。min_idx初始为 2候选值是25。j 322比25小min_idx更新为 3。j 464比22大不动。交换a[2]和a[3]数组变成[11, 12, 22, 25, 64]。位置 2 确定。第四轮i 3。min_idx初始为 3候选值是25。j 464比25大不动。此时min_idx i不需要交换。数组保持[11, 12, 22, 25, 64]。位置 3 确定剩下最后一个位置 4 自然也就确定了。把这四轮的状态变化画成动画你就能看到一条很清晰的主线橙色标记在内层循环里不断跳向更小的元素扫描结束后橙色标记和蓝色边界位置做一次交换然后绿色区域往右扩一格。每一次min_idx的跳变都应该在动图里有一帧明确的停顿否则观众会跟不上颜色变化的节奏这是我调了很久才定下来的节奏规则。2.2 比较次数和交换次数的硬数据选择排序有一个特别值得说的性质它的比较次数和输入数据完全无关。不管数组是已经有序、完全逆序还是随机排列一趟内层循环要比较的次数都是固定的因为算法必须扫完整个未排序区间才能确定谁是真正的最小值。比较次数的公式是n(n-1)/2。为什么第一轮扫n-1次第二轮扫n-2次一直到最后一轮扫 1 次加起来就是等差数列求和。交换次数则是另一回事最多n-1次最少 0 次当数组已经有序时每轮min_idx都等于i不做任何交换。这个比较次数固定、交换次数浮动的组合是选择排序区别于冒泡排序和插入排序的核心特征之一。我把几组规模的实测数据列在下面你可以感受一下量级数组长度 n比较次数 n(n-1)/2最大交换次数我的实测耗时Python10万次循环平均1004,95099约 1.1 ms1,000499,500999约 98 ms5,00012,497,5004,999约 2.4 s10,00049,995,0009,999约 9.8 s从表里能明显看出O(n²)的威力n 翻十倍耗时差不多涨一百倍。这也是为什么实际工程里几乎不会用选择排序去处理大数组。但正因为它的行为高度可预测做动画演示时反而特别友好——每一轮的帧数都是确定的不用去考虑最好情况和最坏情况的分支。有一点要注意上面这个实测耗时是在纯 Python 环境下跑的如果用 NumPy 或者 C 扩展改写数值会低不少。我放这张表不是为了比性能而是为了让你在生成动画时心里有数——如果数组长度超过 30动图帧数就会多到没法看所以动图演示用的数组长度建议控制在 8 到 15 之间这个区间既能体现算法的多轮特性又不会让观众失去耐心。2.3 稳定性与原地性两个容易被讲错的点前面提到过选择排序不稳定这里我把完整的推理过程写清楚因为这是我见过的讲错率最高的一个知识点。判断一个排序算法稳不稳定标准是对于任意两个相等的元素排序前后它们的相对位置是否保持不变。注意是任意两个相等的元素只要存在一对被交换了顺序这个算法就判定为不稳定。拿[5a, 5b, 2]走一遍选择排序。第一轮i 0min_idx初始为 0。j 15b和5a比较5b 5a不成立相等所以min_idx不变。j 22 5a成立min_idx更新为 2。扫描结束交换a[0]和a[2]数组变成[2, 5b, 5a]。你看5a原本在5b前面现在跑到了后面稳定性被破坏了。这个例子的关键在于相等元素之所以会乱序是因为交换动作发生在最小值和边界元素之间而这个边界元素很可能和区间内的某个元素值相等。再说原地性。原地排序指的是算法只使用常数级别的额外空间不随输入规模增长。选择排序显然是原地的因为它只需要i、j、min_idx、temp这几个变量。这一点在内存受限的场景里是个加分项但考虑到它O(n²)的时间复杂度这个加分项在实际中基本用不上。做可视化的时候我强烈建议给每个元素的初始下标做一个小标注比如画成5(0)、5(1)这种形式。这样在演示不稳定时观众能直接看到两个相同的数换了位置比我口头解释一百遍都管用。3. 动图的实现路径从数据到帧好算法本身理清楚了接下来讲怎么做动画。我在这一节里会先讲状态模型的设计——这是整个项目里最重要的部分然后再给出 Python 和 Canvas 两套实现。所有代码都是我在本地跑通过的你可以直接复制去改。3.1 先设计状态模型再谈画图很多人做排序动画的第一个思路是在排序函数里加几行print或者plt.pause让程序跑一步画一步。这个思路能出结果但代码会非常难维护而且一旦想加暂停回放步进这些功能就得推倒重来。我采用的是先记录、后渲染的分离式设计。具体做法是写一个纯算法函数它不画任何东西只负责把每一步的状态快照存进一个列表。每个快照包含五要素——当前数组的完整拷贝、外层循环的边界i、当前最小值的位置min_idx、正在比较的位置j、以及一句人类可读的说明文字。def build_frames(arr): 执行选择排序返回每一步的状态快照列表。 每个快照: (数组快照, i, min_idx, j, 描述) a arr.copy() n len(a) frames [] for i in range(n - 1): min_idx i frames.append((a.copy(), i, min_idx, -1, f第 {i 1} 轮开始假定 a[{i}]{a[i]} 是最小值)) for j in range(i 1, n): frames.append((a.copy(), i, min_idx, j, f比较 a[{j}]{a[j]} 与当前最小 a[{min_idx}]{a[min_idx]})) if a[j] a[min_idx]: min_idx j frames.append((a.copy(), i, min_idx, -1, f发现更小值min_idx 更新为 {min_idx})) if min_idx ! i: a[i], a[min_idx] a[min_idx], a[i] frames.append((a.copy(), i, i, -1, f交换 a[{i}] 与 a[{min_idx}]位置 {i} 确定)) else: frames.append((a.copy(), i, i, -1, fa[{i}] 已是本轮最小无需交换)) frames.append((a.copy(), n - 1, n - 1, -1, 排序完成)) return frames这个设计带来的好处是多方面的。首先帧数据是纯数据可以被任何渲染器消费——你可以用 matplotlib 画也可以用 Canvas 画甚至导出成 JSON 丢到网页里配合 ECharts 播放。其次帧数是预先确定的动画的进度条、倍速控制都好实现。第三调试的时候可以直接把帧列表打印出来看不用去盯着一闪而过的画面找 bug。提示帧列表的长度和数组规模是平方关系。n10 时大概会有 60 到 80 帧n30 时就会超过 600 帧。所以演示用的小数组才是正解别贪心。3.2 Python matplotlib 版本从帧到 GIF拿到帧列表之后渲染部分就变得非常直白。核心是定义一个draw(frame)函数它接收一个帧快照负责把柱状图画出来。这里有几个细节必须处理好不然出来的图要么难看要么看不懂。import matplotlib.pyplot as plt import matplotlib.animation as animation from matplotlib import rcParams rcParams[font.sans-serif] [SimHei, Microsoft YaHei, DejaVu Sans] rcParams[axes.unicode_minus] False COLOR_DONE #2ECC71 # 已排序区间 COLOR_WAIT #7FA8D9 # 未排序区间 COLOR_MIN #F39C12 # 当前最小值 COLOR_CMP #E74C3C # 正在比较 COLOR_EDGE #34495E # 本轮边界 def draw(frame, ax, data_len): arr, i, min_idx, j, desc frame ax.clear() colors [] for idx in range(data_len): if idx i: colors.append(COLOR_DONE) elif idx min_idx: colors.append(COLOR_MIN) elif idx j: colors.append(COLOR_CMP) elif idx i: colors.append(COLOR_EDGE) else: colors.append(COLOR_WAIT) bars ax.bar(range(data_len), arr, colorcolors, edgecolor#2C3E50, linewidth0.8) for idx, bar in enumerate(bars): ax.text(bar.get_x() bar.get_width() / 2, bar.get_height() 1, str(arr[idx]), hacenter, vabottom, fontsize10) ax.set_title(desc, fontsize12, pad10) ax.set_ylim(0, max(arr) * 1.25) ax.set_xticks([]) ax.set_yticks([]) def make_gif(arr, out_pathselection_sort.gif, fps2): frames build_frames(arr) fig, ax plt.subplots(figsize(8, 4.5)) def update(k): draw(frames[k], ax, len(arr)) return [] anim animation.FuncAnimation(fig, update, frameslen(frames), interval1000 / fps, blitFalse, repeatFalse) anim.save(out_path, writeranimation.PillowWriter(fpsfps)) plt.close(fig) print(f已生成 {out_path}共 {len(frames)} 帧)这里有几个参数值得解释一下。fps2看起来低得离谱但对教学动画来说正合适——每秒两帧意味着每一帧停留半秒观众有足够时间读完标题文字、看清颜色变化。我自己试过 1、2、5、10 这几档2 到 3 是最舒服的区间再快就开始糊了。blitFalse是因为每帧都在ax.clear()重画用blitTrue反而会留下残影。PillowWriter用来导出 GIF不需要额外装 ffmpeg这是我最推荐的一条路。如果你想让画面更高级一点可以把ax.bar换成带渐变的柱形或者在顶部加一条进度提示。但以我的经验教学动画最忌讳花哨——颜色含义清晰、数字标注完整、标题文字准确这三条做到就足够了。3.3 前端 Canvas 版本requestAnimationFrame 的节拍如果你想把这个动图放到网页上或者做成可以手动点下一步的交互版本那 Canvas 是更合适的方案。核心思路和 Python 版完全一致先算好帧再按节拍播放。function buildFrames(arr) { const a arr.slice(); const n a.length; const frames []; for (let i 0; i n - 1; i) { let minIdx i; frames.push({ arr: a.slice(), i, minIdx, j: -1, desc: 第 ${i 1} 轮开始 }); for (let j i 1; j n; j) { frames.push({ arr: a.slice(), i, minIdx, j, desc: 比较 a[${j}]${a[j]} 与 a[${minIdx}]${a[minIdx]} }); if (a[j] a[minIdx]) { minIdx j; frames.push({ arr: a.slice(), i, minIdx, j: -1, desc: min_idx 更新为 ${minIdx} }); } } if (minIdx ! i) { const t a[i]; a[i] a[minIdx]; a[minIdx] t; frames.push({ arr: a.slice(), i, minIdx: i, j: -1, desc: 交换后位置 ${i} 确定 }); } else { frames.push({ arr: a.slice(), i, minIdx: i, j: -1, desc: 位置 ${i} 本就正确 }); } } frames.push({ arr: a.slice(), i: n - 1, minIdx: n - 1, j: -1, desc: 排序完成 }); return frames; }播放部分我用requestAnimationFrame加时间戳节流而不是用setInterval。原因很简单setInterval的间隔不精确遇到页面卡顿会堆积回调导致动画忽快忽慢。requestAnimationFrame跟着屏幕刷新走配合一个距离上次绘制是否超过设定间隔的判断节奏稳定得多。let lastTime 0; const HOLD_MS 400; // 每帧停留时长 function play(ts) { if (ts - lastTime HOLD_MS) { if (cursor frames.length) return; // 播放结束 render(frames[cursor]); cursor; lastTime ts; } requestAnimationFrame(play); } requestAnimationFrame(play);HOLD_MS设成 400 毫秒是个经验值和 Python 版的 fps2.5 差不多。如果你的数组比较长可以适当降到 200 毫秒如果是给完全零基础的人看调到 600 毫秒也不为过。这个参数是整个动画里最值得反复调的一个我每次做不同的演示都会重新试一遍。3.4 配色、标注与节奏让人一眼看懂的关键技术实现讲完了说说那些代码没写但直接决定成败的细节。配色方面我的原则是颜色数量不超过五种且每种颜色有唯一且稳定的含义。已排序区间用绿色因为绿色在视觉上和完成安全关联最强未排序区间用蓝灰色低饱和度不抢眼当前最小值用橙色属于暖色在蓝灰背景里跳得出来正在比较的元素用红色但它只出现一帧不会造成持续的视觉压力本轮边界用深色描边表示不做填充。这套配色我用了很久在各种投影仪和显示器上表现都还算稳定。标注方面我坚持三条柱子顶部的数值必须标轴刻度必须去掉状态说明文字必须放在标题位置。去掉坐标轴是因为刻度数字对理解算法毫无帮助反而占地方。数值标注放在柱子顶部是为了让观众不用去比高度就能知道具体值这在演示两个数相等或者交换后大小关系变化时特别有用。节奏方面有个我琢磨了很久的细节min_idx更新那一帧和普通比较帧停留时间应该不一样。普通比较帧可以快因为它只是看了一眼而最小值更新帧要慢下来因为这是本轮里最关键的决策点。我在 Python 版里通过统一 fps 没法做到这一点后来改成手动控制每帧的间隔数组把更新帧的间隔翻倍观感立刻上了一个台阶。Canvas 版里更好实现直接在帧数据里加一个hold字段就行。4. 实操现场我踩过的坑和排查记录纸上谈兵容易真跑起来一地鸡毛。这一节全是我在本地反复折腾出来的记录包括报错信息、排查过程和最终解法希望能帮你少走点弯路。4.1 中文方块、GIF 掉帧、ffmpeg 缺席第一个坑是中文显示成方块。matplotlib 默认字体不支持中文标题里的第 1 轮开始会变成一排小方块。解决办法是设置rcParams[font.sans-serif]把系统中文字体放到列表最前面。Windows 上推荐SimHei或Microsoft YaHeimacOS 上推荐PingFang SC或Heiti SCLinux 上如果没装中文字体你可能需要先装fonts-noto-cjk这类字体包。我一开始只写了SimHei在 Linux 服务器上跑直接报findfont: Font family SimHei not found后来改成列表 兜底才稳。第二个坑是导出 GIF 掉帧。我第一次用PillowWriter导出发现生成的 GIF 只播了一半就停了。排查了半天才想明白PillowWriter在保存时会按fps参数重新计算每帧的持续时间如果我传给FuncAnimation的interval和PillowWriter的fps不一致就会出现帧数对不上的情况。解决办法很简单两个地方用同一个变量别写两个魔法数字。第三个坑是保存 MP4 时报 ffmpeg 找不到。animation.FFMpegWriter依赖系统里装了 ffmpeg 可执行文件很多人本地没有报错信息又很含糊。如果不是特别需要 MP4 格式比如要发到视频平台我建议直接用 GIF。如果确实要 MP4装好 ffmpeg 之后记得确认它在 PATH 里Windows 上重开一个终端让环境变量生效。4.2 状态色错乱的三种典型情况颜色逻辑写错是另一个高频问题我自己就遇到过三次每次症状都不一样。第一种是边界元素变成了双色。因为我的判断是if...elif...elif的顺序如果i恰好等于min_idx那它会先命中当前最小值的条件永远不会走到本轮边界的分支。这在视觉上是可接受的但我后来把顺序调整成先判断idx i已排序再判断min_idx再判断j最后判断i逻辑层次更清楚。第二种是交换后颜色没更新。原因是我的帧快照里数组是拷贝的但渲染器用的还是旧数组。这种 bug 特别隐蔽因为画面看起来只差一帧很容易被当成正常现象。我的做法是在每次交换之后立刻生成一个新帧并且在这一帧里把min_idx重置成i让橙色标记回到边界位置视觉上就能看出交换完成了。第三种是最后一轮没画出来。如果外层循环写的是for i in range(n-1)最后一轮结束时数组其实已经排好但如果不额外 push 一帧排序完成动画会停在倒数第二个状态上看起来像没做完。我在build_frames末尾补了那一帧问题解决。4.3 常见问题速查表我把这一路遇到和收集到的问题整理成了一张表方便你对照排查症状可能原因排查方法解决方案中文标题变方块字体未设置或不支持换一台机器看是否复现设置font.sans-serif并加兜底字体GIF 只播一半帧间隔与 fps 不一致打印总帧数与实际播放数对比统一interval与fps的取值保存 MP4 报错系统缺少 ffmpeg终端执行ffmpeg -version装 ffmpeg 或用PillowWriter导出 GIF柱子颜色不对颜色判断顺序有误逐帧打印颜色列表按状态优先级重排判断顺序动画闪烁clear()后未重设范围观察坐标轴是否跳动每次绘制后重设ylim/xlim帧数过多卡顿数组规模太大统计帧列表长度把演示数组控制在 15 以内交换后画面不变快照未拷贝检查是否用了原数组引用用copy()或slice()做深拷贝动画结束后卡住未设置repeatFalse观察是否循环播放显式设置repeatFalse4.4 提升观感的几个小改法最后分享几个让动画从能看变成好看的小改动都是我反复试出来的。第一个是给柱子上加圆角。matplotlib 里可以用FancyBboxPatch或者直接在ax.bar里设linewidth和edgecolor虽然没有原生圆角但加一圈细描边就能让柱子看起来更有质感。Canvas 里用roundRect就能直接实现圆角效果更明显。第二个是在底部加一条状态条。就是把当前的i、min_idx、j值用一行小字打在画面底部形式是i2 min_idx3 j4。这行字对初学者特别友好因为他们可以一边看动画一边对照代码里的变量。第三个是给相等的元素加下标标注。前面讲过不稳定性的问题我在演示这个特性时会把柱子顶部的数值写成5(0)、5(1)这种格式观察者能清楚看到两个相同的值换了位置。这个改动看起来小但对理解稳定性这个概念帮助巨大。第四个是加一个步进模式。就是在交互版本里加一个按钮点一下走一帧。我自己学算法的时候最喜欢这个模式因为可以反复看某个具体的瞬间而不是被动地等动画播完。Canvas 版实现起来很简单就是手动调用render(frames[cursor])不需要requestAnimationFrame参与。5. 选择排序在真实场景里的位置写到这儿算法讲完了动画也做出来了。但我想再多说几句关于这个东西到底有什么用的实在话因为每次做完一个教学 demo总会有人问现实中谁会用选择排序。5.1 什么时候它真的有用先说结论在通用排序场景里选择排序几乎没有存在感。Python 内置的sorted()用的是一种混合排序策略Java 的Arrays.sort()对基本类型用双轴快排这些都是经过大量工程优化的方案选择排序在性能上完全没有竞争力。但它在几个特定场景里确实还有位置。第一个是元素交换成本极高的场合。选择排序的交换次数最多只有n-1次是所有常见排序算法里最少的之一。如果交换两个元素的操作非常昂贵比如涉及跨网络传输或者物理机械臂移动那么选择排序少交换的特性就有了价值。第二个是内存极度受限的嵌入式环境。选择排序只需要常数级额外空间代码也就十几行在资源紧张的设备上很合适。第三个是教学和面试。它的逻辑足够简单是理解双层循环 状态维护这个模式的绝佳载体我认识不少人就是从手写选择排序开始真正理解指针和边界控制的。还有一个小众但真实的用法当数组规模很小n 小于 15 左右时选择排序的实际表现和一些复杂算法差别并不大因为常数因子的差异在小规模下会被掩盖。有些混合排序算法在切分到小数组时就会退化成插入排序或选择排序这是工程上常见的优化手段。5.2 和其他排序算法同屏对比的设计如果你想把可视化做成一个完整的项目我建议做一个多算法对比模式把冒泡、插入、选择三种排序放在同一屏用同一份随机数据同时开跑看谁的柱子先排好。这个设计里选择排序的表现很有辨识度。冒泡排序的柱子在频繁交换画面一直很躁动插入排序的左半边缓慢地长起来动作集中在一侧而选择排序的橙色标记会一格格往右跳扫到底之后再啪地一下换位置每轮的节奏非常分明。三种截然不同的视觉风格并排放在一起观众对它们各自特征的记忆会深很多。实现上只需要把前面那个build_frames抽象成一个接口每个算法各自实现一份然后让三个 Canvas 共用同一个播放时钟。帧数不一致的时候短的那个播完就停在最终状态。这套东西我在本地做过一个粗糙版本效果比预期好尤其是给团队做分享的时候一屏就能讲完三种算法的差异。5.3 这个可视化还能往哪儿扩最后一个想聊的是延展方向。这套先记录帧、后渲染的思路其实可以套到几乎所有算法上。比如查找算法二分查找的每一步区间收缩都能做成动画而且比排序更简单只需要标记左右边界low、high和中间位置mid。再比如数据结构操作链表的插入删除、二叉树的遍历、图的广度优先和深度优先都可以用同样的帧快照模式来记录状态再用不同的渲染器画出来。如果往工程方向走还可以把帧数据导出成 JSON喂给前端图表库做成一个可以交互的网页版算法演示站点。数据结构和渲染彻底解耦之后换渲染器就像换一件衣服一样简单。我自己下一步打算做的是给这套东西加一个随机生成数据 手动输入数据的入口再配上一个帧进度条让它真正变成一个可以拿出去给别人用的教学小工具。我在实际操作中的体会是做算法可视化最花时间的从来不是绘图代码而是想清楚哪些状态是需要展示的。颜色、动画、圆角这些只是皮真正让一个演示有价值的是你有没有把算法内部的决策过程暴露出来。选择排序的min_idx就是这样一个关键状态把它画对了整个动画就立住了。后面无论你去做哪种算法的可视化先问自己一句这个算法的决策点在哪里剩下的都好办。
返回列表