关于排序算法的稳定性
假定在带排序的记录序列中,存在多个具有相同的关键字的记录。经过排序后,这些记录的相对次序保持不变,则称这种排序算法时稳定的;否则称为不稳定的。
排序算法的稳定性不是由排序方式决定的,而是由具体算法决定的——不稳定的算法和稳定的算法在某种条件下可以互相转换。
排序方式 | 空间复杂度 | 时间复杂度 | 使用场景 | 稳定性 |
---|---|---|---|---|
1. 冒泡排序 | O(1) | 最好:O(n) 最坏:O(n2) 平均:O(n2) | 数据量小于1000 | 稳定 |
2. 选择排序 | O(1) | 最好:O(n2) 最坏:O(n2) 平均:O(n2) | 数据量小于1000 | 不稳定 |
3. 插入排序 | O(1) | 最好:O(n) 最坏:O(n2) 平均:O(n2) | 数据量小于1000 | 稳定 |
4. 快速排序 | 最好:O(logn) 最坏:O(n) | 最好:O(nlog2n) 最坏:O(n2) 平均:O(nlog2n) | 数据量大于1000 | 不稳定 |
一、 冒泡排序
1. 算法原理(升序)
- 内循环比较相邻的元素。如果第一个比第二个大,就交换它们;否则,保持不变。
- 每一次外循环让内循环对每一对相邻元素做同样的工作,从开始第一对到结尾(不是数组的结尾,而是当前剩余数的结尾)的最后一对。这样,总能将当前剩余数中最大的数交换到数组的末尾。
- 第 i 次外循环确定了 i 个最大的数,因此内循环只需要比较 n - i - 1 对;而对于外循环,需要确定出 n-1 个最大数,因此外循环为 n - 1。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2. 算法性能
(1)时间复杂度 若排序序列的初始状态刚好是正序的,则一趟扫描即可完成排序。此时,比较次数 C 和 移动次数 M 均达到最小值:Cmin = n-1,Mmin = 0。所以,冒泡排序最好的时间复杂度为 O(n)。
若排序序列的初始状态刚好是反序的,则需要 n-1 趟排序。每趟排序要进行 n-1 次比较,且每次比较都必须移动三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:Cmax = n(n-1)/2 = O(n2),Mmax = 3n(n-1)/2 = O(n2)。所以,冒泡排序最坏的时间复杂度为 O(n2)。
综上,冒泡排序总的平均时间复杂度为 O(n2)。
(2)算法稳定性
冒泡排序就是把小的元素往前调或者把大的元素往后调——比较和交换发生在相邻的两个元素之间。所以,即使如果两个相邻的元素相等,也不会发生交换;而如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,也不会发生交换——相同元素的前后顺序没有改变,所以冒泡排序是一种稳定排序算法。
3. 算法描述(JavaScript)
function bubbleSort(arr){
var len = arr.length;
// 每一次外循环确定1个最大数,确定 n-1 个最大数即排序成功
for(var i = 0; i < len - 1; i++){
// 为了确定出一个最大数,需要对未排序序列进行两两比较
// n 个未排序序列只需要比较 n-1 对即可确定出一个最大数
// 因为每一次外循环都确定出了一个最大数并把这个最大数按顺序放置在最后构成已排序序列,因此未排序序列的个数是动态的
// 外循环第 i 次即确定 i 个最大的数,因此未排序序列的个数为 n-i,则内循环只需要比较 n - i - 1 对即可
for(var j = 0; j < len - i -1; j++){
if(arr[j] > arr[j+1]){
var temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
二、 选择排序
1. 算法原理(升序)
- 首先在未排序序列中通过比较找到最小元素及其下标,判断:如果该元素下标不是第一个元素的下标,则该元素和第一个元素交换;否则,保持不变。
- 再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕
2. 算法性能
(1)时间复杂度 交换次数:0 ~ (n-1) => O(n)
比较次数:n(n-1) / 2
赋值次数:0 ~ 3(n-1) => O(n2)
选择排序的交换次数比冒泡排序少多了,由于交换所需 CPU 时间比比较所需的 CPU 时间多,n 值较小时,选择排序比冒泡排序块。
选择排序是给每个位置选择当前元素最小的。以序列 5 8 5 2 9 为例,最小元素为 2 ,而在该趟选择中,当前元素为 5——更小元素(2)出现在一个和当前元素(5)相等的元素后面,交换后稳定性就被破坏了。
3. 算法描述(JavaScript)
function selectionSort(arr){
var len = arr.length;
// 每一次外循环确定1个最小数,确定 n-1 个最小数即排序成功
for(var i = 0; i < len - 1; i++){
var minIndex = i;
// 内循环找到未排序序列的最小值下标
// 每一次外循环确定一个最小数并把这个最小数按顺序放置在最前构成已排序序列,因此未排序序列的个数是动态的
// 外循环第 i 次即确定 i 个最小的数,因此内循环只需要从第 i+1 个数开始进行比较即可
for(j = i + 1; j < len; j++){
if(arr[j] < arr[minIndex]){
minIndex = j;
}
}
// 找到最小值的下标后,如果该下标不是未排序序列的第一个元素的下标,则交换
if(i != minIndex){
var temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
三、 插入排序
1. 算法原理(升序)
- 将待排序序列的第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(如果两个元素相等,则将待插入元素插入到相等元素的后面)。
2. 算法性能
(1)时间复杂度 最优情况:待排序数组有序,只需当前数跟前一个数比较一下即可,共比较 n-1 次 => O(n)
最坏情况:待排序数组逆序,共比较 1+2+3+...+n-1 = n(n-1) / 2 次 => O(n2)
平均来说:A[1...j] 中的一半元素小于 A[j],一半元素大于 A[j]。插入排序在平均情况下时间复杂度与最坏情况下是一样的。
关键词相同的数据元素将保持原有位置不变,所以该算法是稳定的。
3. 算法描述(JavaScript)
function insertionSort(arr){
var len = arr.length;
// 第一个元素(下标为0)看做一个已排序序列
// 外循环依次扫描未排序序列
for(var i = 1; i < len; i++){
// 内循环将当前扫描值插入到适当位置,此时的算法类似于冒泡排序(升序)
// 冒泡排序:内循环比较相邻的元素。如果第一个比第二个大,交换。
// 插入排序:内循环比较相邻的元素。如果前一个比后一个大,交换
for(var j = i - 1; j >= 0; j--){
if(arr[j] > arr[j+1]){
var temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}else{
break;
}
}
}
return arr;
}
四、 快速排序
1. 算法原理(升序)
- 从未排序序列中挑出一个元素,称为基准(pivot)
- 分区(partition)操作:所有比基准小的元素摆放在基准前面,比基准大的元素摆放在基准后面
- 将基准前后的元素再次独立地递归地(recursive)把小于或大于基准值的元素进行排序
2. 算法性能
(1)时间复杂度 最优情况:每次划分所选择的中间数恰好将当前序列几乎等分,则经过 log2n 趟划分,便可得到长度为 1 的子表。这样,整个算法的时间复杂度为 O(nlog2n)
最坏情况:每次划分所选择的中间数是当前序列中的最值,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度减一。这样,长度为 n 的数据表的快速排序需要经过 n 趟划分,使得整个排序算法的时间复杂度为 O(n2)
可以证明,快速排序的平均时间复杂度也是O(nlog2n)。因此,快速排序被认为是目前最好的一种内部排序方法。
尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。因此,
最好情况:栈的最大深度为 log2(n+1)
最坏情况:栈的最大深度为 n 综上,快速排序的空间复杂度为 O(log2n)
3. 算法描述(JavaScript)
function quickSort(arr){
var len = arr.length;
var pivot = arr[0];
var left = [];
var right = [];
if(len <= 1){
return arr;
}
for(var i = 1; i < len; i++){
if(arr[i] < pivot){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
return quickSort(left).concat(pivot, quickSort(right));
}