排序算法可视化 - 冒泡排序

1,258 阅读4分钟

1. 前言

以前学习各种排序算法的时候,理解书本上晦涩的排序算法思想有点痛苦,直到在网上看到这个算法可视化的网站,发现通过视觉看到排序算法的过程,可以帮助事半功倍的理解各种排序算法的思想。而且网上没有搜到什么文章讲使用前端技术算法可视化的,所以就写了这篇文章供大家参考。

2. 项目概述

该项目旨在利用前端技术,使排序算法的过程可视化,从而达到更轻松的理解各个排序算法的基本思想。本文重在使用前端技术展示冒泡排序过程,故冒泡排序的思想不是重点。

项目实现效果如图:

所用技术:

  • JavaScript, vanilla JS
  • React
  • CSS(animation)

仓库地址: github.com/Neo-peng/vi…

3. 可视化冒泡排序算法

项目关键步骤解释

3.1 生成随机数组

生成10-20个随机数,用来展示排序过程

// 生成的随机数组的长度
this.lenOfRandomArray = 10 + Math.ceil(Math.random() * 10)  
this.randomArray = []
for (let i = 0; i < this.lenOfRandomArray; i++) {
  // 生成lenOfRandomArray个[1, 50] 的随机数,并放入randomArray中
  this.randomArray.push(Math.floor(Math.random() * 50 + 1)) 
}

3.2 随机数组组件

该组件仅用来将js数组渲染成html。需要注意的是:

  1. 使用ref从而可以使该组件以后可以在父组件中使用。
  2. div的高度,由随机数的大小决定 height: randomNum * 4 + 'px'
import React from 'react'

export const RandomArray = React.forwardRef((props, ref) => {
  return (
    <div ref={ref} className='items'>
      {props.randomArray.map((num, index) => {
        return (
          <div key={index} style={{ height: num * 4 + 'px' }} className='item'>
            <p>{num}</p>
          </div>
        )})}
    </div>
  )}
)

3.3 排序展示过程

思路:

  1. 使用js为需要变换位置、添加背景颜色等html元素添加class
  2. 通过css选取class名,展示排序过程

难点:

  1. js添加className的同时,需要用到异步操作(因为展示排序过程需要时间)
  2. 如何抽象组件使最大化可复用性(这一块有待进一步优化提高,因为之后大概还要写其他排序算法,比如快速排序、归并排序等,所以抽象出来可复用的部分,可以大大减少之后的代码量,当然代码也会更加整洁)

3.3.1 js部分

import React, { Component } from 'react'
import { RandomArray } from './RandomArray'

export default class BubbleSort extends Component {
  constructor() {
    super();
    this.ref = React.createRef()
    console.log('bubble sort')
    this.swap.bind(this)
    // 生成用来排序的随机数组
    this.lenOfRandomArray = 10 + Math.ceil(Math.random() * 10)  // 随机生成的随机数组的长度
    this.randomArray = []
    for (let i = 0; i < this.lenOfRandomArray; i++) {
      this.randomArray.push(Math.floor(Math.random() * 50 + 1)) // 生成lenOfRandomArray个[1, 50] 的随机数,并放入randomArray中
    }
    // console.log(this.randomArray)
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

  async swap(leftIndex, rightIndex) {
    /**
     * 当rightIndex位置的数值大于leftIndex的数值,
     * 交换其位置,通过添加删除class的方式控制动画效果
     */
    const children = this.ref.current.children
    children[leftIndex].classList.add('move-right') // 大的div向右移动
    children[rightIndex].classList.add('move-left') // 小的div向左移动
    const tmp = this.randomArray[leftIndex]
    // 交换数组中左右的值
    this.randomArray[leftIndex] = this.randomArray[rightIndex]
    this.randomArray[rightIndex] = tmp
    setTimeout(() => {
      // 交换html中div的位置
      this.ref.current.insertBefore(children[rightIndex], children[leftIndex])
      children[leftIndex].classList.remove('move-left')
      children[rightIndex].classList.remove('move-right')
      leftIndex += 1
      rightIndex += 1
    }, 800)
  }

  async checking(leftIndex, rightIndex, ms) {
    if (rightIndex >= this.lenOfRandomArray) {
      return
    }
    this.ref.current.children[leftIndex].classList.add('checking')
    this.ref.current.children[rightIndex].classList.add('checking')
    await this.sleep(ms)
    this.ref.current.children[leftIndex].classList.remove('checking')
    this.ref.current.children[rightIndex].classList.remove('checking')
  }

  async componentDidMount() {
    let leftIndex = 0
    let rightIndex = 1
    let indexOfLastSortedNum = this.lenOfRandomArray

    while (rightIndex < indexOfLastSortedNum) {
      // 左侧的值大于右侧的值,需要交换
      if (this.randomArray[leftIndex] > this.randomArray[rightIndex]) {
        this.swap(leftIndex, rightIndex)
        // console.log(`swap ${leftIndex} and ${rightIndex}`)
        await this.sleep(800)
        if (rightIndex + 1 < this.lenOfRandomArray && this.randomArray[rightIndex] > this.randomArray[rightIndex + 1]) {
          this.checking(rightIndex, rightIndex + 1, 300)
          await this.sleep(300)
        }
      } else {
        this.checking(leftIndex, rightIndex, 800)
        await this.sleep(800)
      }
      if (rightIndex + 1 === indexOfLastSortedNum) {
        this.ref.current.children[rightIndex].classList.add('done')
        leftIndex = 0
        rightIndex = 1
        indexOfLastSortedNum -= 1
        continue
      }
      leftIndex += 1
      rightIndex += 1
    }
    this.ref.current.children[0].classList.add('done')
    // console.log(this.randomArray)
  }

  render() {
    return (
      <RandomArray ref={this.ref} randomArray={this.randomArray}></RandomArray>
    )
  }
}

3.3.2 css部分

p {
  /* 使p元素在div中水平居中 */
  position: absolute; 
  bottom: 0;
  left: 50%;
  transform: translate(-50%, 0);
  padding: 0;
  margin: auto;
}

.items {
  /* 使子div下端对齐 */
  display: flex;
  align-items: flex-end;
}

.item {
  justify-content: center;
  position: relative;
  width: 35px;
  background: rgb(158, 207, 224);
  margin: 5px;
}

.move-right{
  /* 控制元素向右滑动 */
  background-color: red;
  animation: move-right 0.5s forwards;
}

.move-left {
  /* 控制元素向左滑动 */
  background-color: red;
  animation: move-left 0.5s forwards;
}
    
.done {
  /* 改变已排好序的元素的背景色 */
  background-color: rgb(253, 148, 10);
}

.checking {
  /* 正在排序中的元素的背景色 */
  background-color: red
}

@keyframes move-right {
  0% {left: 0}
  40% {left: 0}
  /* 50% {left: 45px} */
  100% {left: 45px}
}

@keyframes move-left {
  0% {left: 0}
  40% {left: 0}
  /* 50% {left: -45px} */
  100% {left: -45px}
}