用游标图感受冒泡排序

279 阅读5分钟
有些人学算法的时候喜欢对着一行行冰冷的代码空想,结果半天过去,却是一点进展都没有。

有些人调试排序算法的时候, 会输出各种日志,排序的过程就是一行行日志堆出来的,这样又怎么能感受到算法 的乐趣呢?


下面就用最简单的冒泡算法举例,我们一起来感受算法可视化的乐趣。

希望看过本文后,当别人再提起排序算法时,首先出现在你脑海的将会是一个个可爱的游标 , 就像下面这样的图:)


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,如果对本文的动画代码感兴趣,请关注公众号【字节武装】。

可以反复观看本文所有案例的动画效果。