算法概述
冒泡排序(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,整个动画过程看起来比较麻烦,在正式开始前要学会分析。
把整个动画分解为几个小的部分分别来实现,最后进行重组即可。
-
创建节点
这里对于每个节点可以抽象为一个节点类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
关注公众号《前端筱园》,回复“算法动画”获取完整源码