JavaScript实现五种常见的排序方式

331 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

在前端面试中偶尔也会遇到一些排序相关的问题,其中比较常见的五种排序方式分别为冒泡排序、选择排序、快速排序、归并排序、插入排序,今天用JavaScript实现一下,方便日后复习,如有不正确的地方,欢迎多多指正。

冒泡排序

冒泡排序(Bubble Sort)是一种最基础的交换排序。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。

复杂度

时间复杂度

最好的情况,数组本身是有顺序的,外层循环遍历一次就完成,时间复杂度为O(n)

最坏的情况,数组本身是逆序的,内外层遍历,时间复杂度为O(n^2)

空间复杂度

空间复杂度为O(1),因为该算法只需要一个额外的变量用于交换元素。

动图演示

bubbleSort.gif

代码实现:

1. 初始版本

第一层循环是用来控制趟数,如果数组的长度为len,那么程序就要执行len-1趟;第二层是控制第 i 趟的比较次数,第1趟需要比较len-1次,第 i 趟需要比较 len-i 次。循环中 k 是数组下标,是从 0 开始的,所以第i趟,k取值[0,len-i-1]

let numberArray = [8, 2, 1, 3, 4, 9,5]
// 冒泡排序函数实现
function bubbleSort (arr) {
  if (arr === null || arr.length < 2) {
    return arr
  }
  // arr长度为len, 排完序的话需要len-1趟,每趟需要比较n-i次
  for (let i = 1,len=arr.length; i <= len - 1; i++) {
    for (let k = 0; k < len - i; k++) {
      // 如果左边的元素大于右边的元素就交换他们的位置
      if (arr[k] > arr[k + 1]) {
        let temp = arr[k]
        arr[k] = arr[k + 1]
        arr[k+1] = temp
      }
    }
  }
  return arr
}
let temp = bubbleSort(numberArray)
console.log(temp) // [ 1, 2, 3, 4, 5, 8, 9 ]

2. 优化代码

数组中的元素可能出现这样的情况,在经过前面的几次排序后,数组已经排好序了,不需要再往下继续执行了。因此,我们首先想到的优化方案就是当某一趟排序之后,如果整个序列已排好序了,那么就立即退出函数。如何实现呢?可以在每一趟排序的开始定义一个变量isSort并赋值为true,在接下来的for循环中,如果发生了数据交换,那么就将isSort的值设置为false,当这一趟排序结束,就使用if条件判断isSort的值,若为true,则表示本趟排序没有进行数据交换,意味着数组已经排好序,使用break跳出最外层的for循环。

实现代码:

let arr = [8, 2, 1, 3, 4, 9,5]
// 冒泡排序函数实现
function bubbleSort (arr) {
  if (arr === null || arr.length < 2) {
    return arr
  }
  // arr长度为len, 排完序的话需要len-1趟,每趟需要比较n-i次
  for (let i = 1, len = arr.length; i <= len - 1; i++) {
    let isSort = true
    for (let k = 0; k < len - i; k++) {
      // 如果左边的元素大于右边的元素就交换他们的位置
      if (arr[k] > arr[k + 1]) {
        isSort = false
        let temp = arr[k]
        arr[k] = arr[k + 1]
        arr[k+1] = temp
      }
    }
    if (isSort) {
      break
    }
  }
  return arr
}
let temp = bubbleSort(numberArray)
console.log(temp) // [ 1, 2, 3, 4, 5, 8, 9 ]

3. 继续优化

在前面的排序中,每一趟只能排好一个元素,但是呢,实际数列真正的有序区可能会大于这个长度,举个栗子,如对该数组进行冒泡排序 [5,6,2,7,8,9],第一趟会一次比较5 66 2(交换位置)、6 77 88 9,在第一趟排序后数组为[5,2,6,7,8,9],这时数组排序过后的有序区长度是4,如果按照前面代码的实现方式,第一趟排序后有序区的长度是1,在下一趟还要两两比较前面五个元素的值,因此,可以这样做来避免这种情况:记录一下最后一次元素交换的位置,那个位置也就是无序数列的边界,该元素后就是有序数据。在这个示例数组中,第一趟排序结束得到的无序数列[5,2,6,7,8,9]的边界值为2的下标索引1

let numberArray = [5,6,2,7,8,9]
// 冒泡排序函数实现
function bubbleSort (arr) {
  if (arr === null || arr.length < 2) {
    return arr;
  }
  // arr长度为len, 排完序的话需要len-1趟,每趟需要比较n-i次
  let arrLength = arr.length
  let sortBorder = arrLength - 1
  // 记录最后一次交换的位置
  let lastExchangeSub = 0
  for (let i = 1; i <= arrLength - 1; i++) {
    let isSort = true
    for (let k = 0; k < sortBorder; k++) {
      // 如果左边的元素大于右边的元素就交换他们的位置
      if (arr[k] > arr[k + 1]) {
        isSort = false
        // 重新赋值下次循环遍历的边界,也就是说在本轮比较中,后面如果没有进入这个if条件判断,那么
        // 从下标k+1开始后面的数据都是确定好顺序的不需要再进行两两比较,只需要比较下标从0到k的数据就可以
        lastExchangeSub = k
        let temp = arr[k]
        arr[k] = arr[k + 1]
        arr[k+1] = temp
      }
    }
    sortBorder = lastExchangeSub
    if (isSort) {
      break
    }
  }
  return arr
}
let temp = bubbleSort(numberArray)
console.log(temp) // [ 2, 5, 6, 7, 8, 9 ] 

选择排序

选择排序(selectionSort)是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。算法步骤:

  1. 首先在未排序序列中找到最小元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

动图演示

selectionSort.gif

代码实现:

function selectionSort (arr) {
  if (arr === null || arr.length < 2) {
    return arr;
  }
  // 数组长度
  let len = arr.length;
  // minIndex: 存储每次循环的最小值的下标索引
  let minIndex, temp;
  for (let i = 0; i < len - 1; i++) {
    minIndex = i;
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {     // 寻找最小的数
        minIndex = j;                 // 将最小数的索引保存
      }
    }
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  return arr;
}

let arrays = [3, 2, 5, 10, 6, 4, 1]
let result = selectionSort(arrays)
console.log(result) // [ 1, 2, 3, 4, 5, 6, 10 ]

快速排序

快速排序(quickSort)也是一种较为基础的排序算法,其效率比冒泡排序算法有大幅提升。是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。 算法步骤:

  1. 从数列中挑出一个元素,称为 "基准";
  2. 分区过程:将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
  3. 递归地把小于基准值元素的子数列和大于基准值元素的子数列进行排序;

动图演示

quickSort.gif

代码实现:

方法一:

function quickSort (arr, beginIndex, endIndex) {
  if (beginIndex >= endIndex) {
    return;
  }
  // beginIndex: 开始下标  endIndex: 结束下标
  let temp = arr[beginIndex]
  let left = beginIndex
  let right = endIndex
  while (left < right) {
    while (right > left && arr[right] >= temp) {
      right--      
    }
    while (left < right && arr[left] <= temp) {
      left++     
    }
    // 如果left下标仍然小于right下标,则说明需要交换arr[left]、arr[right]的值
    if (left < right) {
      let temp = left
      arr[left] = arr[right]
      arr[right] = temp
    }
  }
  // 这时left和right的值相等
  arr[beginIndex] = arr[left]
  arr[left] = temp
  // 递归遍历
  quickSort(arr,beginIndex,left-1)
  quickSort(arr, left + 1, endIndex)
  return arr
}
let arrays = [3, 2, 5, 10, 6, 4, 1]
let result = quickSort(arrays,0,6)
console.log(result) // [ 1, 2, 2, 3, 3, 4, 6 ]

方法二:

let arr = [29, 10, 14, 37, 4]
function quickSort (arr) {
  if (arr.length <= 1) {
    return arr
  }
  let leftArr = []
  let rightArr = []
  let mid = Math.floor(arr.length / 2)
  // arr.splice(mid,1)从数组中删除下标为mid的元素并返回
  let midValue = arr.splice(mid,1)[0]
  for (let i = 0; i < arr.length; i++) {
    // 判断如果当前值小于对比值就行左边添加
    if (arr[i] < midValue) {
      leftArr.push(arr[i])
    }else{
      rightArr.push(arr[i])
    }
  }
  // 不断递归,最终将结果返回
  return quickSort(leftArr).concat([midValue],quickSort(rightArr))
}

let result = quickSort(arr)
console.log(result) // [ 4, 10, 14, 29, 37 ]

归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。以数组[8,4,5,7,1,3,6,2]的归并排序为例,函数执行到return merge(mergeSort(arr.slice(0,mid)),mergeSort(arr.slice(mid)),即merge(mergeSort(8,4,5,7),mergeSort(1,3,6,2)),其中mergeSort(8,4,5,7)mergeSort(1,3,6,2)经函数回调得到,merge([4,5,7,8],[1,2,3,6]),最后再次调用merge方法,并 return [1,2,3,4,5,6,7,8]

动图演示

mergeSort.gif

代码实现:

let arr = [8, 4, 5, 7, 1, 3, 6, 2]
/*  
  concat() 方法用于连接两个或多个字符串,也可以连接数组 
  如:[1,2].concat([3,4],[5,6]) ---- [1,2,3,4,5,6] 
  slice(start,end) 表示截取数组下标从start到end-1的数据,并返回截取的数组
  slice(start) 截取数组下标start到结尾
  如:[3,1,2,6,8].slice(2,4)  ----- [2, 6]
*/
function mergeSort (arr) {
  if (arr.length < 2) { return arr }
  // Math.floor(x) 返回小于等于x的最大整数
  let mid = Math.floor(arr.length / 2)
  let merge = function (leftArr, rightArr) {
    let resultArr = []
    // 对两个都有序的数组进行排序,例如:[4,5,7,8]和[1,2,3,6] 
    while (leftArr.length && rightArr.length) {
      // 如果左边数组第一个元素 小于或等于 右边数组第一个元素
      if (leftArr[0] <= rightArr[0]) {
        // array.shift(): 删除数组第一个元素并返回
        resultArr.push(leftArr.shift())
      } else {
        resultArr.push(rightArr.shift())
      }
    }
    // 上面的循环可能存在一个数组一个数组先遍历完,另一个数组还有元素
    return resultArr.concat(leftArr,rightArr)
  }
  
  return merge(mergeSort(arr.slice(0,mid)),mergeSort(arr.slice(mid)))
}

let result = mergeSort(arr)
console.log(result) // [ 1, 2, 3, 4, 5, 6, 7, 8 ]

插入排序

插入排序(insertSort)是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。第一个元素开始可以看作是有序的,从第二个元素开始至元素的结尾是未排序的,从头到尾依次扫描未排序序列元素,将扫描到的每个元素插入有序序列的适当位置。举个栗子,以数组[5, 3, 4, 2, 1]的插入排序为例,第一次取到3,将3与5比较,5大于3,将5移动到3的位置,数组变为[5,5,4,2,1],已经扫描完有序序列,将3放到下标0的位置,数组变为[3,5,4,2,1]。第三次取4,将4与5比较,5大于4,将5向后移动一个位置,数组变为[3,5,5,2,1],接着将4与3比较,由于4>3,因此3不移动,将4放到下标1的位置。2和1的比较同理。

动图演示

insertionSort.gif

代码实现:

function insertSort (arr) {
  // i从1开始,一个元素可以被认定是有序的
  for (let i = 1; i < arr.length; i++) {
    let temp = arr[i]
    // 记录默认已经排好序的元素
    let sortListIndex = i - 1
    // 在已经排序好的序列中进行从后到前的扫描 
    while (sortListIndex >= 0 && arr[sortListIndex] > temp) {
      // 已排序的元素大于新元素,将该元素移动到下一个位置
      arr[sortListIndex + 1] = arr[sortListIndex]
      sortListIndex--
    }
    arr[sortListIndex + 1] = temp
  }
  return arr
}

let arr = [5, 3, 4, 2, 1]
let result = insertSort(arr)
console.log(result) // [ 1, 2, 3, 4, 5 ]