常见的排序算法

176 阅读2分钟

还记得大学时候学到的数据结构,书上有一个大章节,专门讲解几种排序算法,老师也颇为重视(期末必考)。排序算法大概分为插入排序、交换排序、选择排序、归并排序、基数排序,其中每个大分类下又有几个小分类,例如插入排序又分为直接插入排序、希尔排序,交换排序又分为冒泡排序、快速排序等等,评价一个算法的优劣可以从时间复杂度和空间复杂度两方面考量,排序算法除了从这两方面,还要考虑稳定性(相同的数值在排序前和排序后的顺序是否保持一致)。

今天主要总结一下这几种排序算法:快速排序、选择排序、归并排序、计数排序、堆排序。

一、快速排序

见代码:

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr
  }
  let index = parseInt(arr.length / 2)
  let leftArr = []
  let rightArr = []
  let pivot = arr.splice(index, 1)[0] //如果不删除比较的那一个数就会引起无限循环
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < pivot) {
      leftArr.push(arr[i])
    } else {
      rightArr.push(arr[i])
    }
  }
  return quickSort(leftArr).concat(pivot, quickSort(rightArr))
}

快速排序的思想是先选出数组的一个数作为标杆(一般为中间那个数),然后定义两个数组,分别存储所有比该数小的数和比该数大的数,接着再分别对这两个数组进行快速排序(递归),代码如上所示,要特别注意递归结束的判断条件以及将作为标杆的那个数从原数组中删除,否则将会引起无限循环。这是一种不稳定的排序算法,平均时间复杂度为 O(nlog2n),空间复杂度为 O(log2n) (2 为底数)。

二、选择排序

见代码:

function chooseSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    let index = i
    //找出 i 之后数组中最小的数的下标
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[j] < arr[index]) {
        index = j
      }
    }
    if (arr[index] < arr[i]) {
      let temp = arr[i]
      arr[i] = arr[index]
      arr[index] = temp
    }
  }
  return arr
}

选择排序的主要思想就是将原数组中的每一个数依次和后面最小的数进行比较,如果较小就将两者进行交换,最后返回原数组。该算法的时间复杂度为 O(n2)(2为上标),算法中没有定义多余的变量,空间复杂度为 O(1),是一种不稳定的排序算法。

三、归并排序

见代码:

function merge(a, b) {
  if (a.length === 0) {
    return b
  }
  if (b.length === 0) {
    return a
  }
  return a[0] > b[0] ? [b[0]].concat(merge(a, b.slice(1))) : [a[0]].concat(merge(a.slice(1), b))
}

function mergeSort(arr) {
  if (arr.length === 1) {
    return arr
  }
  let left = arr.slice(0, Math.floor(arr.length / 2))
  let right = arr.slice(Math.floor(arr.length / 2))
  return merge(mergeSort(left), mergeSort(right))
}

归并排序的思想就像体育老师指挥排队一样,先告诉同学们一个排队的规则(从高到矮或从矮到高),然后让同学们自行比较,比较的时候先两两分组比较邻近的同学,然后再在临近两组之间比较,直至只剩两组,比较完成后形成一个有序的队列,这是一个慢慢归并的过程,代码如上所示,时间复杂度为 O(nlog2n)(2为底数),空间复杂度为 O(n),是一种稳定的排序算法。

四、计数排序

function countSort(arr) {
  let hashTable = {}
  let result = []
  let min = 0
  let max = 0
  for (let i = 0; i < arr.length; i++) {
    if (!(arr[i] in hashTable)) {
      hashTable[arr[i]] = 1
    } else {
      hashTable[arr[i]] ++
    }
    if (arr[i] > max) {
      max = arr[i]
    }
    if (arr[i] < min) {
      min = arr[i]
    }
  }
  for (let j = min; j <= max; j++) {
    if (hashTable[j]) {
      for (let k = 1; k <= hashTable[j]; k++) {
        result.push(j)
      }
    }
  }
  return result
}

计数排序使用空间复杂度来换取时间复杂度,它的思想主要是利用一个哈希表,使用其中的 key 来存储数组中的每一个数并进行计数,计数的原因是同一个数可能在数组中出现多次,在存储哈希表的过程中记录下数组中的最大值和最小值,最后从最小数开始遍历,如果在哈希表中存在的话就将之存入 result 中。这种算法的时间复杂度和空间复杂度主要取决于最大值和最小值之间的差,是一种稳定的排序算法。

再附上一个计数排序的代码:

function countSort(arr, maxValue) {
  let result = []
  let bucket = new Array(maxValue + 1)
  let startIndex = 0
  for (let i = 0; i < arr.length; i++) {
    if (!bucket[arr[i]]) {
      bucket[arr[i]] = 0;
    }
    bucket[arr[i]] ++
  }
  for (let j = 0; j < bucket.length; j++) {
    while (bucket[j] > 0) {
      result[startIndex++] = j
      bucket[j] --
    }
  }
  return result
}

想要了解更多该算法的内容,可参考:计数排序

五、堆排序

//维护堆的性质
function heapify(array, n, i) {
    let lson = 2 * i + 1
    let rson = 2 * i + 2
    let largest = i
    if (lson < n && array[lson] > array[largest]) {
        largest = lson
    }
    if (rson < n && array[rson] > array[largest]) {
        largest = rson
    }
    if (largest !== i) {
        const temp = array[i]
        array[i] = array[largest]
        array[largest] = temp
        heapify(array, n, largest)
    }
}
function heapSort(array) {
    const n = array.length
    //建堆
    for (let i = (n - 1) >> 1; i >= 0; i--) {
        heapify(array, n, i)
    }
    //排序
    for (let j = n - 1; j >= 0; j--) {
        const temp = array[0]
        array[0] = array[j]
        array[j] = temp
        heapify(array, j, 0)
    }
    return array
}

堆排序的思想是先建立一个大根堆,然后基于堆的性质进行排序,建堆的时间复杂度是 O(n),堆排序的时间复杂度是O(nlog2n),推荐一个堆排序讲得还不错的视频:堆排序

最后分享一个有趣的算法在线图解