有些人学算法的时候喜欢对着一行行冰冷的代码空想,结果半天过去,却是一点进展都没有。
有些人调试排序算法的时候, 会输出各种日志,排序的过程就是一行行日志堆出来的,这样又怎么能感受到算法 的乐趣呢?
下面就用最简单的冒泡算法举例,我们一起来感受算法可视化的乐趣。
希望看过本文后,当别人再提起排序算法时,首先出现在你脑海的将会是一个个可爱的游标 , 就像下面这样的图:)

1 柱状游标图
首先, 这是一个乱序数组:
let data = [4,1,8,5]用经典的柱状图(Bar)表示出来就是下面这样:(柱状图是用d3.js画的)

我们先试着给一个简单的for循环加上游标动画效果:
for (let i = 0; i > data.length; i++) {
// ...
}
动画是用anim.js实现,当然d3.js也是可以实现同样的动画,动画的具体实现不是本篇的重点,后面会专门更新文章讲解。
当两个数进行大小比较时,我会把他们同时标红,如下图,数字i和数字j比较大小:

基于上面的可视化游标图, 我们可以在冒泡排序身上试试效果。
2 两个冒泡算法
冒泡算法应该怎么实现呢?这里先给出两段冒泡排序算法的js实现代码,
到底哪个版本才是比较好的实现呢?同时,请留一下两个版本中变量 j 的边界差异。
冒泡案例A:
function bubbleSortA(data){
for (let i = data.length - 1; i >= 1; i--) {
for (let j = 0; j < i; j++) {
if (data[i] < data[j]) {
swap(data, i, j)
}
}
}
冒泡案例B:
function bubbleSortB(data){
for (let i = data.length - 1; i >= 1; i--) {
for (let j = 1; j <= i; j++) {
if (data[j] < data[j-1]) {
swap(data, j, j-1)
}
}
}
}如果不使用算法可视化的前提下,你是很难直接“感受”到他们之间的差异。比如:
关于 j 的边界问题:
案例A中 (let j = 0; j < i; j++) 和 案例B 的 (let j = 1; j <= i; j++) 有什么不同?
为什么案例A中是 j < i 而 案例B 则是 j <= i?
把案例B中的 let j = 1 改成 let j = 0 又会发生什么事情?
关于值的比较问题:
案例A中 data[ i ] < data[ j ] 和 案例B的 data[ j ] < data[ j-1 ] 有什么不同?
这些都可以从动画中找到答案。
3 感受算法的差异
我们直接通过看游标动画感受冒泡案例A和案例B的差异。一图胜千言!
冒泡案例A(左图)和冒泡案例B(右图)的游标动画效果对比:


案例A的动画伪代码:
function bubbleSortA(data){
for (let i = data.length - 1; i >= 1; i--) {
// anime move i
for (let j = 0; j < i; j++) {
// anime move j
// anime compare
if (data[i] < data[j]) {
swap(data, i, j)
// anime swap
}
}
}案例B的动画伪代码:
function bubbleSortB(data){
for (let i = data.length - 1; i >= 1; i--) {
// anime move i
for (let j = 1; j <= i; j++) {
// anime move j
// anime compare
if (data[j] < data[j-1]) {
swap(data, j, j-1)
// anime swap
}
}
}
}关于 j 的边界的差异:
案例A中是 j < i 而 案例B 则是 j <= i 的差异,请看案例A(左图)和 案例B(右图)j 第一次循环完毕的截图:


案例A中的 let j = 0 和 案例B let j = 1 的差异,请看案例A(左图)和 案例B(右图)j 的初始位置截图:


关于值的比较的差异:
案例A中 data[ i ] < data[ j ] (左图)和 案例B的 data[ j ] < data[ j-1 ] (右图)的第一帧动画截图:


4 冒泡算法B的优化
如果我们把排序数组换成有序数组
let data = [1, 2, 3, 4]冒泡案例B对有序数组的排序动画是下面这样的:

从动画中可以看到,已经排好序的数组,很多排序步骤是不需要执行的。
如果我们优化代码为案例B1,那么会发生什么情况呢?
冒泡案例B1:
function bubbleSortB1(data){
let isSwap = false
for (let i = data.length - 1; i >= 1; i--) {
for (let j = 1; j <= i; j++) {
if (data[j] < data[j-1]) {
swap(data, j, j-1)
isSwap = true
}
}
if (isSwap == false) break
}
}案例B1的动画效果就是下面这样, 很明显从动画上我们是看不到多余的排序步骤了;

5 冒泡算法A的优化
这里要注意的是, 案例A是不能像案例B这样优化的,为什么呢?
假如我们对这个乱序数组排序:
let data = [4,1,8,5]冒泡案例A1:
function bubbleSortA1(data){
let isSwap = false
for (let i = data.length - 1; i >= 1; i--) {
for (let j = 0; j < i; j++) {
if (data[i] < data[j]) {
swap(data, i, j)
isSwap = true
}
if (isSwap == false) break
}
}请看动画,元素还没有排序完成动画就结束了。

6 结尾
本文只是简单介绍了算法可视化的妙用,也是为接下来的算法可视化系列文章做的铺垫,后面笔者对“归并排序”和“快速排序”的可视化都是以柱状游标图为基础的。
Tips
本文的动画使用的是d3.js,如果对本文的动画代码感兴趣,请关注公众号【字节武装】。
可以反复观看本文所有案例的动画效果。

