疯狂的小鸡

常用排序算法

字数统计: 3.1k阅读时长: 11 min
2018/11/08 Share

常见排序算法分类

十种常见排序算法一般分为以下两种:

(1) 非线性时间比较类排序:

  • 交换类排序(快速排序和冒泡排序)
  • 插入类排序(简单插入排序和希尔排序)
  • 选择类排序(简单选择排序和堆排序)
  • 归并排序(通常为二路归并)

(2) 线性时间非比较类排序:计数排序、基数排序和桶排序。

排序算法分类

总结:
(1)在比较类排序中,归并排序号称最快,其次是快速排序和堆排序,两者不相伯仲,但是有一点需要注意,数据初始排序状态对堆排序不会产生太大的影响,而快速排序却恰恰相反。
(2)线性时间非比较类排序一般要优于非线性时间比较类排序,但前者对待排序元素的要求较为严格,比如计数排序要求待排序数的最大值不能太大,桶排序要求元素按照hash分桶后桶内元素的数量要均匀。线性时间非比较类排序的典型特点是以空间换时间。

交换类排序

交换排序的基本方法是:两两比较待排序记录的排序码,交换不满足顺序要求的偶对,直到全部满足位置。常见的冒泡排序和快速排序就属于交换类排序。

冒泡排序

特点:稳定排序(stable sort)、原地排序(In-place sort)

最坏运行时间:O(n^2)
最佳运行时间:O(n^2)(如果待排序数据序列为正序,则一趟冒泡就可完成排序)

原理:两层循环,里循环从未排序的数中两两相邻比较并交换,比较每一轮里循环下来找出剩余未排序数的中的最大(小)数并”冒泡”至数列的顶端。

快速排序

特性:不稳定排序(unstable sort)、原地排序(In-place sort)。

最坏运行时间:O(n^2) 每趟排序结束后,元素全部属于某一边子文件,例如原本有序的序列。解决办法是使用快排前先shuffle序列。
最佳运行时间:O(nlgn) 每趟排序结束后,每次划分使两个子文件的长度大致相等

原理:
(1)从待排序的n个记录中任意选取一个记录(通常选取第一个记录)为分区标准;
(2)把所有小于该排序列的记录移动到左边,把所有大于该排序码的记录移动到右边,中间放所选记录,称之为第一趟排序;
(3)然后对前后两个子序列分别重复上述过程,直到所有记录都排好序。

快排代码一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void quickSort(int[] arr){
quickSort(arr, 0, arr.length-1);
}

private static void quickSort(int[] arr, int low, int high){
if (low < high){
int pivot=partition(arr, low, high); //将数组分为两部分
quickSort(arr, low, pivot-1); //递归排序左子数组
quickSort(arr, pivot+1, high); //递归排序右子数组
}
}
private static int partition(int[] arr, int low, int high){
int pivot = arr[low]; //枢轴记录
while (low<high){
while (low<high && arr[high]>=pivot) --high;
arr[low]=arr[high]; //交换比枢轴小的记录到左端
while (low<high && arr[low]<=pivot) ++low;
arr[high] = arr[low]; //交换比枢轴小的记录到右端
}
//扫描完成,枢轴到位
arr[low] = pivot;
//返回的是枢轴的位置
return low;
}

插入类排序

插入排序的基本方法是:每步将一个待排序的记录,按其排序码大小,插到前面已经排序的文件中的适当位置,直到全部插入完为止。

直接插入排序

特点:稳定排序(stable sort)、原地排序(In-place sort)。插入排序比较适合用于“少量元素的数组”。

最优复杂度:当输入数组就是排好序的时候,复杂度为O(n),而快速排序在这种情况下会产生O(n^2)的复杂度。
最差复杂度:当输入数组为倒序时,复杂度为O(n^2)

原理:两层循环,每一轮里循环将一个元素插入到前面已经有序的元素中

希尔排序

特点:不稳定排序(stable sort)、原地排序(In-place sort)。

时间复杂度:希尔算法的性能与所选取的增量(分组长度)序列有很大关系。只对特定的待排序记录序列,可以准确地估算比较次数和移动次数。想要弄清比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。

原理:
Shell排序法是对相邻指定距离(称为增量)的元素进行比较,并不断把增量缩小至1,完成排序。

Shell排序开始时增量较大,分组较多,每组的记录数目较少,故在各组内采用直接插入排序较快,后来增量逐渐缩小,分组数减少,各组的记录数增多,但由于已经按分组排序,文件叫接近于有序状态,所以新的一趟排序过程较快。因此Shell排序在效率上比直接插入排序有较大的改进。

在直接插入排序的基础上,将直接插入排序中的1全部改变成增量d即可,因为Shell排序最后一轮的增量d就为1。

增量选择:比较常用的是每次取半, Knuth增量序列 h1=1,hi=3∗hi-1+1, Hibbard增量序列 h1=1,hi=2∗hi-1+1

希尔排序一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static int[]  ShellSort(int[] arr){
if(arr == null || arr.length <= 1){
return arr;
}
//希尔排序 升序
for (int d = arr.length / 2; d > 0; d /= 2){ //d:增量 7 3 1
for (int i = d; i < arr.length; i++){
//i:代表即将插入的元素角标,作为每一组比较数据的最后一个元素角标
//j:代表与i同一组的数组元素角标
for (int j = i-d; j>=0; j-=d){ //在此处-d 为了避免下面数组角标越界
if (arr[j] > arr[j + d]) {// j+d 代表即将插入的元素所在的角标
//符合条件,插入元素(交换位置)
int temp = arr[j];
arr[j] = arr[j + d];
arr[j + d] = temp;
}
}
}
}
return arr;
}

选择类排序

选择类排序的基本方法是:每步从待排序记录中选出排序码最小(大)的记录,顺序放在已排序的记录序列的后面,直到全部排完。

简单选择排序

特性:原地排序(In-place sort),不稳定排序(unstable sort)。

思想:每次找一个最小值。

最好情况时间:O(n^2)。
最坏情况时间:O(n^2)。

原理:从所有记录中选出最小的一个数据元素与第一个位置的记录交换;然后在剩下的记录当中再找最小的与第二个位置的记录交换,循环到只剩下最后一个数据元素为止。

堆排序

特性:不稳定排序(unstable sort)、原地排序(In-place sort)。

最优时间:O(nlgn)
最差时间:O(nlgn)

原理:堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得在当前无序区中选取最大(或最小)关键字的记录变得简单。

复合排序

二路归并排序

特点:稳定排序(stable sort)、Out-place sort

思想:运用分治法思想解决排序问题。

最坏情况运行时间:O(nlgn)
最佳运行时间:O(nlgn)

原理:
(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
(4)重复步骤3直到某一指针达到序列尾
(5)将另一序列剩下的所有元素直接复制到合并序列尾
(6)在此之前将子序列递归归并。子序列元素数量较少时可采用其他排序方法。

桶排序

特性:out-place sort、stable sort。

最坏情况运行时间:当分布不均匀时,全部元素都分到一个桶中,则O(n^2)
最好情况运行时间:O(n)

思想:
桶排序的基本思想是将一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法。也是典型的divide-and-conquer分而治之的策略。

桶排序

线性时间非比较类排序

计数排序

计数排序是一个非基于比较的排序算法, 它的优势在于在对于较小范围内的整数排序。它的复杂度为Ο(n+k),快于任何比较排序算法,缺点就是非常消耗空间。很明显,如果而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序,比如堆排序和归并排序和快速排序。

k为待排序列所在区间中独立元素的个数。举例来说,若带排序列为[5,3,1,0,2],其所在区间为[0,5],其中独立元素有0,1,2,3,4, k=5

特性:stable sort、out-place sort。

最坏情况运行时间:O(n+k)
最好情况运行时间:O(n+k)
空间复杂度:Ο(k)。

原理: 基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,在代码中作适当的修改即可。

实现:
(1)根据k构造count序列,用于存放待排序列source中元素数量的统计。
(2)根据source构造result序列,用于存放排好序的元素
(3)统计source中各元素数量,存入count
(4)修改count,使其统计数量依次叠加
(5)遍历source,根据count中的信息填充result

计数排序一例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int[] countSort(int[] source, int k, int min){
int[] count = new int[k]; //构建count数组用于统计各元素个数
int[] result = new int[source.length]; //result数组用于存放最终结果

for (int i=0; i<source.length; i++ ) {
//source中值为min的数量保存在count[0], 值为min+1的数量保存在count[1],以此类推
count[source[i]-min]++;
}
//修改count使其变成计数和累加形式
int sum = 0;
for (int i=0; i<count.length; i++) {
sum += count[i];
count[i] = sum;
}
//遍历source,根据count中的信息填充result
for (int i=0; i<source.length; i++ ) {
result[count[i]-1] = source[i];
}
return result;
}

基数排序

基数排序(以整形为例),将整形10进制按每位拆分,然后从低位到高位依次比较各个位。主要分为两个过程:
  (1)分配,先从个位开始,根据位值(0-9)分别放到0~9号桶中(比如64,个位为4,则放入4号桶中);
  (2)收集,再将放置在0~9号桶中的数据按顺序放到数组中;
  重复(1)(2)过程,从个位到最高位(比如32位无符号整形最大数4294967296,最高位为第10位)。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
以【520 350 72 383 15 442 352 86 158 352】序列为例,排序过程描述如下:
基数排序
平均时间复杂度:O(dn)(d为基数,此例即表示整形的最高位数)。
空间复杂度:O(10n) (10表示0~9,用于存储临时的序列) 。
稳定性:稳定。

CATALOG
  1. 1. 常见排序算法分类
  2. 2. 交换类排序
    1. 2.1. 冒泡排序
    2. 2.2. 快速排序
  3. 3. 插入类排序
    1. 3.1. 直接插入排序
    2. 3.2. 希尔排序
  4. 4. 选择类排序
    1. 4.1. 简单选择排序
    2. 4.2. 堆排序
  5. 5. 复合排序
    1. 5.1. 二路归并排序
    2. 5.2. 桶排序
  6. 6. 线性时间非比较类排序
    1. 6.1. 计数排序
    2. 6.2. 基数排序