ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

11种常见排序算法可视化详解:从冒泡到快排的Python动图实现

11种常见排序算法可视化详解:从冒泡到快排的Python动图实现 从冒泡到快排一个视频看完11种常见排序算法的可视化演示附Python动图实现源码提到“排序算法”很多人的第一反应是面试要考背就完了。但如果你真的背过冒泡、快排、堆排的代码就会发现自己处在一种很尴尬的状态——代码能默写出来面试官问“快排在最坏情况下为什么退化到O(n²)”“归并排序为什么稳定”“计数排序为什么不能排序小数”的时候又答不上来。这背后的原因很直接排序算法的核心不是代码而是数据元素在每一步如何移动。只记代码而不看过程等于看着地图背路线却从没走过那条路。可视化演示解决的就是这个问题。用一个动画把数组元素从无序逐步变成有序的过程放出来你会瞬间明白为什么冒泡排序“慢”为什么快速排序“快”为什么归并排序需要一个临时数组为什么计数排序可以做到线性时间。文章里我会先用对比表格把11种常见排序算法的分类和复杂度一次讲清楚然后逐个从可视化视角拆解每种算法的过程最后给出一个基于 Python Matplotlib 的完整可视化示例让你自己也能生成“从冒泡到快排”的动画。如果你准备面试、复习数据结构与算法或者想让团队里新人快速建立排序直觉这篇文章建议先收藏。1. 先澄清一个容易混淆的概念排序“冒泡”和事件“冒泡”在展开排序算法之前有必要先处理一个老生常谈的误区。很多人第一次搜“冒泡排序”结果搜到一堆“事件冒泡”“停止事件冒泡”的文章第一反应都是这俩是不是有什么关系可以很明确地说没有任何关系。事件冒泡Event Bubbling是浏览器里 DOM 事件传播机制的一种方向描述当点击一个子元素时事件会先经过捕获阶段到达目标元素再沿着 DOM 树向上逐层传播到父节点这个过程叫冒泡。它处理的是“谁响应了用户点击”的问题属于前端面试范畴。冒泡排序则是排序算法里最简单的一种每一轮从头到尾比较相邻的两个元素如果顺序不对就交换像气泡一样把当前最大的元素“顶”到数组末尾。它处理的是“数据怎么排成有序”的问题属于数据结构与算法范畴。这两个概念除了都叫“冒泡”外没有任何交集。理解这一点你才能在排错时快速定位问题看到“stopPropagation”应该去查 DOM 事件看到“swapped False”才是在写排序。2. 11种常见排序算法的全景图谱先看全貌再逐个击破排序算法很多但常见、常考的其实是有限的。这篇文章覆盖的11种分别是简单比较排序冒泡排序、鸡尾酒排序、选择排序、插入排序进阶比较排序希尔排序、归并排序、快速排序、堆排序非比较线性排序计数排序、桶排序、基数排序为什么是这11种因为它们基本覆盖了面试和工程里 90% 的场景也是学习“排序算法的时间复杂度”最好的样本集。先看一张全局对比表建议收藏排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性排序方式冒泡排序O(n²)O(n²)O(1)稳定比较鸡尾酒排序O(n²)O(n²)O(1)稳定比较选择排序O(n²)O(n²)O(1)不稳定比较插入排序O(n²)O(n²)O(1)稳定比较希尔排序O(n log² n)O(n²)O(1)不稳定比较归并排序O(n log n)O(n log n)O(n)稳定比较快速排序O(n log n)O(n²)O(log n)不稳定比较堆排序O(n log n)O(n log n)O(1)不稳定比较计数排序O(n k)O(n k)O(k)稳定非比较桶排序O(n k)O(n²)O(n)稳定非比较基数排序O(d × (n k))O(d × (n k))O(n k)稳定非比较这里的 k 和 d 分别表示计数排序中的取值范围、基数排序中的位数。这几列数据就是整个排序算法学习的“骨架”先了解哪些排序快、哪些排序稳、哪些排序吃内存再去看可视化的过程才有参照系。3. 11种排序算法的可视化视角逐个拆解我这里说的“可视化视角”不是把网上现成的动图搬过来而是从“如果我要自己画这个动画数据每一步会移动到哪里”的角度来理解。这样印象要深刻得多。3.1 冒泡排序理解“交换”的起点冒泡排序的可视化画面是所有算法里最容易想象的一个直方图里最高的柱子像气泡一样不断向右移动直到到达正确位置然后第二轮继续处理剩下的区间。核心操作相邻比较 交换。每一轮结束后最大的元素一定被“顶”到了当前区间的最后一位所以下一轮比较范围可以减少一个。这个算法在可视化里最能暴露的问题就是如果数组已经有序它依然会进行完整的双层循环。所以工程实现里通常会加一个swapped标志某一轮没有任何交换就直接结束。这也是冒泡排序可视化里最值得观察的点——数据越接近有序动画越快结束。3.2 鸡尾酒排序双向冒泡的改进思路鸡尾酒排序又叫双向冒泡排序是冒泡排序的改进版。它在每一轮里先从左到右把最大值送到末尾再从右到左把最小值送到开头像摇鸡尾酒一样来回摆动。可视化中最大的区别是指针的方向会周期性反转而不是一直单向扫描。这能让动画看起来更“对称”也能让某些特殊数组比如大部分元素已经有序的数据更快收敛。理解它意义不在于工程价值而在于建立“交换次数是能耗少交换一次就是优化一次”的思维。3.3 选择排序找最值放前面减少交换次数选择排序的过程很直观每一轮从未排序区间里找到最小的元素放到已排序区间的末尾。它的交换次数永远只有 n-1 次比冒泡少很多但比较次数依然是 O(n²)。从可视化角度看选择排序最有意思的地方在于动画的大部分时间都不是在“交换”而是在“寻找”——一个扫描指针不断移动每遇到更小值就更新最小值下标最后才做一次交换。看动态图你会发现柱子高度变化不像冒泡那么频繁但每一步比较都在发生。这也解释了为什么选择排序是不稳定的比如数组 [5, 8, 5, 2]第一轮会找到 2 并和第一个 5 交换结果两个 5 的相对顺序就变了。稳定性是一个容易被忽略、但在可视化里很容易失去观察的维度。3.4 插入排序像整理扑克牌一样插入排序对大多数人来说是最符合直觉的就像打牌时整理手牌你拿起一张新牌找到它在前面的位置然后把后面的牌依次往后挪。可视化里插入排序的典型画面是数组左侧逐渐有序右侧保持原样每次“抽”一个元素出来通过已排序区域的元素逐个后移找到插入点再把元素放进去。插入排序“工程价值”很高。虽然平均复杂度是 O(n²)但它在数组基本有序时接近 O(n)而且属于稳定排序、原地排序因此是很多高级排序算法比如 TimSort在数据量小或近乎有序时的兜底排序。看它的可视化你会发现数据越有序动画中的“移动”越少这也正是插入排序在工程中备受青睐的原因。3.5 希尔排序让数据“大步跳”起来希尔排序是插入排序的升级版。它先把数组按下标的一定增量分组对每组使用插入排序随着增量逐渐减小整个数组越来越接近有序当增量为 1 时其实就是一次完整的插入排序。从可视化角度看希尔排序和插入排序最大的区别是“步长”插入排序每次都和前一个元素比较动画里元素是一格一格挪动的希尔排序初期的元素会跨越很长距离移动动画看起来像在大步跳跃。这本质上是让小元素更快地跳到数组前面而不是一次只挪一步。如果只记理论很容易把希尔排序当成“分了组的插入排序”但看过可视化后它带来的“跳跃感”会帮你形成印象优化的本质是减少逆序对的数量而不是减少比较这一动作本身。3.6 快速排序选基准、分左右、递归处理快速排序是面试中出现频率最高、也是理解门槛最高的排序算法之一。核心逻辑四句话选一个基准值pivot把小于基准的元素放左边大于基准的元素放右边然后对左右两个子区间递归执行同样操作。可视化里快速排序的画面非常有辨识度一个元素被标记为基准色然后其他元素围绕它不断交换位置最终基准元素停在某个位置左边全比它小右边全比它大。接下来左右两侧分别重复这个过程动画呈现出“分区域、逐层细化”的效果。真正容易踩坑的地方是基准值的选择。如果每次都选第一个或最后一个元素而数据本身已经有序快速排序会退化到 O(n²)。工程里更常用的做法是三数取中法取区间首、中、尾三个位置的中位数作为基准。这一点面试官很喜欢追问也是可视化里最值得做实验的点——把 pivot 改成固定取首元素再输入有序数组动画的运行时间会肉眼可见地变长。3.7 堆排序用完全二叉树做排序堆排序是理解“堆”这个数据结构的敲门砖。它先把数组构建成一个大顶堆最大值在堆顶然后反复把堆顶和堆末交换缩小堆的范围再重新调整堆结构。从可视化来看堆排序没有快排那种“分治”的整齐感它更像是“选最大、放末尾、再调整”的循环。堆的调整过程在数组索引上表现为父节点和子节点之前的跳跃式交换动画上的直观感受是没有冒泡那么密、也没有快排那么规则但每一轮都能稳定地把一个最大值送到数组末尾。这里想提醒一点堆排序常被误认为是“稳定的堆调整”实际上它是不稳定排序而且因为缓存命中率不高工程里实际表现往往不如快速排序和归并排序。但它有一个无法替代的优势最坏时间复杂度也是 O(n log n)不会像快排一样退化。3.8 计数排序用数组下标解决排序计数排序是第一种非比较排序。它不比较元素之间的大小而是统计每个值出现的次数再按照值的大小顺序把元素“放回去”。可视化里计数排序的画面非常简单先是统计阶段所有柱子按值映射到一个频率数组形成频率分布图然后重建阶段按照统计结果把元素依次填入原数组。这里最需要注意的是适用范围计数排序要求数据是非负整数并且取值范围 k 不能太大否则空间复杂度 O(k) 会失控。举个例子给 10 个数排序但它们的范围是 0 到 1 亿常规计数排序会直接开一个 1 亿大小的数组这在工程里是完全不可接受的。可视化演示可以让你很直观地看到“频率数组的长度”和“数据范围”之间的线性关系。3.9 桶排序把数据分桶再分别排序桶排序可以看成计数排序的泛化版本。它根据某种映射函数把元素分到若干个桶里每个桶内部用插入排序或其他排序算法处理最后按桶顺序输出。可视化视角下桶排序的最大特点是“分而治之”元素先按区间落入不同桶在直方图上看起来像被隔成了几个小组组内再做小范围排序。和计数排序的区别是桶排序不要求整数但对映射函数的选择很敏感如果数据分布非常不均匀大部分元素落到少数桶里桶内排序的成本会上升甚至退化成 O(n²)。3.10 基数排序按位排序低位到高位基数排序是一种完全不同的思路它不进比较而是把整数按位数拆开先按个位排序再按十位排序以此类推。每一轮都要求使用稳定排序典型实现是计数排序作为辅助。可视化里基数排序的动画非常有节奏感同一组数据会被反复排序若干轮每一轮只关注某一位的数字。你会看到柱子按“个位”不断变化顺序然后按“十位”再变一遍。用图形理解时很形象数组像是被一次次“洗牌”但每一次洗牌都保留上一轮的相对顺序最终实现整体有序。需要强调的是基数排序的复杂度是 O(d × (n k))其中 d 是数字的位数k 是每一位的取值范围。它不是任何时候都快在处理位数很小的整数时优势明显但处理浮点数或者字符串时要额外设计映射方案。3.11 把 11 种算法串成一条学习路径把上面11种算法连起来看学习路径其实非常清晰先看冒泡和选择理解“比较 交换”的基础模型再看插入排序理解数据局部有序时的优化空间由插入排序扩展出希尔排序理解“大步长分组”的思想由分治思想引出快速排序和归并排序奠定 O(n log n) 级别的核心理解由二叉树思想引出堆排序掌握另一条 O(n log n) 的路线最后跳出“比较”框架理解计数排序、桶排序、基数排序如何利用数据本身的结构换取速度。这个顺序也正好是“从冒泡到快排”这条主线的升级过程从最直观的开始逐步引入更抽象、更高效、更工程化的思路。4. 可视化演示的技术选型与环境准备理解了算法原理之后动手做一个自己的可视化演示会让整个学习过程落地。这里推荐两种常见技术方案你先根据自己的场景选择方案语言/技术优点缺点Matplotlib FuncAnimationPython代码简单、易修改、可输出动图数据量大时性能一般HTML Canvas / requestAnimationFrameJavaScript浏览器直接运行、交互性好、动画流畅算法需用 JS 重写ManimPython视频质量高、适合教学讲解学习曲线和使用成本较高CSDN 读者中最容易跑通的是 Python Matplotlib 方案。它不需要额外准备前端工程一个.py文件就能完成。运行一个可视化排序动画核心需要三样东西Python 3.7 及以上版本版本请以实际环境为准本文重点演示通用实现思路matplotlib 库numpy 库可用可不用但用起来更方便。安装命令很简单pip install matplotlib numpy如果环境里已经安装过建议检查版本不低于当前主流稳定版本pip show matplotlib numpy在使用 Matplotlib 保存 GIF 时需要 Pillow 作为动图写入器建议一并安装pip install pillow5. 手写一个排序可视化演示从冒泡到快排的核心代码可视化排序动画的技术方案并不复杂算法每执行一步交换或赋值就把当前数组状态“抛”出来动画函数拿到状态后更新柱状图的高度。难点在于如何让“算法运行”和“画面刷新”解耦。这里最优雅方式是用 Python 生成器yield把每一步状态暴露出来。下面我会给出一个完整可运行的示例覆盖数据生成、冒泡排序生成器、快速排序生成器和动画装配几个部分。5.1 数据生成与画布初始化# 文件路径sort_visualizer.py import random import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation # 生成 50 个 1 到 100 之间的整数 data list(range(1, 101)) random.shuffle(data) data data[:50] # 创建画布 fig, ax plt.subplots(figsize(10, 5)) bar_rects ax.bar(range(len(data)), data, colorskyblue) ax.set_title(Sorting Algorithm Visualization) ax.set_ylim(0, max(data) * 1.1) ax.set_xlim(-1, len(data))这里的关键点是数据是从 1 到 100 随机打乱后取前 50 个保证柱状图高度分布比较均匀ax.bar返回一个BarContainer后续每一帧通过更新容器内每个矩形的高度来完成动画纵轴范围设置为最大值的 1.1 倍避免最高柱子顶到画布边界。5.2 冒泡排序的生成器版本def bubble_sort_gen(arr): 冒泡排序每交换一次就 yield 一次当前数组状态 a arr[:] n len(a) for i in range(n - 1): swapped False for j in range(n - 1 - i): if a[j] a[j 1]: a[j], a[j 1] a[j 1], a[j] swapped True yield a.copy() # 如果本轮没有交换说明已经有序提前结束 if not swapped: break注意几点yield a.copy()很关键。如果直接yield a动画帧持有的还是同一个列表引用后面算法继续修改数据时前面已经渲染的帧也会受到影响动画会全部乱掉。这里加上了swapped优化数据有序时可以提前退出不只是教学演示真实工程里也值得使用。5.3 快速排序的生成器版本def quick_sort_gen(arr, low0, highNone): 快速排序使用生成器在每次交换和划分后返回当前数组状态 if high is None: high len(arr) - 1 if low high: return # 三数取中取 low、mid、high 三个位置的中位数作为基准 mid (low high) // 2 candidates [(arr[low], low), (arr[mid], mid), (arr[high], high)] pivot_val sorted(candidates)[1][0] pivot_idx sorted(candidates)[1][1] arr[low], arr[pivot_idx] arr[pivot_idx], arr[low] pivot arr[low] i low 1 j high while True: while i j and arr[i] pivot: i 1 while i j and arr[j] pivot: j - 1 if i j: break arr[i], arr[j] arr[j], arr[i] yield arr.copy() arr[low], arr[j] arr[j], arr[low] yield arr.copy() # 递归处理左右子区间 yield from quick_sort_gen(arr, low, j - 1) yield from quick_sort_gen(arr, j 1, high)这段代码相对复杂需要说明几个设计点快排的递归结构天然适合生成器Python 的yield from可以把递归子过程的帧逐个“透传”给外层使用三数取中法选取基准目的是降低输入已经有序时快排退化到 O(n²) 的概率arr[low], arr[j] arr[j], arr[low]是分区完成后的放置基准操作这一步需要单独yield一次否则画面会跳过基准归位的瞬间。5.4 动画装配与运行def update(frame_data, bar_rectsbar_rects): 动画回调函数把算法生成器输出的数组映射到柱子高度 for rect, h in zip(bar_rects, frame_data): rect.set_height(h) return bar_rects # 选择一个算法生成器这里以冒泡排序为例 generator bubble_sort_gen(data) # 如果换成快速排序取消注释下一行 # generator quick_sort_gen(data) anim FuncAnimation( fig, update, framesgenerator, repeatFalse, interval20, # 每帧间隔毫秒数 blitFalse ) plt.show()运行上面的脚本会弹出一个实时演示窗口。你可以观察到冒泡排序动画中最大的值像气泡一样逐步“冒”到右侧快速排序动画中基准元素会先被选出来随后数组以基准为界分成左右两半动画下方的时间间隔可以直接调节interval参数数值越小跑得越快。根据我的经验第一次跑通这个可视化后再去回看算法原理理解深度是完全不同的你不再需要通过代码脑补数据移动而是直接看到了结果。5.5 保存为 GIF方便复盘和分享动画窗口关掉后如果你想保存成 GIF 放进自己的 CSDN 文章或笔记里只需要在plt.show()之后加一句anim.save(sort_animation.gif, writerpillow, dpi80)把运行代码改一下anim.save(bubble_sort.gif, writerpillow, dpi80)这里建议使用pillow而不是默认的ffmpeg作为写入器原因有二Pillow 不需要额外安装编解码器对大多数开发者来说更省事保存 GIF 时 Pillow 的兼容性更好不容易出现编码器找不到的问题。6. 运行结果与效果验证如果你上面的代码全部照抄运行后会得到以下几种结果可以根据效果判断是否成功。6.1 成功画面窗口弹出50 根柱子随机分布高度和数值成正比动画开始后柱子的高度逐步变化最终所有柱子从左到右严格递增冒泡排序动画结束时右侧变成最高的一根柱子最左侧是最矮的一根快速排序动画结束时整体有序并且整个过程看起来像是不断在“切分区间”。6.2 如何确定排序逻辑正确排序是否成功有两个判断标准最终柱状图是否从左到右严格递增生成器结束后原数据的副本是否和sorted(data)完全一致。可以在代码最后加一行验证final_state list(generator) sorted_data final_state[-1] assert sorted_data sorted(data), 排序结果有误 print(排序验证通过最终结果为, sorted_data[:10], ...)如果验证失败说明某个排序算法的实现有 bug优先检查交换逻辑和边界条件。6.3 性能表现与调整建议数据规模 50 以内时动画运行非常流畅interval20毫秒足够如果把数据量提高到 200动画帧增多Matplotlib 刷新率会明显下降此时建议调大interval到 50 或 80 毫秒如果你有 500 以上粒度的数据量建议改用 Canvas 方案或者将柱状图换成点状图避免画面卡顿。从实践角度看学习用动画不需要追求大数据量50 到 100 个元素足够看到每一种算法执行的分寸感真正需要大数据量时直接分析代码时间复杂度更合理可视化更适合教学认知不适合做性能压测。7. 常见问题与排查方法自己动手写可视化时遇到问题是很常见的。这里我把最常见的几个问题整理成表方便直接检索问题现象可能原因排查方式解决方案动画窗口弹出后立即卡死生成器返回的是列表引用而不是副本检查生成器是否使用.copy()在yield处返回a.copy()保存 GIF 时报错找不到编码器系统没装 ffmpeg 或 Pillow 不当确认pillow已安装使用writerpillow保存动画刷新闪烁严重blitTrue但更新逻辑不完整检查 update 函数返回值设置blitFalse或正确返回所有更新对象柱子高度不变但排序已完成生成器一次性执行完没有按帧产出检查生成器里是否漏写yield在每次交换后添加yield中文标题显示为方块系统缺少中文字体或 Matplotlib 默认字体不支持查看系统字体列表增加中文字体设置或先把标题写成英文窗口关闭后anim被 GC 回收代码中动画对象被内存回收确认anim是全局变量保持anim引用不释放如果动画一次性把整个排序过程全部跑完了而你想慢慢观察每一步可以再加一个控制按钮或把interval设得更大。一个实用的做法是先把interval设为 100 毫秒跑一遍冒泡排序感受每轮比较和交换的节奏。8. 最佳实践与工程建议可视化演示适合“看懂原理”但真实业务里写排序逻辑需要注意的东西远远超过算法本身。我这里结合工程经验给出几点建议都是自己踩过的坑。8.1 实际项目优先用语言内置排序无论你是写 Java、Python 还是 Go第一选择永远是语言或标准库提供的排序而不是自己再写一遍快排或归并。Python 的sorted、Java 的Collections.sort、Go 的sort.Slice都经过充分优化并且会根据数据特点自动选择排序策略。自己实现排序算法的主要场景是面试、教学、或者内置排序无法满足需求比如需要自定义稳定性语义或特殊比较器。8.2 理解时间复杂度不能只看平均值很多人背快排“平均 O(n log n)”却忽略了最坏情况是 O(n²)。可视化实验可以很直观地验证这一点如果用固定取首元素的基准策略去排序一个已经有序的数组动画跑完所需的时间会显著变长。真实系统里的数据往往存在某种局部有序性如果不考虑这一点快速排序可能比插入排序还慢。从工程角度看更稳妥的思路是数据量小用插入排序数据量大但要稳定用归并排序数据量大且不要求稳定优先用内置排序或快排。切忌只看一张复杂度表格就盲目选择。8.3 注意“稳定性”在工程里的真实价值稳定性是什么意思简单说如果数组里有两个相同的值在排序之后它们的相对位置不改变这个排序就是稳定的。为什么这在工程里重要因为实际业务经常是多字段排序。比如表格先按“组别”排序再按“金额”排序。如果排序算法是不稳定的第二次排序可能会打乱第一次的组内顺序如果稳定第二次排序在金额相同时仍能保持第一次的组别顺序。所以有时候“稳定”比“快”更关键。8.4 学习排序算法要和排查 bug 一样有耐心我自己带新人的时候发现最容易出错的并不是算法本身的思维而是边界条件。比如快速排序分区的左右指针相遇条件、归并排序的临时数组合并边界、堆排序的下标从 0 开始的父子关系。看可视化动画会缓解这个问题因为它让你看到数据真实落位而不是盯着 debug 输出空想。建议练习顺序是先用可视化动画把每种算法观察一遍关掉动画手写代码用随机数组和有序数组测一遍再打开动画对比自己写的版本和动画过程是否一致最后再去看复杂度和稳定性推导。这个过程比单纯刷题高效得多。8.5 可视化代码本身也可以模块化如果你想把 11 种排序算法都做成可切换的演示不要一个算法写一个脚本。把生成器函数统一设计为“接收数组、逐步产出数组状态”的接口然后通过一个字典把算法名字和函数映射起来ALGORITHMS { bubble: bubble_sort_gen, quick: quick_sort_gen, # 其他算法依次扩展 } def run_animation(algorithm_name, data): generator ALGORITHMS[algorithm_name](data) anim FuncAnimation(fig, update, framesgenerator, repeatFalse, interval20) plt.show()这样每新增一个排序算法只需要实现一个生成器函数再注册到字典里即可。后面无论你是想扩展成 11 种还是 20 种改动成本都很低。9. 总结与后续学习方向排序算法的可视化演示真正解决的问题不只是“看懂动画”而是帮你建立对数据移动过程的直觉。当你看到冒泡排序的“大数上浮”、快排的“区间切分”、归并排序的“合并有序段”、堆排序的“堆顶下沉”时这些算法就不再是一段需要背诵的代码而是一幅幅会在脑子里自动播放的过程图。这篇文章里我把 11 种常见排序算法按比较排序和非比较排序做了完整拆解并给出了基于 Python Matplotlib 的可视化示例代码。你可以直接运行也可以把这段代码扩展成自己的排序动画工具。值得深入的方向还有三个一是在可视化里加入“着色高亮”比如标记基准值和已排序区间让动画更有层次感二是把算法性能对比用曲线图做出来观察不同数据规模下的运行时间差异三是走向工程化研究 TimSort 和 Dual-Pivot QuickSort 这些生产级排序实现为什么更快。如果你正在复习数据结构与排序算法建议把文章里的生成器模板保存下来每天跑一种算法边跑边对照本章里的复杂度表格。想看快排最坏情况时把三数取中改成固定取最右元素然后输入一个有序数组你会很直观地理解退化是什么样。建议收藏备用遇到需要向别人解释排序过程时把这个动画工具拿出来效率比你讲一百句话都高。
返回列表