常见排序算法
前言
概念
维基百科上定义算法:一系列有限的、清晰定义的、可实现的计算机指令,并用于处理一类问题或进行计算。
算法的五大特点
- 输入:有零个或多个输入。
- 输出: 有一个或等多个输出。
- 有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成。
- 确定性:每一步都有明确的意义,不存在歧义。
- 可行性:每一个步骤都可以分解成基本的步骤,在有限时间内实现。
一、排序算法分类
排序算法分成两大类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
- 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1oYFp9A-1658991216771)(./images/排序算法分类.png)]
二、时间复杂度
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(nlogn) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n^2) | O(n) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
三、常见排序算法代码实现
1、插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
function insertSort(arr) {
let len = arr.length
let prevIndex, current
for(let i = 1; i < len; i++) {
prevIndex = i - 1
current = arr[i]
while(prevIndex >=0 && arr[prevIndex] > current) {
arr[prevIndex + 1] = arr[prevIndex]
prevIndex--
}
arr[prevIndex + 1] = current
}
return arr
}
分析:假设数组中有n个数字要排序,先从下标为1的位置开始往前比较,把当前的元素插到合适的位置大致需要比较一次,以此类推…最后到n-1的位置需要比较n-1次。比较替换的次数首尾相加求和:
∑
i
=
1
n
−
1
i
=
n
∗
n
2
\sum_{i=1}^{n -1}i = \frac{n * n} {2}
i=1∑n−1i=2n∗n
一般时间复杂度的计算忽略系数,所以插入排序的时间复杂度记为O(n^2);最坏的情况是每次都要比较到下标为零的元素,那么插入排序的比较次数就是n^2 /2
;最好的情况是循环一遍,只比较一次,这样比较的次数是n-1
。
2、希尔排序
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
let arr = [3,14,2,35,22,46,11,55,6,100,77,46,23,66]
function shellSort(arr) {
if (arr == null || arr.length < 2) return arr;
let n = arr.length;
// 对每组间隔为 h的分组进行排序,刚开始 h = n / 2;
for (let h = Math.floor(n / 2); h > 0; h = Math.floor(h / 2)) {
//对各个局部分组进行插入排序
insertSort(arr,h)
}
return arr;
}
function insertSort(arr, h) {
let len = arr.length
let prevIndex, current
for(let i = 1; i < len; i++) {
prevIndex = i - h
current = arr[i]
while(prevIndex >=0 && arr[prevIndex] > current) {
arr[prevIndex + h] = arr[prevIndex]
prevIndex-=h
}
arr[prevIndex + h] = current
}
return arr
}
shellSort(arr)
分析: 希尔排序时间复杂度相对比较复杂点
3、选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
function selectionSort(arr) {
let len = arr.length
let minIndex, temp
for (let i = 0; i < len; i++) {
minIndex = i
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j
}
}
temp = arr[i]
arr[i] = arr[minIndex]
arr[minIndex] = temp
}
return arr
}
分析:每一轮比较都保存剩余元素中最小的下标,一轮结束后只进行一次交换,省却了每次比较完之后的元素交换
∑
i
=
1
n
−
1
i
=
n
∗
n
2
\sum_{i=1}^{n -1}i = \frac{n * n} {2}
i=1∑n−1i=2n∗n
4、冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
function bubbleSort(arr) {
let len = arr.length;
for(let i = 0; i < len - 1; i++) {
for(let j = 0; j < len - 1 - i; j++) {
if(arr[j] > arr[j+1]) { // 相邻元素两两对比
let temp = arr[j+1]; // 元素交换
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
分析:冒泡排序和插入排序类似,就不做解释。
5、快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
function quickSort(arr, left, right) {
let len = arr.length,
partitionIndex,
left =typeof left !='number'? 0 : left,
right =typeof right !='number'? len - 1 : right;
if(left < right) {
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex-1);
quickSort(arr, partitionIndex+1, right);
}
return arr;
}
function partition(arr, left ,right) { // 分区操作
let pivot = left, // 设定基准值(pivot)
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;
}
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
分析:以一个元素为基准,比完一轮,基准更换下一个元素,直到n-1个元素,比较大小的次数(n-1,n-2,n-3…1)
∑ i = 1 n − 1 i = n ∗ n 2 \sum_{i=1}^{n -1}i = \frac{n * n} {2} i=1∑n−1i=2n∗n
6、计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
function countingSort(arr, maxValue) {
let bucket =new Array(maxValue + 1),
sortedIndex = 0,
arrLen = arr.length,
bucketLen = maxValue + 1;
// 计算元素个数
for(let i = 0; i < arrLen; i++) {
if(!bucket[arr[i]]) {
bucket[arr[i]] = 0;
}
bucket[arr[i]]++;
}
for(let j = 0; j < bucketLen; j++) {
while(bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
分析:计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
7、基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
let counter = [];
function radixSort(arr, maxDigit) {
let mod = 10;
let dev = 1;
for(let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for(let j = 0; j < arr.length; j++) {
let bucket = parseInt((arr[j] % mod) / dev);
if(counter[bucket]==null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
let pos = 0;
for(let j = 0; j < counter.length; j++) {
let value =null;
if(counter[j]!=null) {
while((value = counter[j].shift()) !=null) {
arr[pos++] = value;
}
}
}
}
return arr;
}
分析:假设最大数的位数有k位,每次分类n次,那么时间复杂度是O(n*k)
参考博客:
https://www.cnblogs.com/onepixel/articles/7674659.html
https://www.cnblogs.com/chengxiao/p/6104371.html