用JavaScript实现排序算法动画【第二期】插入排序

算法概述

插入排序(Insertion sort)是一种简单直观且稳定的排序算法。

将数组的第一个数认为是有序数组,从后往前(从前往后)扫描该有序数组,把数组中其余n-1个数,根据数值的大小,插入到有序数组中,直至数组中的所有数有序排列为止。这样的话,n个元素需要进行n-1趟排序!

主要参数

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

O(n)

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

演示动画

实现动画

本期的插入排序动画相对于上期的冒泡排序来说,难度增加了很多,也使用到了前端中更多的知识点,动画只是结果,我们的目的是在动画的实现过程中学到更多的知识,以及解决问题的能力。

本期重点:节点数组与排序数组、递归思想、async、await、promise

在阅读下面的内容前,建议先看一下上期内容,本期只会不同的地方进行分析讲解:用JavaScript实现排序算法动画【第一期】冒泡排序

CSS改动:

#warp .active{  /*比较对象*/
    background-color: #da4d66;
}
#warp .compare{  /*被比较对象*/
    background-color: green;
}

公共JS改动:

1.节点位置移动方法更加灵活,接收方向和像素大小两个参数

move(position,pixel){   //移动位置
    this.dom.style[position] = pixel
    this[position] = pixel
    return this
}

2.Dom加入了bottom属性,节点的移动不止是左右了

let Dom = function(height, left){
    this.dom = null
    this.height = height
    this.bottom = 0
    this.left = left
    this.createDom()
}

3.公共操作类函数也变得更加灵活,都以参数的形式传入,不再是固定值

let operation = {
    //改变节点样式
    changeStyle(n, name){
        for (let l = 0; l < len; l++) {  //删除样式类
            point[l].removeClass(name)
        }
        point[n].addClass(name)
    },
    //移动位置
    changePosition(n,positon, px){
        let dis = parseInt(point[n][positon].replace('px',''))
        point[n].move(positon,dis + px + 'px')
    },
}

重点来啦

我们先来看一下传统的插入排序过程

for (let i = 1; i < len; i++) {
    temp = array[i]
    let j = i - 1
    while(array[j] > temp && j >= 0){
        array[ j + 1] = array[j]
        j--
    }
    array[j + 1] = temp   
}

看起来代码量很少,也很容易理解。要在这个基础上加上延迟来实现动画过程可不是件容易的事。与上期的魔炮排序有什么不同呢?

  • 冒泡排序永远是相邻两个节点进行交换位置

  • 冒泡排序我们可以知道确定的每次内外层所需要的交换次数,以此可以来准确的设置延迟时间,但是插入排序的内层循环是无法确定要执行多少次的,也就无法确定下一次外层循环应该开始的时间。

  • 插入排序的动画过程更加复杂,需要先将待插入节点下移,然后逐个比较,找到其应该在的位置后,再上移插入。

对于上面的问题我们逐步的来使用相应的方案解决。

1. 如何确定下轮外层循环开始的时间,下轮开始的时间是由本轮内层循环交换比较次数决定。这里我使用的是 promise + 递归 的思想。创建一个函数 insertArray ,函数内有一个promise对象,这个函数就可以看作是外层的for循环,在promise中进行内层循环。

function insertArray(i){
    let promise = new Promise((resolve) =>{
        temp = array[i]
        let j = i - 1
        let time = 0
        for (; array[j] > temp && j >= 0;j--) {
            let delay = (i - j - 1)*500
            time = delay + 1000
        }
        setTimeout(() => {
            array[j + 1] = temp
            resolve()
        }, time);
    }).then(() => {
        if(n < len)
            this.insertArray(n++)
    })
}

这里的time就是用来计算延迟时间的,是它的值在内层循环中更新,始终等于最后一个交换的延迟时间,为什么要加上1000ms呢,这是因为最后一次交换和插入节点也需要一段时间。等待time时长后进行then中,来判断是否继续下一轮外层循环。

2. 现在在内层循环中就需要进行节点的位置移动,那是不是和冒泡排序一样,只需要在for中使用setTimeout就可以了呢?当然不是,这里又涉及到了setTimeout异步执行的问题,上期中我们使用let可以解决,但是细心的朋友可以发现,这里初始化let j 是在for 的外部,因为在后面还需要使用到 j ,所以这里就无法使用原来的方法了。

这次使用拆分结构的方式,即把内部延时函数独立出来。

function compareDom(i, j, delay){
    setTimeout(() => {
        operation.changePosition(j, 'left', 50) 
        operation.changeStyle(j, 'compare')
        operation.changePosition(i, 'left', -50)
        operation.changeStyle(i, 'active')
        array[ j + 1] = array[j]
    }, delay);
}
----------------------------------------
for (; array[j] > temp && j >= 0;j--) {
    let delay = (i - j - 1)*500
    compareDom(i, j, delay)
    time = delay + 1000
}

3. 在开始比较前,需要将待插入对象进行下移,这个过程也需要时间,并且在内层循环的前面。这里我使用的是async与await。封装了一个等待函数。如果没有这一步你会看到动画是这样的。

function waitTime(time){   //等待函数
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve()
        }, time);
    } )
}
function insertArray(i){
    let promise = new Promise(async (resolve) =>{
        temp = array[i]
        let j = i - 1
        operation.changeStyle(i, 'active')
        operation.changeStyle(i - 1, 'compare')
        point[i].move('bottom','-200px')
        await waitTime(1000)
        let time = 0
        for (; array[j] > temp && j >= 0;j--) {
            let delay = (i - j - 1)*500
            compareDom(i, j, delay)
            time = delay + 1000
        }
        setTimeout(() => {
            point[i].move('bottom','0')
            array[j + 1] = temp
            resolve()
        }, time);
    }).then(() => {
        if(n < len)
            this.insertArray(n++)
    })
}

4. 最后一点也是最重要的一点,从头到尾其实有两个数组,一个是进行排序的数组,另一个是保存dom节点的数组。上面做进行的所有操作都是在对排序数组改变,虽然视觉上节点的位置在发生变化,但是这只是对每个节点的属性在改变,他们在节点数组中的顺序并没有变化。因此在每轮动画结束后必须要调整实际的节点数组顺序与排序数组中的顺序一致,否则会影响到下一轮的排序。

完整的插入排序代码如下,这里我就不再对每个变量为什么这么取值进行讲解了,其中还设有很多小的细节处理,他们也都非常重要,起着关键性的作用。需要自己去实际操作来领悟。

function insertArray(i){
    let promise = new Promise(async (resolve) =>{
        temp = array[i]
        let j = i - 1
        operation.changeStyle(i, 'active')
        operation.changeStyle(i - 1, 'compare')
        point[i].move('bottom','-200px')
        await waitTime(1000)
        let time = 0
        for (; array[j] > temp && j >= 0;j--) {
            let delay = (i - j - 1)*500
            compareDom(i, j, delay)
            time = delay + 1000
        }
        setTimeout(() => {
            point[i].move('bottom','0')
            for (let p = i; p > j + 1; p--) { //动画完成后再进行dom数组的位置重排
                point.arrExchange(p,p-1)   
            }
            array[j + 1] = temp
            resolve()
        }, time);
    }).then(() => {
        if(n < len)
            this.insertArray(n++)
        else
        for (let l = 0; l < len; l++) {  //删除样式
            point[l].removeClass('active')
            point[l].removeClass('compare')
        }
    })
​

最后调用函数,初始值为1。

insertArray(1)

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

当然你也可以预览下面这个链接

https://www.dzyong.com/JavaScript/sortAnimation/insertSort.html

通过查看源代码来获取源码。

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

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

猜你喜欢

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