冒泡排序

冒泡排序的思路是从数组的第一个元素开始,依次比较相邻的两个元素,如果不符合我们想要的排序顺序则交换位置。通过n-1次循环即可得到排好序的数组。

  1. function bubbleSort(arr) {
  2. for (let i = 0; i < arr.length - 1; i++) {
  3. for (let j = 0; j + 1 < arr.length - i; j++) {
  4. if (arr[j] > arr[j + 1]) [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
  5. }
  6. }
  7. }

因为冒泡排序在第k(k < n)轮循环可能就已经有序,因此可以设置一个flag去判断是否已经有序:

  1. function bubbleSort(arr) {
  2. for (let i = 0; i < arr.length - 1; i++) {
  3. let isSwapped = false
  4. for (let j = 0; j + 1 < arr.length - i; j++) {
  5. if (arr[j] > arr[j + 1]) {
  6. [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
  7. isSwapped = true
  8. }
  9. }
  10. if (!isSwapped) break
  11. }
  12. }

通过上面的思想,其实当一轮冒泡排序结束后,可能第[m, n - 1]的元素已经是有序的,因此其实只需要对[0, m - 1]的元素进行排序即可。同样,也设置一个flag,不过这里是记录已经有序的子数组的起始下标。

  1. function bubbleSort(arr) {
  2. for (let i = 0; i < arr.length - 1;) {
  3. let lastSwapedIndex = 0
  4. for (let j = 0; j + 1 < arr.length - i; j++) {
  5. if (arr[j] > arr[j + 1]) {
  6. [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
  7. lastSwapedIndex = j + 1 // [j + 1, arr.length - 1]的元素已经有序
  8. }
  9. }
  10. i = arr.length - lastSwapedIndex // 这里 i 可以看成每轮循环后已经有序的元素数量
  11. }
  12. }

选择排序

选择排序的思路是遍历数组,每次从剩余元素中选择最大/最小的值和当前元素交换位置,直至所有元素都排好序,时间复杂度为O(n^2)。

  1. class SelectionSort {
  2. constructor () {
  3. if (new.target) throw new Error('this is a static class.')
  4. }
  5. static sort (list) {
  6. if (list.length < 2) return list
  7. for (let i = 0; i < list.length; i ++) {
  8. let minIndex = i
  9. for (let j = i + 1; j < list.length; j ++) {
  10. if (list[j].compareTo(list[minIndex]) < 0) minIndex = j
  11. }
  12. this._swap(list, minIndex, i)
  13. }
  14. return list
  15. }
  16. // 从后往前遍历
  17. static reverseSort (list) {
  18. if (list.length < 2) return list
  19. for (let i = list.length - 1; i >= 0; i --) {
  20. let maxIndex = i
  21. for (let j = i - 1; j >= 0; j --) {
  22. if (list[j].compareTo(list[maxIndex]) > 0) maxIndex = j
  23. }
  24. this._swap(list, maxIndex, i)
  25. }
  26. return list
  27. }
  28. static _swap (list, i, j) {
  29. let temp = list[i]
  30. list[i] = list[j]
  31. list[j] = temp
  32. }
  33. }
  34. module.exports = SelectionSort

插入排序

插入排序的思路是每次找到当前元素应该插入的相对位置,然后将元素放在其正确的位置。插入排序的复杂度为O(n^2)。但是对于有序数组插入排序的复杂度为O(n)

  1. class InsertionSort {
  2. static sort (list) {
  3. for (let i = 0; i < list.length; i ++) {
  4. for (let j = i; j > 0 && list[j].compareTo(list[j - 1]) > 0; j --)
  5. this._swap(list, j, j - 1)
  6. }
  7. return list
  8. }
  9. // 通过平移的方式优化代码
  10. static betterSort (list) {
  11. for (let i = 0; i < list.length; i ++) {
  12. let t = list[i], j // 保存当前需要插入的元素下标
  13. for (j = i; j > 0 && t.compareTo(list[j - 1]) > 0; j --)
  14. list[j] = list[j - 1] // 当不是最终需要插入的位置时将元素向后平移一位
  15. // 寻找到最终位置后
  16. list[j] = t
  17. }
  18. return list
  19. }
  20. // 从后往前遍历
  21. static reverseSort (list) {
  22. for (let i = list.length - 1; i >= 0; i --) {
  23. let t = list[i], j
  24. for (j = i; j < list.length - 1 && t.compareTo(list[j + 1]) > 0; j ++)
  25. list[j] = list[j + 1]
  26. list[j] = t
  27. }
  28. return list
  29. }
  30. static _swap (list, i, j) {
  31. let temp = list[i]
  32. list[i] = list[j]
  33. list[j] = temp
  34. }
  35. }
  36. module.exports = InsertionSort

归并排序

归并排序的思路在于每次将数组平分成两份,然后分别对两个数组进行排序,再合并两个有序数组即可。归并排序的时间复杂度是O(nlogn)。同样,对于有序数组,归并排序的时间复杂度为O(n)
排序 - 图1

  1. class MergeSort {
  2. constructor () {
  3. throw new SyntaxError('cannot use new operator.')
  4. }
  5. static sort (list) {
  6. this._sort(list, 0, list.length - 1)
  7. this._cacheList = [...list]
  8. }
  9. static _sort (list, l, r) {
  10. if (l >= r) return
  11. // const m = Math.floor((l + r) / 2) 推荐使用下面的方式计算m,防止数据超出JavaScript可表示的整数范围
  12. const m = l + Math.floor((r - l) / 2)
  13. this._sort(list, l, m)
  14. this._sort(list, m + 1, r)
  15. if (list[m] > list[m + 1]) this._merge(list, l, m, r) // 如果 list[m] <= list[m + 1] 说明list已经有序了
  16. }
  17. static _merge (list, l, m, r) {
  18. for (let i = l; i <= r; i ++) this._cacheList[i] = list[i]
  19. let k = l, i = l, j = m + 1
  20. for (; k <= r; k ++) {
  21. if (i > m) {
  22. list[k] = this._cacheList[j]
  23. j ++
  24. } else if (j > r) {
  25. list[k] = this._cacheList[i]
  26. i ++
  27. } else if (this._cacheList[i] <= this._cacheList[j]) {
  28. list[k] = this._cacheList[i]
  29. i ++
  30. } else {
  31. list[k] = this._cacheList[j]
  32. j ++
  33. }
  34. }
  35. // or
  36. // while (i <= m || j <= r) {
  37. // if (i > m) {
  38. // list[k++] = this._cacheList[j]
  39. // j ++
  40. // } else if (j > r) {
  41. // list[k++] = this._cacheList[i]
  42. // i ++
  43. // } else if (this._cacheList[i] < this._cacheList[j]) {
  44. // list[k++] = this._cacheList[i]
  45. // i ++
  46. // } else {
  47. // list[k++] = this._cacheList[j]
  48. // j ++
  49. // }
  50. // }
  51. }
  52. }
  53. module.exports = MergeSort

快速排序

快速排序的思想是每次确定其中一个元素的最终位置,直至所有元素都在其最终位置上。快速排序是一个不稳定的排序算法,其时间复杂度为O(n*2),但是由于实际情况下这种最差的情况几乎不可能出现,因此通常使用期望来权衡其性能,快速排序的期望是nlogn。

单路快速排序

排序 - 图2
根据上述过程,我们假设需要确定位置的元素的下标为l,则通过一轮循环,l对应的元素已经在其最终位置,其下标为lt,它将数组分成了两个子数组。对这两个子数组重复上述操作,又将确定两个元素的位置,以此类推直至所有元素有序。

  1. class QuickSort {
  2. constructor () {
  3. throw new SyntaxError('cannot use new operator.')
  4. }
  5. static sort (list) {
  6. this._sort(list, 0, list.length - 1)
  7. }
  8. static _sort (list, l, r) {
  9. if (l >= r) return
  10. const p = this._partition(list, l, r)
  11. this._sort(list, l, p - 1)
  12. this._sort(list, p + 1, r)
  13. }
  14. static _partition (list, l, r) {
  15. let lt = l
  16. for (let i = l + 1; i <= r; i ++) {
  17. if (list[i] < list[l]) {
  18. lt ++
  19. this._swap(list, i, lt)
  20. }
  21. }
  22. this._swap(list, l, lt)
  23. return lt
  24. }
  25. static _swap (list, i, j) {
  26. const temp = list[i]
  27. list[i] = list[j]
  28. list[j] = temp
  29. }
  30. }

上述算法存在一个问题,由于每次都是确定第一个元素的位置,如果对完全有序的数组进行排序,则时间复杂度会达到O(n*2)。原因是当数组有序时,每个元素都在其最终位置上,因此当每轮循环后,都会将原有数组分成一个空数组和一个数组长度为原数组长度减1的数组。这样导致递归执行的次数等于数组长度。解决方法是每次执行_partition的时候先取一个大小为[l, r]之间的随机整数random,然后将list[l]list[random]进行交换。修改_partition如下:

  1. static _partition (list, l, r) {
  2. const random = Math.floor(Math.random() * (r - l + 1)) + l
  3. this._swap(list, l, random)
  4. let lt = l
  5. for (let i = l + 1; i <= r; i ++) {
  6. if (list[i] < list[l]) {
  7. lt ++
  8. this._swap(list, i, lt)
  9. }
  10. }
  11. this._swap(list, l, lt)
  12. return lt
  13. }

双路快速排序

上面的优化虽然一定程度上避免了有序数组造成的性能倒退问题,但是如果整个数组的值都相同,则性能依旧会倒退。这时我们可以使用双路快速排序,使用两个指针从数组的两端遍历,有效的避免了这个问题。
排序 - 图3

  1. class QuickSort {
  2. constructor () {
  3. throw new SyntaxError('cannot use new operator.')
  4. }
  5. static sort (list) {
  6. this._sort(list, 0, list.length - 1)
  7. }
  8. static _sort (list, l, r) {
  9. if (l >= r) return
  10. const p = this._partition(list, l, r)
  11. this._sort(list, l, p - 1)
  12. this._sort(list, p + 1, r)
  13. }
  14. static _partition (list, l, r) {
  15. const random = Math.floor(Math.random() * (r - l + 1)) + l
  16. this._sqrt(list, l, random)
  17. let lt = l + 1
  18. let gt = r
  19. while (true) {
  20. while (list[lt] < list[l]) lt ++
  21. while (list[gt] > list[l]) gt --
  22. if (lt >= gt) break
  23. this._sqrt(list, lt, gt)
  24. lt ++
  25. gt --
  26. }
  27. this._sqrt(list, l, gt)
  28. return gt // 注意这里返回的是 gt
  29. }
  30. static _swap (list, i, j) {
  31. const temp = list[i]
  32. list[i] = list[j]
  33. list[j] = temp
  34. }
  35. }

三路快速排序

双路快速排序虽然解决了数组元素相同的问题,但是没有对这种情况做特殊处理。事实上当元素相同的时候无需做交换的操作,此时排序的复杂度为O(n)。
排序 - 图4

  1. class QuickSort {
  2. constructor () {
  3. throw new SyntaxError('cannot use new operator.')
  4. }
  5. static sort (list) {
  6. this._sort(list, 0, list.length - 1)
  7. }
  8. static _sort (list, l, r) {
  9. if (l >= r) return
  10. const [lt, gt] = this._partition(list, l, r)
  11. this._sort(list, l, lt - 1)
  12. this._sort(list, gt, r)
  13. }
  14. static _partition (list, l, r) {
  15. const random = l + Math.floor(Math.random() * (r - l + 1))
  16. this._swap(list, l, random)
  17. let cur = l + 1
  18. let lt = l
  19. let gt = r + 1
  20. while (cur < gt) {
  21. if (list[cur] < list[l]) {
  22. lt ++
  23. this._swap(list, lt, cur)
  24. cur ++
  25. } else if (list[cur] > list[l]) {
  26. gt --
  27. this._swap(list, lt, gt)
  28. } else {
  29. cur ++
  30. }
  31. }
  32. this._swap(list, l, lt)
  33. return [lt, gt]
  34. }
  35. static _swap (list, i, j) {
  36. const temp = list[i]
  37. list[i] = list[j]
  38. list[j] = temp
  39. }
  40. }

希尔排序

对于插入排序来说,性能消耗主要在于元素之间的比较,在极端情况下,例如数组最小的元素位于末尾,则此次循环比较次数为数组长度减1,为了减少比较次数,可以考虑增大比较步长,不再固定移动1位而是将相隔h的元素进行比较。通过减小h至1完成整个排序。每次排序完一次后数组的逆序对都会减少,从而在h等于1时(相当于插入排序)需要比较的次数大大减少。这就是希尔排序。

  1. function shellSort (list) {
  2. let h = list.length >>> 1
  3. while (h >= 1) {
  4. for (let start = 0; start < h; start ++) {
  5. for (let i = start + h; i < list.length; i += h) {
  6. let cur = list[i]
  7. let j
  8. for (j = i; j - h >= 0 && cur < list[j - h]; j -= h) {
  9. list[j] = list[j - h]
  10. }
  11. list[j] = cur
  12. }
  13. }
  14. h = h >>> 1
  15. }
  16. return list
  17. }

上面的方法是将原数组按照h分成了多个子数组,通过分别对子数组排序一步步使得数组变得有序,最终完成排序。另一种方式是无需特意分成多个子数组,只需每隔h个元素进行比较即可:

  1. function shellSort (list) {
  2. let h = list.length >>> 1
  3. while (h >= 1) {
  4. for (let i = h; i < list.length; i ++) {
  5. let cur = list[i]
  6. let j
  7. for (j = i; j - h >= 0 && cur < list[j - h]; j -= h) {
  8. list[j] = list[j - h]
  9. }
  10. list[j] = cur
  11. }
  12. h = h >>> 1
  13. }
  14. return list
  15. }

上述两种方式的h都是每次减小一半,实际h的大小和希尔排序算法性能息息相关,通过将h的值变为1、4、7、10...这种规律可以提高算法性能:

  1. function shellSort (list) {
  2. let h = 1
  3. while (h < list.length) h = 3 * h + 1
  4. while (h >= 1) {
  5. for (let i = h; i < list.length; i += h) {
  6. let cur = list[i]
  7. let j
  8. for (j = i; j - h >= 0 && cur < list[j - h]; j -= h) {
  9. list[j] = list[j - h]
  10. }
  11. list[j] = cur
  12. }
  13. h = Math.floor(h / 3)
  14. }
  15. return list
  16. }

希尔排序处理一些中等大小的数组和其他时间复杂度更小的排序算法性能相差不大,且无需使用递归,因此在处理实际问题时如果数据规模不大可以考虑使用希尔排序。