六个排序比较

507 阅读3分钟

选择排序

选择排序:找下标0-N-1上的最小值,将最小值下标与0位置交换。循环依次确定下标1、下标2的值。

function selectSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    let minIndex = i
    for (let j = i + 1; j < arr.length; j++) {
      minIndex = arr[minIndex] > arr[j] ? j : minIndex
    }
    let temp = arr[i]
    arr[i] = arr[minIndex]
    arr[minIndex] = temp
  }
  return arr
}

冒泡排序

两两比较,大的放后面,小的放前面。每次先确定最后一个。

 function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] > arr[j]) {
        let temp = arr[i]
        arr[i] = arr[j]
        arr[j] = temp
      }
    }
  }
  return arr
}

插入排序

第一次循环,0-1内有序;第二次循环,0-2内有序。。。

function insertSort(arr) {
  if (arr.length < 2) return
  for (let i = 1; i < arr.length; i++) {
    for (let j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
      // let temp = arr[j]
      // arr[j] = arr[j + 1]
      // arr[j + 1] = temp
      swap(arr, j, j + 1)
    }
  }
  return arr
}
// 异或运算  只能两个不同位置的内存区域交换,否则自己和自己交换会变成0
function swap(arr, i, j) {
  arr[i] = arr[i] ^ arr[j]
  arr[j] = arr[i] ^ arr[j]
  arr[i] = arr[i] ^ arr[j]
}

归并排序

将一个数组l-mid上有序, mid--r上有序,最后合并两个。

核心方法:定义一个辅助数组,两个指针,分别从左数组、右数组开始。比较,把最小的放到辅助数组中。

function mergeSort1(arr) {
  if (arr === null || arr.length < 2) return arr
  process(arr, 0, arr.length - 1)
  return arr
}
function process(arr, l, r) {
  if (l === r) return
  let mid = l + Math.floor((r - l) >> 1)
  process(arr, l, mid)
  process(arr, mid + 1, r)
  merge(arr, l, mid, r)
}
function merge(arr, l, mid, r) {
  let p1 = l,
    p2 = mid + 1
  let h = [],
    index = 0
  while (p1 <= mid && p2 <= r) {
    h[index++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]
  }
  while (p1 <= mid) {
    h[index++] = arr[p1++]
  }
  while (p2 <= r) {
    h[index++] = arr[p2++]
  }
  for (let i = 0; i < h.length; i++) {
    arr[l + i] = h[i]
  }
}

快速排序

快排的核心就是把比某个数小的放到左边,大的放到右边,相等的在中间。

在此,介绍的是随机快排。 随机获取数组中的一个数,把它作为基准,与最后的交换,把基准数放到最后。划分小于区域、大于区域。

  • 比这个数大的就交换,大区域前进一步,停留当前下标继续比较
  • 相等的继续下一步,
  • 小于, 小区域前进一步,继续执行下一步
function quickSort3(arr) {
  if (arr.length < 2 || arr === null) return arr
  return randomProcess(arr, 0, arr.length - 1)
}
function randomProcess(arr, l, r) {
  if (l >= r) return
  let m = netherlandsFlag2(arr, l, r)
  randomProcess(arr, l, m.l - 1)
  randomProcess(arr, m.r + 1, r)
  return arr
}
function netherlandsFlag2(arr, l, r) {
  if (l > r) return { l: -1, r: -1 }
  if (l === r) return { l, r }
  let less = l - 1,
    more = r,
    index = l
  let random = Math.floor(Math.random() * (r - l + 1) + l)
  let m = arr[random]
  swap3(arr, random, r)
  while (index < more) {
    if (arr[index] > m) {
      swap3(arr, index, --more)
    } else if (arr[index] < m) {
      swap3(arr, ++less, index++)
    } else {
      index++
    }
  }
  swap3(arr, more, r)
  // console.log(m, arr, less, more)
  return { l: less + 1, r: more }
}
function swap3(arr, i, j) {
  let temp = arr[i]
  arr[i] = arr[j]
  arr[j] = temp
}

堆排序

思路:数组先变成大根堆。把堆顶和最后一个值交换,减少堆的大小,再排成大根堆,周而复始直到堆的小为0。

在循环时,可以使用heapInsert或者heapify。但是heapify方法在循环中,中的事件复杂度为O(nlogn)。

上升: 将刚要插入的节点与父节点相比 子节点大就交换,下标跑到父节点处,再与它的父节点相比。直到堆顶。

下沉:父节点与子节点比较,子节点取最大的,最大子节点和父节点相比,父节点大break,子节点大交换。将最大值得节点放到父节点上,下标跑到最大值节点处。继续比较,直到left超过堆长度。

function heapSort(arr) {
  if (arr === null || arr.length < 2) return arr
  for (let i = arr.length - 1; i >= 0; i--) {
    heapify(arr, i, arr.length)
  }
  let heapSize = arr.length
  swap(arr, 0, --heapSize)
  while (heapSize > 0) {
    heapify(arr, 0, heapSize)
    swap(arr, 0, --heapSize)
  }
  return arr
}
// 上升
function heapInsert(arr, index) {
  while (arr[index] > arr[Math.floor((index - 1) >> 1)]) {
    swap(arr, index, Math.floor((index - 1) >> 1))
    index = Math.floor((index - 1) >> 1)
  }
}
// 下沉
function heapify(arr, index, heapSize) {
  let left = (index << 1) + 1
  while (left < heapSize) {
    let largest =
      left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left
    largest = arr[index] > arr[largest] ? index : largest
    if (largest === index) break
    swap(arr, index, largest)
    index = largest
    left = (index << 1) + 1
  }
}

function swap(arr, i, j) {
  let temp = arr[i]
  arr[i] = arr[j]
  arr[j] = temp
}

稳定性与时间、额外空间复杂度比较

稳定性:相等的数在排序后仍然保持排序前的相对顺序

  • 时间复杂度O(n^2)
    • 选择排序,不稳定 额外空间复杂度O(1)
    • 冒泡排序,稳定 额外空间复杂度O(1)
    • 插入排序,稳定 额外空间复杂度O(1)
  • 时间复杂度O(nlogn)
    • 归并排序,(相等时先拷贝左边的稳定) 额外空间复杂度O(nlogn)
    • 快速排序,不稳定(划分小中大) 额外空间复杂度O(nlogn)
    • 堆排序,不稳定 额外空间复杂度O(nlogn)
    • 希尔排序,不稳定 三个排序,归并较稳定,快排速度快,堆排序空间少

计数排序 O(n) 额外空间复杂度O(M) 基数排序 O(n) 额外空间复杂度O(n)

总结

  • 不基于比较的排序,对样本数据有严格要求,不易改写
  • 基于比较的排序只要规定好两个样本怎么比较大小就直接可以复用
  • 基于比较排序的时间复杂度极限是O(nlogn)
  • 时间复杂度O(nlogn)、额外空间复杂度低于O(N)、且稳定的基于比较排序是不存在的
  • 归并较稳定,快排速度快,堆排序空间少