排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:插入排序、冒泡排序、选择排序、希尔排序、归并排序、快速排序、堆排序、基数排序等。
各种排序的稳定性,时间复杂度、空间复杂度、稳定性总结如下图:
一、插入排序
插入排序示意图
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序算法是稳定的,其空间复杂度为O(1), 时间复杂度为O(n2)。
最差情况:反序,需要移动n*(n-1)/2个元素 ,O(n2)。
最好情况:正序,不需要移动元素,O(n)。
算法步骤:
1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
代码示例:(java)
public int[] insertSort(int[] array, int N) {
for(int i=1; i<N; i++) {
int temp = array[i];
int j=i;
while(j>0 && array[j-1] > temp) {
array[j] = array[j-1];
j--;
}
array[j] = temp;
}
return array;
}
二、冒泡排序
冒泡排序示意图
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
该算法也是稳定的,其空间复杂度为O(1), 时间复杂度为O(n2)。
最差情况:反序,需要移动n*(n-1)/2个元素 ,O(n2)。
最好情况:正序,不需要移动元素,O(n)。
算法步骤:
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码示例:
public int[] bubbleSort(int[] array, int N) {
int temp = 0;
for(int i=N; i>0; i--) {
for(int j=0; j<i; j++) {
if(array[j] > array[j+1]){
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
return array;
}
三、选择排序
选择排序示意图
选择排序(Selection sort)也是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。 选择排序是不稳定的排序方法(比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
选择排序算法的时间复杂度为O(n2), 最好最坏都是一样。
算法步骤:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
代码示例:
public int[] selectSort(int[] array, int N) {
int temp = 0;
int index = 0;
for(int i=0; i<N; i++) {
index = i;
for(int j=i+1; j<N; j++) {
if(array[j] < array[index]) {
index = j;
}
}
temp = array[i];
array[i] = array[index];
array[index] = temp;
}
return array;
}
四、 希尔排序
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
希尔排序的时间复杂度为O(n^1.25)。
以n=10的一个数组49, 38, 65, 97, 26, 13, 27, 49, 55, 4为例
第一次 gap = 10 / 2 = 5
49 38 65 97 26 13 27 49 55 4
1A 1B
2A 2B
3A 3B
4A 4B
5A 5B
1A,1B,2A,2B等为分组标记,数字相同的表示在同一组,大写字母表示是该组的第几个元素, 每次对同一组的数据进行直接插入排序。即分成了五组(49, 13) (38, 27) (65, 49) (97, 55) (26, 4)这样每组排序后就变成了(13, 49) (27, 38) (49, 65) (55, 97) (4, 26),下同。
第二次 gap = 5 / 2 = 2
排序后
13 27 49 55 4 49 38 65 97 26
1A 1B 1C 1D 1E
2A 2B 2C 2D 2E
第三次 gap = 2 / 2 = 1
4 26 13 27 38 49 49 55 97 65
1A 1B 1C 1D 1E 1F 1G 1H 1I 1J
第四次 gap = 1 / 2 = 0 排序完成得到数组:
4 13 26 27 38 49 49 55 65 97
代码示例:
public int[] shellSort(int[] array, int N) {
int temp = 0;
for(int gap=N/2; gap>0; gap/=2) {
for(int i=gap; i<N; i++) {
if(array[i] < array[i-gap]) {
temp = array[i];
int j = i;
while(j>0 && array[j-gap]>temp) {
array[j] = array[j-gap];
j = j-gap;
}
array[j] = temp;
}
}
}
return array;
}
五、归并排序
归并排序示意图
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序比较占用内存,但却是一种效率高且稳定的算法。
算法步骤:
public static void mergeSort(int[] array, int first, int last) {
int[] temp = new int[last-first+1];
if(first < last) {
int mid = (first+last)/2;
mergeSort(array, first, mid);
mergeSort(array, mid+1, last);
mergeArray(array, first, mid, last);
}
}
public static void mergeArray(int[] array, int first, int mid, int last) {
int[] temparray = new int[last-first+1];
int i = first;
int j = mid + 1;
int k = 0;
while(i<=mid && j<=last) {
if(array[i] > array[j])
temparray[k++] = array[j++];
else
temparray[k++] = array[i++];
}
while(i<=mid) {
temparray[k++] = array[i++];
}
while(j<=last) {
temparray[k++] = array[j++];
}
for(i=0; i<k; i++) {
array[first+1] = temparray[i];
}
}
六、快速排序
快速排序示意图
快速排序(Quicksort)是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
算法步骤:
1 从数列中挑出一个元素,称为 “基准”(pivot),
2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
代码示例:
public static void quickSort(int[] array, int begin, int end) {
if(begin < end) {
int i = begin;
int j = end;
int baseValue = array[begin];
while(i < j) {
while(i<j && array[j] >= baseValue) {
j--;
}
if(i < j){
array[i++] = array[j];
}
while(i<j && array[i] < baseValue) {
i++;
}
if(i < j) {
array[j--] = array[i];
}
}
array[i] = baseValue;
quickSort(array, begin, i-1);
quickSort(array, i+1, end);
}
}
七、堆排序
堆排序是利用堆的性质进行的一种选择排序。下面先讨论一下堆。
1.堆
堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
数组与堆的结构
2.堆排序的思想
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。
其基本思想为(大顶堆):
1)将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区;
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n];
3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
操作过程如下:
1)初始化堆:将R[1..n]构造为堆(大定堆——R[1]为最大值, 或者小顶堆——R[1]为最小值);
2)将当前无序区的堆顶元素R[1]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。
因此对于堆排序,最重要的两个操作就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。
3. 代码示例
public static int[] heapAdjust(int[] array, int size) {
int temp;
for(int subSize=size; subSize>1; subSize--){
for(int i=subSize/2-1; i>=0; i--) {
int l = 2*i+1;
int r = 2*i+2;
int max = l;
if(r<subSize && array[r]>array[l]) {
max = r;
}
if(array[max] > array[i]) {
temp = array[i];
array[i] = array[max];
array[max] = temp;
}
}
temp = array[0];
array[0] = array[subSize-1];
array[subSize-1] = temp;
}
return array;
}
八、基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
算法思想:
(1)假设有欲排数据序列如下所示:
73 22 93 43 55 14 28 65 39 81
首先根据个位数的数值,在遍历数据时将它们各自分配到编号0至9的桶(个位数值与桶号一一对应)中。
分配结果(逻辑想象)如下图所示:
分配结束后。接下来将所有桶中所盛数据按照桶号由小到大(桶中由顶至底)依次重新收集串起来,得到如下仍然无序的数据序列:
81 22 73 93 43 14 55 65 28 39
接着,再进行一次分配,这次根据十位数值来分配(原理同上),分配结果(逻辑想象)如下图所示:
分配结束后。接下来再将所有桶中所盛的数据(原理同上)依次重新收集串接起来,得到如下的数据序列:
14 22 28 39 43 55 65 73 81 93
观察可以看到,此时原无序数据序列已经排序完毕。如果排序的数据序列有三位数以上的数据,则重复进行以上的动作直至最高位数为止。
基于两种不同的排序顺序,我们将基数排序分为LSD(Least significant digital)或MSD(Most significant digital),
LSD的排序方式由数值的最右边(低位)开始,而MSD则相反,由数值的最左边(高位)开始。
MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
代码示例:
LSD
public static int[] lsdIndexSort(int[] array) {
int length = array.length;
int[][] map = new int[10][length];
map[0][0] = 0;
int index = 10;
int temp = 0;
boolean runstate = true;
while(runstate) {
runstate = false;
int i;
for(i=0; i<10; i++) {
map[i][0] = 0;
}
for(i=0; i<length; i++) {
if((array[i] / index) > 0){
runstate = true;
}
temp = (array[i]%index) / (index/10);
map[temp][0]++;
map[temp][map[temp][0]] = array[i];
}
int k=0;
for(i=0; i<10; i++) {
for(int j=1; j<=map[i][0]; j++) {
array[k] = map[i][j];
k++;
}
}
index = index*10;
}
return array;
}