丰富图例讲解十大经典排序算法 | 面试必备

面试官问:你会三路快排吗?git

我:github

...面试


对比

关于时间复杂度:算法

  1. 平方阶 (O(n**2)) 排序 各种简单排序:直接插入、直接选择和冒泡排序。
  2. 线性对数阶 (O(nlog2n)) 排序: 快速排序、堆排序和归并排序;
  3. O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。 希尔排序
  4. 线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。

原地排序:特指空间复杂度是 O(1) 的排序算法。shell

稳定性:若是待排序的序列中存在值相等的元素,通过排序以后,相等元素之间原有的前后顺序不变。api

冒泡排序

冒泡排序(英语:Bubble Sort)又称为泡式排序,是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,若是他们的顺序错误就把他们交换过来。走访数列的工做是重复地进行直到没有再须要交换,也就是说该数列已经排序完成。这个算法的名字由来是由于越小的元素会经由交换慢慢“浮”到数列的顶端。数组

冒泡排序对 n 个项目须要 O(n**2) 的比较次数,且能够原地排序。尽管这个算法是最简单了解和实现的排序算法之一,但它对于包含大量的元素的数列排序是很没有效率的。微信

冒泡排序是与插入排序拥有相等的运行时间,可是两种算法在须要的交换次数却很大地不一样。在最坏的状况,冒泡排序须要 O(n**2) 次交换,而插入排序只要最多 O(n) 交换。冒泡排序的实现(相似下面)一般会对已经排序好的数列拙劣地运行(O(n ** 2)),而插入排序在这个例子只须要 O(n) 个运算。所以不少现代的算法教科书避免使用冒泡排序,而用插入排序取代之。冒泡排序若是能在内部循环第一次运行时,使用一个旗标来表示有无须要交换的可能,也能够把最优状况下的复杂度下降到 O(n) 。在这个状况,已经排序好的数列就无交换的须要。若在每次走访数列时,把走访顺序反过来,也能够稍微地改进效率。有时候称为鸡尾酒排序,由于算法会从数列的一端到另外一端之间穿梭往返。markdown

冒泡排序算法的运做以下:数据结构

  1. 比较相邻的元素。若是第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素做一样的工做,从开始第一对到结尾的最后一对。这步作完后,最后的元素会是最大的数。
  3. 针对全部的元素重复以上的步骤,除了最后一个。
  4. 持续每次对愈来愈少的元素重复上面的步骤,直到没有任何一对数字须要比较。
export function bubbleSort(arr: number[]) {
  const length = arr.length
  if (length <= 1) return arr
  for (let i = 0; i < length; i++) {
    let changed: boolean = false // 没有数据交换则表示已经有序了
    for (let j = 0; j < length - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        swap(arr, j, j + 1)
        changed = true
      }
    }
    if (!changed) break
  }
  return arr
}
复制代码

鸡尾酒排序

export function cocktailSort(arr: number[]) {
  const len = arr.length
  for (let i = 0; i < len / 2; i++) {
    let start: number = 0
    let end: number = len - 1
    for (let j = start; j < end; j++) {
      if (arr[j] > arr[j + 1]) swap(arr, j, j + 1)
    }
    end--
    for (let j = end; j > start; j--) {
      if (arr[j] < arr[j - 1]) swap(arr, j - 1, j)
    }
    start++
  }
  return arr
}
复制代码

冒泡排序

冒泡排序

冒泡排序2

鸡尾酒排序

鸡尾酒排序

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工做原理以下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,而后,再从剩余未排序元素中继续寻找最小(大)元素,而后放到已排序序列的末尾。以此类推,直到全部元素均排序完毕。

选择排序的主要优势与数据移动有关。若是某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,所以对 n 个元素的表进行排序总共进行至多 n - 1 次交换。在全部的彻底依靠交换去移动元素的排序方法中,选择排序属于很是好的一种。

export function selectionSort(arr: number[]) {
  const length = arr.length
  if (length <= 1) return arr
  for (let i = 0; i < length; i++) {
    let min = i
    for (let j = i + 1; j < length; j++) {
      if (arr[j] < arr[min]) {
        min = j
      }
    }
    swap(arr, i, min)
  }
  return arr
}
复制代码

选择排序1

选择排序2

选择排序3

插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工做原理是经过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,一般采用 in-place 排序(即只需用到 O(1) 的额外空间的排序),于是在从后向前扫描过程当中,须要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

Insertion Sort 和打扑克牌时,从牌桌上逐一拿起扑克牌,在手上排序的过程相同。

通常来讲,插入排序都采用 in-place 在数组上实现。具体算法描述以下:

  1. 从第一个元素开始,该元素能够认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 若是该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤 2~5
export function insertionSort(arr: number[]) {
  const length = arr.length
  if (length <= 1) return arr
  for (let i = 1; i < length; i++) {
    const cur = arr[i]
    let j = i - 1
    for (; j >= 0; j--) {
      if (arr[j] > cur) {
        arr[j + 1] = arr[j]
      } else {
        break
      }
    }
    arr[j + 1] = cur
  }

  return arr
}
复制代码

or

export function insertionSort2(arr: number[]) {
  const len = arr.length
  for (let i = 1; i < len; i++) {
    for (let j = i - 1; j >= 0; j--) {
      if (arr[j] > arr[j + 1]) {
        // 这里是更改两个元素,因此比上面的方法效率低
        swap(arr, j + 1, j)
      } else {
        break
      }
    }
  }
  return arr
}
复制代码

插入排序1

插入排序2

插入排序3

快速排序

快速排序,快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最先由东尼·霍尔提出。在平均情况下,排序 n 个项目要 O(nlogn) (大 O 符号)次比较。在最坏情况下则须要 O(n**2) 次比较,但这种情况并不常见。事实上,快速排序 O(nlogn) 一般明显比其余算法更快,由于它的内部循环(inner loop)能够在大部分的架构上颇有效率地达成。

快速排序使用 分治法(Divide and conquer) 策略来把一个序列(list)分为较小和较大的 2 个子序列,而后递归地排序两个子序列。

步骤为:

  1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot),
  2. 分割:从新排序数列,全部比基准值小的元素摆放在基准前面,全部比基准值大的元素摆在基准后面(与基准值相等的数能够到任何一边)。在这个分割结束以后,对基准值的排序就已经完成,
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。

选取基准值有数种具体方法,此选取方法对排序的时间性能有决定性影响。

1 普通快排

function partition(arr: number[], left: number, right: number): number {
  let pivot: number = left // 默认从最左边开始,有优化空间
  let index = pivot + 1
  for (let i = index; i <= right; i++) {
    if (arr[i] < arr[pivot]) {
      swap(arr, i, index)
      index++
    }
  }
  swap(arr, pivot, index - 1)
  return index - 1
}

export function quickSort(arr: number[], l?: number, r?: number) {
  const len = arr.length
  const left: number = typeof l === 'number' ? l : 0
  const right: number = typeof r === 'number' ? r : len - 1
  let partitionIndex = 0
  if (left < right) {
    partitionIndex = partition(arr, left, right)
    quickSort(arr, left, partitionIndex - 1)
    quickSort(arr, partitionIndex + 1, right)
  }
  return arr
}
复制代码

2 左右指针快排

function partition(arr: number[], left: number, right: number): number {
  let l: number = left // 默认从最左边开始,有优化空间
  let r: number = right
  const target: number = arr[left]

  while (l < r) {
    while (arr[r] >= target && r > l) {
      r--
    }
    while (arr[l] <= target && l < r) {
      l++
    }
    swap(arr, l, r)
  }

  if (l !== left) {
    swap(arr, l, left)
  }

  return l
}

export function quickSort2(arr: at, l?: number, r?: number) {
  const len = arr.length
  const left: number = typeof l === 'number' ? l : 0
  const right: number = typeof r === 'number' ? r : len - 1
  let partitionIndex = 0
  if (left < right) {
    partitionIndex = partition(arr, left, right)
    quickSort2(arr, left, partitionIndex - 1)
    quickSort2(arr, partitionIndex + 1, right)
  }
  return arr
}
复制代码

3 三路快排

function partion(arr: at, l: number, r: number) {
  // 基准数选取区间的第一个值
  let v = arr[l]
  let lt = l
  let gt = r + 1

  // 下面的循环很差理解
  // i 和 gt 都在变化,gt 向左移动能够不影响 i,lt 增加会把等于v的项转移到 i,因此须要 i++
  for (let i = l + 1; i < gt; ) {
    if (arr[i] === v) {
      // lt 和 i 在这里拉开差距
      i++
    } else if (arr[i] > v) {
      swap(arr, gt - 1, i)
      gt--
    } else {
      swap(arr, lt + 1, i)
      lt++
      i++
    }
  }

  swap(arr, l, lt) // arr[lt] === v
  lt--
  return { lt, gt }
}

export function quickSort3(arr: at, l?: number, r?: number) {
  const len = arr.length
  const left: number = typeof l === 'number' ? l : 0
  const right: number = typeof r === 'number' ? r : len - 1
  if (left >= right) return
  let { lt, gt } = partion(arr, left, right)
  quickSort3(arr, l, lt)
  quickSort3(arr, gt, r)
  return arr
}
复制代码

快速排序

快速排序

希尔排序(shell sort)

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是基于插入排序的如下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操做时,效率高,便可以达到线性排序的效率
  2. 但插入排序通常来讲是低效的,由于插入排序每次只能将数据移动一位

希尔排序经过将比较的所有元素分为几个区域来提高插入排序的性能。这样可让一个元素能够一次性地朝最终位置前进一大步。而后算法再取愈来愈小的步长进行排序,算法的最后一步就是普通的插入排序,可是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。

export function shellSort(arr: number[]) {
  const length: number = arr.length
  let i, j
  // 调整 gap
  for (let gap = length >> 1; gap > 0; gap >>= 1) {
    // 按区间插排
    for (i = gap; i < length; i++) {
      let temp: number = arr[i]
      // 从当前位置往左按区间扫描
      for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
        arr[j + gap] = arr[j]
      }
      arr[j + gap] = temp
    }
  }
  return arr
}
复制代码

or

export function shellSort2(arr: number[]) {
  const length: number = arr.length
  let i, j
  // 调整 gap
  for (let gap = length >> 1; gap > 0; gap >>= 1) {
    // 按区间插排
    for (i = gap; i < length; i++) {
      // 从当前位置往左按区间扫描
      for (j = i - gap; j >= 0 && arr[j] > arr[j + gap]; j -= gap) {
        // 这里是更改两个元素,因此比上面的方法效率低
        swap(arr, j, j + gap)
      }
    }
  }
  return arr
}
复制代码

希尔排序

归并排序(merge sort)

归并排序(英语:Merge sort,或 mergesort),是建立在归并操做上的一种有效的排序算法,效率为 O(nlogn)。1945 年由约翰·冯·诺伊曼首次提出。该算法是采用 分治法(Divide and Conquer) 的一个很是典型的应用,且各层分治递归能够同时进行。

采用分治法:

  1. 分割:递归地把当前序列平均分割成两半。
  2. 集成:在保持元素顺序的同时将上一步获得的子序列集成到一块儿(归并)。

归并操做(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操做。归并排序算法依赖归并操做。

归并排序有两种思路:

递归法(Top-down)

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤 3 直到某一指针到达序列尾
  5. 将另外一序列剩下的全部元素直接复制到合并序列尾
function merge(lArr: number[], rArr: number[]) {
  const result: number[] = []
  while (lArr.length && rArr.length) {
    if (lArr[0] < rArr[0]) {
      result.push(<number>lArr.shift())
    } else {
      result.push(<number>rArr.shift())
    }
  }
  while (lArr.length) {
    result.push(<number>lArr.shift())
  }
  while (rArr.length) {
    result.push(<number>rArr.shift())
  }
  return result
}

function merge2(lArr: number[], rArr: number[]) {
  const result: number[] = []
  let lLen = lArr.length
  let rLen = rArr.length
  let i = 0
  let j = 0
  while (i < lLen && j < rLen) {
    if (lArr[i] < rArr[j]) result.push(lArr[i++])
    else result.push(rArr[j++])
  }

  while (i < lLen) result.push(lArr[i++])
  while (j < rLen) result.push(rArr[j++])

  return result
}
复制代码

迭代法(Bottom-up)

原理以下(假设序列共有 n 个元素):

  1. 将序列每相邻两个数字进行归并操做,造成 ceil(n/2) 个序列,排序后每一个序列包含两/一个元素
  2. 若此时序列数不是 1 个则将上述序列再次归并,造成 ceil(n/4) 个序列,每一个序列包含四/三个元素
  3. 重复步骤 2,直到全部元素排序完毕,即序列数为 1
export function mergeSort2(arr: number[]): number[] {
  const len = arr.length
  for (let sz = 1; sz < len; sz *= 2) {
    for (let i = 0; i < len - sz; i += 2 * sz) {
      const start = i
      const mid = i + sz - 1
      const end = Math.min(i + 2 * sz - 1, len - 1)
      merge(arr, start, mid, end)
    }
  }
  return arr
}

function merge(arr: number[], start: number, mid: number, end: number) {
  let i = start
  let j = mid + 1
  const tmp = []
  let k = start
  for (let w = start; w <= end; w++) {
    tmp[w] = arr[w]
  }
  while (i < mid + 1 && j < end + 1) {
    if (tmp[i] < tmp[j]) arr[k++] = tmp[i++]
    else arr[k++] = tmp[j++]
  }
  while (i < mid + 1) arr[k++] = tmp[i++]
  while (j < end + 1) arr[k++] = tmp[j++]
}
复制代码

归并排序
归并排序
排序一组数字

堆排序(heap sort)

一般堆是经过一维数组来实现的。在数组起始位置为 0 的情形中:

  1. 父节点 i 的左子节点在位置 2 * i + 1
  2. 父节点 i 的右子节点在位置 2 * i + 2
  3. 子节点 i 的父节点在位置 floor((i -1) / 2)

堆的操做

在堆的数据结构中,堆中的最大值老是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义如下几种操做:

  1. 最大堆调整(Max Heapify):将堆的末端子节点做调整,使得子节点永远小于父节点
  2. 建立最大堆(Build Max Heap):将堆中的全部数据从新排序
  3. 堆排序(HeapSort):移除位在第一个数据的根节点,并作最大堆调整的递归运算
function heapifyMax(arr: at, i: number, len: number) {
  const left = 2 * i + 1
  const right = 2 * i + 2
  let max = i

  if (left < len && arr[left] > arr[max]) {
    max = left
  }

  if (right < len && arr[right] > arr[max]) {
    max = right
  }

  if (max !== i) {
    swap(arr, max, i)
    heapifyMax(arr, max, len)
  }
}

function heapifyMin(arr: at, i: number, len: number) {
  const left = 2 * i + 1
  const right = 2 * i + 2
  let min = i

  if (left < len && arr[left] < arr[min]) {
    min = left
  }

  if (right < len && arr[right] < arr[min]) {
    min = right
  }

  if (min !== i) {
    swap(arr, min, i)
    heapifyMin(arr, min, len)
  }
}

// 构建大顶堆
function buildMaxHeap(arr: at) {
  const len = arr.length
  for (let i = Math.floor(len / 2); i >= 0; i--) {
    heapifyMax(arr, i, len)
  }
}

// 构建小顶堆
function buildMinHeap(arr: at) {
  const len = arr.length
  for (let i = Math.floor(len / 2); i >= 0; i--) {
    heapifyMin(arr, i, len)
  }
}

// asc 为 true 表示从小到大,false 为从大到小
export function heapSort(arr: at, asc: boolean = true) {
  if (asc) {
    buildMaxHeap(arr)
    const len = arr.length
    for (let i = len - 1; i > 0; i--) {
      swap(arr, 0, i)
      heapifyMax(arr, 0, i)
    }
  } else {
    buildMinHeap(arr)
    const len = arr.length
    for (let i = len - 1; i > 0; i--) {
      swap(arr, 0, i)
      heapifyMin(arr, 0, i)
    }
  }
  return arr
}
复制代码

堆排序

计数排序

限定为非负数

计数排序(Counting sort)是一种稳定的线性时间排序算法。该算法于 1954 年由 Harold H. Seward 提出。计数排序使用一个额外的数组 C ,其中第 i 个元素是待排序数组 A 中值等于 i 的元素的个数。而后根据数组 C 来将 A 中的元素排到正确的位置。

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 t(n+k)。计数排序不是比较排序,排序的速度快于任何比较排序算法

因为用来计数的数组 C 的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,须要大量时间和内存。例如:计数排序是用来排序 0 到 100 之间的数字的最好的算法,可是它不适合按字母顺序排序人名。可是,计数排序能够用在基数排序算法中,可以更有效的排序数据范围很大的数组。

算法的步骤以下:

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每一个值为 i 的元素出现的次数,存入数组 C 的第 i 项
  3. 全部的计数累加(从 C 中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每一个元素 i 放在新数组的第 C[i]项,每放一个元素就将 C[i] 减去 1
export function countingSort(arr: at) {
  const bucket: at = []
  const len = arr.length
  // 数组下标的游标
  let sortIndex: number = 0

  for (let i = 0; i < len; i++) {
    if (bucket[arr[i]]) {
      bucket[arr[i]]++
    } else {
      // 数组的下标不能为负数,因此计数排序限制只能排序天然数
      bucket[arr[i]] = 1
    }
  }

  for (let j = 0; j < bucket.length; j++) {
    while (bucket[j]) {
      arr[sortIndex++] = j
      bucket[j]--
    }
  }
  return arr
}
复制代码

计数排序

基数排序

基数排序(英语:Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不一样的数字,而后按每一个位数分别比较。因为整数也能够表达字符串(好比名字或日期)和特定格式的浮点数,因此基数排序也不是只能使用于整数。基数排序的发明能够追溯到 1887 年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献。

它是这样实现的:将全部待比较数值(正整数)统一为一样的数字长度,数字较短的数前面补零。而后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成之后,数列就变成一个有序序列。

基数排序的方式能够采用 LSD(Least significant digital)或 MSD(Most significant digital),LSD 的排序方式由键值的最右边开始,而 MSD 则相反,由键值的最左边开始。

基数排序的时间复杂度是 O(k*n),其中 n 是排序元素个数,k 是数字位数。这不是说这个时间复杂度必定优于 O(nlogn),k 的大小取决于数字位的选择(好比比特位数),和待排序数据所属数据类型的全集的大小;k 决定了进行多少轮处理,而 n 是每轮处理的操做数目。

export function radixSort(arr: at): at {
  const len = arr.length
  const max = Math.max(...arr)
  let buckets: at[] = []
  let digit = `${max}`.length
  let start = 1
  let res: at = arr.slice()
  while (digit > 0) {
    start *= 10
    for (let i = 0; i < len; i++) {
      const j = res[i] % start
      if (buckets[j] === void 0) {
        buckets[j] = []
      }
      buckets[j].push(res[i])
    }
    res = []
    for (let j = 0; j < buckets.length; j++) {
      buckets[j] && (res = res.concat(buckets[j]))
    }
    buckets = []
    digit--
  }
  return res
}
复制代码

基数排序

桶排序、箱排序

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工做的原理是将数组分到有限数量的桶里。每一个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种概括结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(O(n))。但桶排序并非比较排序,他不受到 O(nlogn)下限的影响。

桶排序如下列程序进行:

  1. 设置一个定量的数组看成空桶子。
  2. 寻访序列,而且把项目一个一个放到对应的桶子去。
  3. 对每一个不是空的桶子进行排序。
  4. 从不是空的桶子里把项目再放回原来的序列中。
export function bucketSort(arr: at, size: number = 5) {
  const len = arr.length
  const max = Math.max(...arr)
  const min = Math.min(...arr)
  const bucketSize = Math.floor((max - min) / size) + 1
  const bucket: at[] = []
  const res: at = []

  for (let i = 0; i < len; i++) {
    const j = Math.floor((arr[i] - min) / bucketSize)
    !bucket[j] && (bucket[j] = [])
    bucket[j].push(arr[i])
    let l = bucket[j].length
    while (l > 0) {
      // 每一个桶内部要进行排序
      // 冒泡已经很快了,其实只有一个元素须要肯定本身的位置
      bucket[j][l] < bucket[j][l - 1] && swap(bucket[j], l, l - 1)
      // 不要直接这么一个排序,bucket[j]内部都是有序的,只有最后一个是无序的
      // bucket[j].sort((a, b) => a - b)
      l--
    }
  }

  // 每一个桶内部数据已是有序的
  // 将桶内数组拼接起来便可
  for (let i = 0; i < bucket.length; i++) {
    const l = bucket[i] ? bucket[i].length : 0
    for (let j = 0; j < l; j++) {
      res.push(bucket[i][j])
    }
  }
  return res
}
复制代码

桶排序
图片来自:五分钟学算法

简单测下性能

性能测试代码

测试条件是

for (let i = 0; i < 100000; i++) {
  arr.push(Math.floor(Math.random() * 10000))
}
复制代码

测试结果

我测了不少遍,发现计数排序的速度是绝对的第一,固然空间上落后了,若是是大量重复度高,间距不大的值能够考虑。普通快排的速度也是很是快,基数、桶(可能与桶内排序算法速度有关)虽然是非比较类排序,可是速度上并不占优点,希尔、堆排序速度也是很是快,而选择、冒泡排序则很是缓慢。

参考


欢迎你们关注个人掘金和公众号,算法、TypeScript、React 及其生态源码按期讲解。


推荐一个很是好用的多平台编辑器, 支持直接复制到微信、知乎、掘金、头条等平台,样式可自定义,预约义的也很好看。

相关文章
相关标签/搜索