用JavaScript实现排序算法动画【第一期】冒泡排序

算法概述

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。

它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成

主要参数

平均时间复杂度 最好情况 最坏情况 空间复杂度 排序方式 稳定性
O(n²)

O(n)

O(n²) O(1) In-place 稳定

演示动画

实现动画

HTML部分:这里只需要一个div作为容器即可,内部的节点我们会根据每个节点的值的大小来进行动态的创建。

<div id="warp"></div>

CSS部分:这里需要体检设置好样式,可分为3种情况,

  • 默认基础样式:节点的宽度,背景色,过度动画等

  • 比较时的样式:背景色变为红色

  • 位置确定后的样式:背景色为绿色,数值加粗并变为白色

#warp{
    position: relative;
    height: 200px;
    margin: 50px;
}
#warp div{
    position: absolute;
    width: 30px;
    display: inline-block;
    background: pink;
    text-align: center;
    bottom: 0;
    transition: all .2s;
}
#warp .active{
    background-color: #da4d66;
}
#warp .fixed{
    background-color: green;
    color: #fff;
    font-weight: 600;
}

万事俱备,只欠东风

这里的东风当然是JavaScript,整个动画过程看起来比较麻烦,在正式开始前要学会分析。

把整个动画分解为几个小的部分分别来实现,最后进行重组即可。

  1. 创建节点

这里对于每个节点可以抽象为一个节点类Dom,对于本例来说,该类需要高度、位置和dom节点属性,节点创建、添加样式、移除样式和移动位置4个方法。

  • 为了方便,这里每当实例化一个对象时就自动调用创建节点的方法

  • 每个方法最后都加上 return this ,这样做的目的是可以使用链式调用(虽然在本例中没有用到)

var warp = document.getElementById('warp')
let Dom = function(height, left){
    this.dom = null
    this.height = height
    this.left = left
    this.createDom()
}
Dom.prototype = {
    createDom: function(){     //创建节点
        var ele = document.createElement('div')
        ele.style.height = this.height + "%"
        ele.style.left = this.left
        ele.innerHTML = `<span>${this.height}</span>`
        warp.appendChild(ele)
        this.dom = ele
        return this  //返回this是为可以使用链式调用
    },
    addClass: function(name){      //添加样式类
        this.dom.classList.add(name)
        return this
    },
    removeClass: function(name){   //移除样式类
        this.dom.classList.remove(name)
        return this
    },
    move(position,pixel){   //移动位置
        this.dom.style[position] = pixel
        this.left = pixel
        return this
    }
}
//创建dom
var point = []   //保存所有的节点
for (let index = 0; index < array.length; index++) {
    let ele = new Dom(array[index],index*50 + 'px')
    point.push(ele)
}

   2. 为Array在原型上添加一个互换位置的方法

排序中必然会进行对数组中某两项进行互换位置的操作,这个操作起来虽然说很简单,只需要三行代码,但是当多处都需要用到时,整体的代码看起来就会更加的混乱,这里我们在Array在原型上添加一个互换位置的方法,交换位置后返回数组本身。

Array.prototype.arrExchange = function(p1, p2){
    let v =  this[p1]
    this[p1] = this[p2]
    this[p2] = v
    return this
}

    3. 封装其他操作函数

我在这封装了两个函数

  • 取消所有节点的 active 样式,为当前正在比较的两个节点添加 active 样式

compare(x,y){
    for (let l = 0; l < len; l++) {  //删除样式类;这里的len指的是数组长度
        point[l].removeClass('active')
    }
    point[x].addClass('active')
    point[y].addClass('active')
}
  • 交换两个节点的位置,这里不仅包括保存节点的数组中的节点位置,还有对界面中两个节点显示的位置进行视觉上的交换。冒泡排序是相邻两个节点进行交换,这里不需要进行复杂的计算,只需要对前面节点的left加50px,后面节点的left减50px即可。

changePosition(x,y){
    let left1 = parseInt(point[x].left.replace('px',''))
    let left2 = parseInt(point[y].left.replace('px',''))
    point[x].move('left',left1 + 50 + 'px')
    point[y].move('left',left2 - 50 + 'px')
    point = point.arrExchange(x, y)
}

    4. 冒泡排序

单纯的一个冒泡排序很简单,只需要两层for循环就可以了。但是要注意的是,要在界面以动画的形式展现这个过程,这里就需要用到延迟函数,然而难点就在于对于每一步的延迟时间的把控。

在for循环中如果直接使用 setTimeout 所看到的结果会与预期的不一样。

for (var i=1; i<=5; i++) {
    setTimeout(()=> {
        console.log( i );
    }, i*1000 );
}
// 6 6 6 6 6

这是因为setTimeout是异步执行,每一次for循环的时候,setTimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里,等待执行。只有主线上的任务执行完,才会执行任务队列里的任务。也就是说它会等到for循环全部运行完毕后,才会执行fun函数,但是当for循环结束后此时i的值已经变成了6,因此虽然定时器跑了5秒,控制台上的内容依然是6。

那么如何解决呢?

  • 使用闭包

  • 使用ES6中的let

  • 使用 setTimeout  的第三个参数

  • 拆分结构

    这里我就不对每种方案进行详细的讲解了,我这里使用的是let方案,先上代码,再对其中的几个点进行讲解。

let time = 0;
for (let j = 0; j < array.length - 1; j++) {
        setTimeout(() => {
            for (let i = 0; i < len - 1 -j; i++) {
                    setTimeout(() => {
                        operation.compare(i,i + 1)    //为当前比较的两个节点改变样式
                        if (array[i] > array[i + 1]) {   //如果前面的值大于后面的值则交换位置
                            array = array.arrExchange( i, i + 1)
                            setTimeout(() => {
                                operation.changePosition(i,i + 1)    //改变位置
                            }, 200);
                        }
                        setTimeout(() => {
                            if(i == len - 2 - j){
                                point[len - 1 - j].addClass('fixed')
                            }
                        }, 300);
                    }, i*500);
            }
        }, time);
    time += (len - j)*500
}

内层循环的终止条件为什么是 i < len - 1 -j ?

传统的冒泡排序内层终止的条件是 i < len - 1 ,每一轮排序都会确定一个节点的位置,某节点的位置确定后,在后面的排序中就没必要再次与其他节点进行比较。因此后面的每轮比较都可以比前面一轮少比较一个节点。

time 是做什么的?

与内层延迟不同,内层无论多少次循环,每次的循环间隔都为500ms。由于每个周期的内层循环节点数是不同的(每个周期递减一个元素),如:第一个周期10个节点(总时间5000ms),第二个周期9个节点(总时间4500ms),第三个周期8个节点(4000ms)......

那么对于外层来说,它的每个节点的延迟时间应为0ms,5000ms,5000+4500ms,5000+4500+4000ms,...... 。这里就用到了  time 来记录时间,time += (len - j)*500

博主网站: http://www.dzyong.top

关注公众号《前端筱园》,回复“算法动画”获取完整源码​

发布了81 篇原创文章 · 获赞 104 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/DengZY926/article/details/105045420