排序算法归纳
关键字说明
评价算法的术语
时间复杂度: 算法中基本操作重复执行的次数是问题规模n的函数f(n),T(n)=O(f(n))
平均时间复杂度: 所有可能的输入数据对应f(n)的平均值,一般我们所说的时间复杂度都是指平均时间复杂度
空间复杂度: 算法所需要的存储空间同问题规模n的函数
稳定: 对于大小相等的两个数x1和x2,排序前x1在x2前面,排序后x1还是会在x2前面
不稳定: 排序后x1可能会在x2后面
内部排序: 所有排序操作都在内存中完成
外部排序: 待排序的序列存储在外存上,排序过程中需要进行多次的内、外存之间的交换。例如大文件的排序。
排序算法比较
二分查找
在一个有序序列中查找出关键字所在的位置。可以利用二分的思想,假设序列为非递减序列,将关键字和中间的元素比较,如果等于中间元素,则返回中间元素的下标,如果比中间元素小,那么查找的关键字必定在左边区域,如果比中间元素大,那么查找的关键字比定在右边区域,此为一趟查找,然后再进行递归,直到找到关键字或者查找的区域为空,则此序列中没有该关键字。
function binarySearch(arr, key, left, right) {
if (left > right) { return -1}
let mid = Math.floor((left+right)/2)
if (key === arr[mid]) { return mid }
if (key < arr[mid]) { return binarySearch(arr, key, left, mid-1) }
if (key > arr[mid]) { return binarySearch(arr, key, mid+1, right) }
}
// 二分查找 非递归
function binarySearch(arr, key) {
if (!Array.isArray(arr) || arr.length <= 0) {
throw Error('非法输入')
}
let index = -1, left = 0, right = arr.length-1
while (left <= right) {
let mid = Math.floor((left+right)/2)
if (key === arr[mid]) { return mid }
(key < arr[mid]) ? (right = mid-1) : (left = mid+1);
}
return index
}
七大排序算法
插入排序
基本思想:在一个长度为i-1的已经排好序的序列中插入一个元素,从而得到一个长度为i的有序序列。
思路:从已排好序的序列的最后一个元素往前搜索,找到第一个比插入元素小的元素,插入到它后面
当待排序的序列已经有序时,时间复杂度最小,为O(n-1);当待排序的序列为逆序时,时间复杂度最大。平均时间复杂度为为O(n^2)
当待排序序列的长度比较小时,插入排序是一种很好的选择。js数组原生的sort方法,在数组长度比较小时用的就是插入排序。
function insertSort(arr) {
if (!Array.isArray(arr) || arr.length <= 1) {
return arr
}
let i, j
for (i = 1; i < arr.length; i++) {
let key = arr[i]
for (j = i-1; j >= 0; j--) {
if (key > arr[j]) {
arr[j+1] = key
break;
}else {
arr[j+1] = arr[j]
}
}
j < 0 && (arr[0] = key)
}
return arr
}
希尔排序
希尔排序属于插入排序的一种,利用插入排序的特性:当待排序的序列基本有序时,插入排序的效率会大大提升。先将待排序的序列按照固定增量分割为若干个子序列,分别进行插入排序,使得各个子序列有序,再缩小增量,使得整个序列基本有序,最后一趟增量缩小为1,对整个序列进行插入排序。
希尔排序需要一个合适的增量序列,应使增量序列中的任意两个值没有有大于1的公因子,否则就重复了。目前还没有公认的最佳的增量的序列。
function shellSort(arr, stepArr = [5, 3, 1]) {
if (!Array.isArray(arr) || !Array.isArray(stepArr) || arr.length <= 1) {
return arr
}
stepArr.forEach(step => shellInsert(arr, step))
return arr
}
function shellInsert(arr, step) {
if (!Array.isArray(arr) || arr.length <= 1) {
return arr
}
let i, j, k
for (k = 0; k < step; k++) {
for (i = step+k; i < arr.length; i += step) {
let key = arr[i]
for (j = i-step; j >= 0; j -= step) {
if (key > arr[j]) {
arr[j+step] = key
break
}else {
arr[j+step] = arr[j]
}
}
j < 0 && (arr[k] = key)
}
}
}
冒泡排序
冒泡排序是最简单的排序算法之一。每一趟循环将待排序中最大的元素放到序列末尾,每一趟能将一个元素排好序,经过n-1趟后整个数组便排好序了。时间复杂度为O(n^2)
function bubbleSort(arr) {
if ( (!Array.isArray(arr) || arr.length <= 1)) {
return arr
}
let i, j, len = arr.length
for (i = 0; i < len-1; i++) {
for (j = 0; j < len-1; j++) {
if (arr[j] > arr[[j+1]]) {
[arr[j], arr[j+1]] = [arr[j+1], arr[j]]
}
}
}
return arr
}
快速排序
快速排序是实际工作中最常用的排序算法,是最好的排序算法之一,在时间复杂度为O(nlogn)的排序算法中,平均性能最好。
基本思想:从待排序序列中选取一个关键字,将其他元素和关键字进行比较,比关键字小的元素放在它前面,比关键字大的元素放在它后面,这样通过这个关键字将数组划分成了两部分,关键字前面的元素都比后面的元素小,这是一趟快排。再利用递归的思想,对关键字前面的元素和关键字后面的元素分别进行快排,递归的终止条件是关键字分割的前面和后面两部分只剩下一个元素,或者没有元素,此时局部便排好序了。
快排的时间复杂度和关键字的选取有关,当每次选取的关键字为数组中最大或最小的元素时,快排退化为冒牌排序,时间复杂度为O(n^2);当每次选取的关键字能将前后划分成长度相近的两部分,快排的性能达到最优,时间复杂度为O(nlogn)。平均时间复杂度为O(nlogn)
function quickSort(arr) {
if (!Array.isArray(arr) || arr.length <= 1) {
return arr
}
let index, key, left = [], right = []
// 选取关键字,并在数组中剔除关键字
index = Math.floor((arr.length)/2)
key = arr[index]
arr.splice(index, 1)
arr.forEach(el => { (el < key) ? left.push(el) : right.push(el) });
return quickSort(left).concat(key, quickSort(right))
}
选择排序
选择排序也是最简单的排序算法之一。其思路是每一趟遍历找出未排序序列中最小或最大的元素,将其和第一个元素和最后一个元素交换位置。时间复杂度为O(n^2)
function selectSort(arr) {
if (!Array.isArray(arr) || arr.length <= 1) {
return arr
}
let i, j, min, minIndex, len = arr.length
for (i = 0; i < len; i++) {
min = arr[i]
minIndex = i
for (j = i+1; j < len; j++) {
if (arr[j] < min) {
min = arr[j]
minIndex = j
}
}
(minIndex !== i) && ([arr[i], arr[minIndex]] = [min, arr[i]])
}
return arr
}
堆排序
基本思想:堆排序是选择排序的一种。堆的定义:在一颗完全二叉树中,非终端节点的值全都大于(或全都小于)其左右子节点的值,对应的堆称为大顶堆或小顶堆,堆顶的元素一定是最大的或最小的。对一组无序的序列进行堆排序,就是把这组序列对应的完全二叉树建成大顶堆或小顶堆,再不断取出堆顶元素。
建堆:从最后一个非终端节点开始,往前一直到第一个节点,调整节点和子节点的位置。如果要建大顶堆,则将当前根节点和子节点中最大的元素调整为根节点,使得左右子节点的值都小于根节点。
思路:先建成大顶堆,再每次将堆顶元素和无序序列中最后一个元素交换位置,并重新调整堆,每次放到最后的都是无序序列中最大的元素。建成小顶堆直接遍历的话,小顶堆只能保证非终端节点的值小于左右节点的值,但不能保证整个堆是有序的,所以建成小顶堆只能依次取出堆顶元素,需要额外O(n)的存储空间。
function heapSort(arr) {
if (!Array.isArray(arr) || arr.length <= 1) {
return arr
}
let i, j, heapSize = arr.length
// 建大顶堆
for (i = Math.floor(heapSize/2)-1; i >= 0; i--) {
heapAdjust(arr, i, heapSize)
}
for (i = heapSize-1; i >= 1; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]]
heapAdjust(arr, 0, --heapSize)
}
return arr
}
function heapAdjust(arr, i, size) {
if (!Array.isArray(arr) || typeof i !== 'number' || typeof size !== 'number') {
throw TypeError('Illegal Input')
}
let left = i*2 + 1, right = i*2 + 2, max = i
if (left < size && arr[left] > arr[max]) {
max = left
}
if (right < size && arr[right] > arr[max]) {
max = right
}
if (max !== i) {
[arr[i], arr[max]] = [arr[max], arr[i]]
max <= Math.floor(size/2)-1 && heapAdjust(arr, max, size)
}
}
归并排序
归并排序是一类不同的排序方法
基本思想:将两个已经有序的子序列合并为一个有序的序列的时间复杂度为O(m+n),此为二路归并,相应的还可以有三路归并等。
思路:自顶向下的设计,将待排序序列分割成两个子序列,再对这两个子序列分别进行归并排序,最后,将这两个子序列合并为一个,需要用到递归,递归的终止条件是分割的子序列的长度为1,那么长度为1的序列必定是有序的。
归并排序是稳定的排序算法,可以保证大小相等的元素排序前后的相对位置不变。归并排序需要额外O(n)的存储空间,用于将两个有序子序列合并为一个序列,其时间复杂度为O(nlogn)
function mergeSort(arr) {
if (!Array.isArray(arr) || arr.length <= 1) {
return arr
}
MSort(arr, 0, arr.length-1)
return arr
}
function MSort(arr, left, right) {
if (left === right) {
return
}
let mid = Math.floor((left+right)/2)
MSort(arr, left, mid)
MSort(arr, mid+1, right)
merge(arr, left, mid, right)
}
function merge(arr, left, mid, right) {
let i, j, _arr = []
for (i = left, j = mid+1; i <= mid && j <= right; ) {
if (arr[i] <= arr[j]) {
_arr.push(arr[i++]);
}else {
_arr.push(arr[j++]);
}
}
while (i <= mid) { _arr.push(arr[i++]) }
while (j <= right) { _arr.push(arr[j++]) }
_arr.forEach((el, index) => arr[left+index] = el)
}
基数排序
最后说一下基数排序,在实际问题当中,对于两个数据的比较并非只是简单地比较大小,可能需要根据多个关键字进行比较,比如对于扑克牌地比较,就可以有两个关键字:面值和花色,可能需要先比较面值,面值相同的情况下再比较花色,可以把面值看做高位关键字,花色看作低位关键字。对于这种多关键字排序有两种做法:最高位优先和最低位优化。最高位优先需要将序列逐层分割为若干子序列,再对若干子序列分别进行排序;最低位优先不必分割子序列,对每个关键字都是整个序列参与排序,因为高位关键字在低位关键字之后排序。多关键字排序对于具体的排序算法并不要求。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后收集;以此类推,直到最高位。
此外还有计数排序和桶排序,计数排序需要与关键字大小相关的额外存储空间,这几种排序算法很少被使用。