排序算法总结(JAVA)
排序算法总结
排序算法是《数据结构与算法》中最基本的算法之一。
本文讲解了共11种排序算法,包括冒泡排序的改进版,堆排序的另类实现等等。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。大致可以用一张图概括:
点击以下图片查看大图:
关于时间复杂度
平方阶 (O(n2)) 排序: 各类简单排序:直接插入、直接选择和冒泡排序。
线性对数阶 (O(nlog2n)) 排序: 快速排序、堆排序和归并排序;
O(n1+§)) 排序:§ 是介于 0 和 1 之间的常数。 希尔排序
线性阶 (O(n)) 排序 :基数排序,此外还有桶、箱排序。
关于稳定性
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
名词解释:
n:数据规模
k:"桶"的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
交换位置的函数swap()
下列算法中多用到swap()函数执行位置置换功能。
private static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
基本排序算法
- 冒泡排序
- 插入排序
- 选择排序
1. 冒泡排序
冒泡排序 是一种简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
算法描述
具体算法描述:
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码实现
public static int[] BubbleSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++)
for (int j = 0; j < array.length-1-i; j++)
if (array[j+1] < array[j]) {
swap(array, j, j+1);
}
return array;
}
算法分析
时间复杂度
若文件的初始状态是正序的,一趟扫描即可完成排序。对应时间复杂度为O(n);若初始文件是反序的,需要进行 n-1 趟排序。每趟排序要进行 n-i 次关键字的比较(1 ≤ i ≤ n-1)。对应时间复杂度为O(n2)。
因此,
最好情况的时间复杂度为O(n),
最坏情况的时间复杂度为O(n2),
平均时间复杂度为O(n2)。
空间复杂度
O(1)
稳定性
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
算法改进
改进部分:如果在第二层for循环中,如果不发生交换,则代表数据已经排好序了,不需要继续排序。
bubbleSort2()并不是一个多么令人欣喜的改进,但是基于bubbleSort2()的梳排序,却值得研究一下。
——《C++数据结构与算法》
public static int[] BubbleSort2(int[] array) {
int n = array.length;
boolean flag = true;
for (int i = 0; i < n-1 && flag; i++) {
int j = n-1;
for (flag = false; j > i; j--) {
if(array[j-1] > array[j]){
flag = true;
swap(array, j-1, j);
}
}
}
return array;
}
2. 插入排序
插入排序 插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素);在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。
算法描述
将n个元素的数列分为已有序和无序两个部分,如下所示:
{{a1},{a2,a3,a4,…,an}}
{{a1⑴,a2⑴},{a3⑴,a4⑴ …,an⑴}}
…
{{a1(n-1),a2(n-1) ,…},{an(n-1)}}
每次处理就是将无序数列的第一个元素与有序数列的元素从后往前逐个进行比较,找出插入位置,将该元素插入到有序数列的合适位置中。
代码实现
public static int[] InsertionSort(int[] array) {
int temp;
if (array.length == 0)
return array;
for(int j = 1; j < array.length; j++) {
temp = array[j];
int i = j-1;
while(i >= 0 && array[i] > temp) {
array[i+1] = array[i];
i--;
}
array[i+1] = temp;
}
return array;
}
算法分析
时间复杂度
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。
因此,
最好情况的时间复杂度为O(n),
最坏情况的时间复杂度为O(n2),
平均时间复杂度为O(n2)。
空间复杂度
O(1)
稳定性
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
3. 选择排序
选择排序 是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
算法描述
n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果:
①初始状态:无序区为R[1…n],有序区为空。
②第1趟排序
在无序区R[1…n]中选出关键字最小的记录R[k],将它与无序区的第1个记录R[1]交换,使R[1…1]和R[2…n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
……
③第i趟排序
第i趟排序开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
代码实现
public static int[] SelectionSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex])
minIndex = j;
}
swap(array, minIndex, i);
}
return array;
}
算法分析
时间复杂度
选择排序的交换操作介于 0 和 (n - 1) 次之间。选择排序的比较操作为 n (n - 1)/ 2 次之间。选择排序的赋值操作介于 0 和 3 (n - 1)次之间。
比较次数O(n2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1+(n-2+…+1=n*(n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况是,交换n-1次,逆序交换n/2次。
因此,
最好情况的时间复杂度为O(n2),
最坏情况的时间复杂度为O(n2),
平均时间复杂度为O(n2)。
空间复杂度
O(1)
稳定性
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。
举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
高效排序算法
- 快速排序
- 希尔排序
- 梳排序
- 堆排序
1. 快速排序
快速排序 是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
(1)从数列中挑出一个元素,称为 “基准”(pivot);
(2)重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
(3)递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码实现
public static int[] QuickSort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end)
return null;
int smallIndex = partition(array, start, end);
if (smallIndex > start)
QuickSort(array, start, smallIndex-1);
if (smallIndex < end)
QuickSort(array, smallIndex+1, end);
return array;
}
//分区操作
private static int partition(int[] array, int start, int end) {
int pivot = (int) (start+Math.random()*(end-start+1));
int smallIndex = start - 1;
swap(array, pivot, end);
for (int i = start; i <= end; i++)
if (array[i] <= array[end]) {
smallIndex++;
if (i > smallIndex)
swap(array, i, smallIndex);
}
return smallIndex;
}
算法分析
时间复杂度
快速排序的一次划分算法从两头交替搜索,直到start和end重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)。
平均时间复杂度也是O(nlog2n)
空间复杂度
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1);但最坏的情况下,栈的最大深度为n。这样,快速排序的空间复杂度为O(log2n))。
稳定性
快速排序是不稳定的。
2. 希尔排序
希尔排序 是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
(1)插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
(2)但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
(1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
(2)按增量序列个数k,对序列进行k 趟排序;
(3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
代码实现
public static int[] ShellSort(int[] array) {
int len = array.length;
int temp, gap = len/2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
temp = array[i];
int preIndex = i-gap;
while (preIndex >= 0 && array[preIndex] > temp) {
array[preIndex+gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
return array;
}
算法分析
时间复杂度
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n2)好一些。
希尔排序的时间的时间复杂度为O( n3/2 ),希尔排序时间复杂度的下界是nlog2n。
希尔排序没有快速排序算法 O(nlog2n)快,因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。
空间复杂度
O(1)
稳定性
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
3. 梳排序
梳排序 是改良自泡沫排序和快速排序,其要旨在于消除乌龟,亦即在数组尾部的小数值,这些数值是造成泡沫排序缓慢的主因。相对地,兔子,亦即在数组前端的大数值,不影响泡沫排序的性能。
在泡沫排序中,只比较数组中相邻的二项,即比较的二项的间距(Gap)是1,梳排序提出此间距其实可大于1,改自插入排序的希尔排序同样提出相同观点。梳排序中,开始时的间距设置为数组长度,并在循环中以固定比率递减,通常递减率设置为1.3。在一次循环中,梳排序如同泡沫排序一样把数组从首到尾扫描一次,比较及交换两项,不同的是两项的间距不固定于1。如果间距递减至1,梳排序假定输入数组大致排序好,并以泡沫排序作最后检查及修正。
算法描述
假设待数组[8 4 3 7 6 5 2 1]
待排数组长度为8,而8÷1.3≈6,则比较8和2,4和1,并做交换
[8 4 3 7 6 5 2 1]
[8 4 3 7 6 5 2 1]
交换后的结果为
[2 1 3 7 6 5 8 4]
第二次循环,更新间距为6÷1.3≈4,比较2和6,1和5,3和8,7和4
[2 1 3 7 6 5 8 4]
[2 1 3 7 6 5 8 4]
[2 1 3 7 6 5 8 4]
[2 1 3 7 6 5 8 4]
只有7和4需要交换,交换后的结果为
[2 1 3 4 6 5 8 7]
第三次循环,更新距离为3,没有交换
第四次循环,更新距离为2,没有交换
第五次循环,更新距离为1,三处交换
[2 1 3 4 6 5 8 7]
[2 1 3 4 6 5 8 7]
[2 1 3 4 6 5 8 7]
三处交换后的结果为[1 2 3 4 5 6 7 8]
交换后排序结束,顺序输出即可得到[1 2 3 4 5 6 7 8]
代码实现
public static int[] CombSort(int[] array) {
int n = array.length;
int step = n;
int k;
// 第一部分
while((step /= 1.3) > 1) {
for (int i = n-1; i >= step; i--) {
k = i-step;
if(array[k] > array[i]){
swap(array,k,i);
}
}
}
// 第二部分:进行冒泡排序(使用改进后的冒泡排序算法)
array= BubbleSort2(array);
return array;
}
4. 堆排序
堆排序 是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆的操作
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
(1)最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
(2)创建最大堆(Build Max Heap):将堆中的所有数据重新排序
(3)堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
算法描述
具体算法描述:
(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,则整个排序过程完成。
代码实现
注意:这里用到了完全二叉树的部分性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
public static int[] HeapSort1(int[] array) {
//这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1
for (int i = array.length/2-1; i >= 0; i--) {
adjustHeap(array, i, array.length); //调整堆
}
// 上述逻辑,建堆结束
// 下面,开始排序逻辑
for (int j = array.length-1; j > 0; j--) {
// 元素交换,作用是去掉大顶堆
// 把大顶堆的根元素,放到数组的最后;换句话说,就是每一次的堆调整之后,都会有一个元素到达自己的最终位置
swap(array, 0, j);
// 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。
// 接下来我们需要排序的,就是已经去掉了部分元素的堆了,这也是为什么此方法放在循环里的原因
// 而这里,实质上是自上而下,自左向右进行调整的
adjustHeap(array, 0, j);
}
return array;
}
public static void adjustHeap(int[] array, int i, int length) {
// 先把当前元素取出来,因为当前元素可能要一直移动
int temp = array[i];
for (int k = 2*i+1; k < length; k = 2*k+1) { //2*i+1为左子树i的左子树(因为i是从0开始的),2*k+1为k的左子树
// 让k先指向子节点中最大的节点
if (k+1 < length && array[k] < array[k+1]) { //如果有右子树,并且右子树大于左子树
k++;
}
//如果发现结点(左右子结点)大于根结点,则进行值的交换
if (array[k] > temp) {
swap(array, i, k);
// 如果子节点更换了,那么,以子节点为根的子树会受到影响,所以,循环对子节点所在的树继续进行判断
i = k;
} else { //不用交换,直接终止循环
break;
}
}
}
算法分析
时间复杂度
最佳情况:T(n) = O(nlog2n)
最差情况:T(n) = O(nlog2n)
平均情况:T(n) = O(nlog2n)
空间复杂度
O(1)
稳定性
我们知道堆的结构是节点i的孩子为2 * i和2 * i + 1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
堆排序算法的另类实现
使用堆的元素下沉思想,在给定空间进行原地排序。
具体下沉操作:若当前节点元素比较大的一个子节点的值要小,就需要将当前节点的值与较大节点的值做交换。然后将值较大的节点作为当前节点,继续做判断。直到当前节点为叶子节为止。
public static void HeapSort2(int[] array) {
int n = array.length;
// 构造一个堆有序
for (int i = n/2; i >= 0; i--) {
sink(array, i, n-1);
}
n = n-1;
// 然后进行下沉排序
while(n>0){
swap(array, 0, n--);
sink(array, 0, n);
}
}
private static void sink(int[] array, int i, int j) {
// 满足向下移动的条件
while(2*i+1 <= j){
int k = 2*i+1;
// 从 array[k]和array[k+1]中a比较出较大的元素
if(k < j && array[k+1] > array[k]) { //k<j,j最大为n-1,所以k+1未溢出
k++;
}
if(array[k] < array[i]) {
break;
}
// 将大的元素移动到上面去
swap(array, i, k);
i = k;
}
}
牺牲空间以节约时间的高效排序算法
- 归并排序
- 基数排序
- 计数排序
- 桶排序(计数排序的升级版)
1. 归并排序
归并排序 是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并操作
归并操作(merge),也叫归并算法,指的是将两个顺序序列合并成一个顺序序列的方法。
如 设有数列{6,202,100,301,38,8,1}
初始状态:6,202,100,301,38,8,1
第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;
第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;
第三次归并后:{1,6,8,38,100,202,301},比较次数:4;
总的比较次数为:3+4+4=11;
逆序数为14;
算法描述
归并操作的工作原理如下:
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
代码实现
public static int[] MergeSort(int[] array) {
if (array.length < 2)
return array;
int mid = array.length/2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(MergeSort(left), MergeSort(right));
}
//归并操作
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length)
result[index] = right[j++];
else if (j >= right.length)
result[index] = left[i++];
else if (left[i] > right[j])
result[index] = right[j++];
else
result[index] = left[i++];
}
return result;
}
算法分析
归并排序比较占用内存,但却是一种效率高且稳定的算法。
时间复杂度
最佳情况:T(n) = O(n)
最差情况:T(n) = O(nlog2n)
平均情况:T(n) = O(nlog2n)
空间复杂度
O(n)
稳定性
归并排序算法是稳定的。
2. 基数排序
基数排序 属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。
基数排序有两种方法:
(1)MSD 从高位开始进行排序
(2)LSD 从低位开始进行排序
算法描述
实现原理:
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
具体算法描述:
(1)取得数组中的最大数,并取得位数;
(2)array为原始数组,从最低位开始取每个位组成radix数组;
(3)对radix进行计数排序(利用计数排序适用于小范围数的特点);
代码实现
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大数的位数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
算法分析
时间复杂度
设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。
空间复杂度
空间效率:需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。
稳定性
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
3. 计数排序
计数排序 是一个非基于比较的排序算法。它的优势在于在对一定范围内的整数排序时,它的时间复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(nlog2n)的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog2n), 如归并排序,堆排序)
算法描述
具体算法描述:
(1)找出待排序的数组中最大和最小的元素;
(2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
(4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
代码实现
public static int[] CountingSort(int[] array) {
if (array.length == 0) return array;
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i - bias;
bucket[i]--;
index++;
} else
i++;
}
return array;
}
算法分析
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
时间复杂度
最佳情况:T(n) = O(n+k)
最差情况:T(n) = O(n+k)
平均情况:T(n) = O(n+k)
空间复杂度
O(k)
稳定性
经计数排序,输出序列中值相同的元素之间的相对次序与他们在输入序列中的相对次序相同,换句话说,计数排序算法是一个稳定的排序算法。
4. 桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
算法描述
工作原理:
假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排
具体算法描述:
(1)人为设置一个BucketSize,作为每个桶所能放置多少个不同数值(例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,但是容量不限,即可以存放100个3);
(2)遍历输入数据,并且把数据一个一个放到对应的桶里去;
(3)对每个不是空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
(4)从不是空的桶里把排好序的数据拼接起来。
注意,如果递归使用桶排序为各个桶排序,则当桶数量为1时要手动减小BucketSize增加下一循环桶的数量,否则会陷入死循环,导致内存溢出。
代码实现
public static ArrayList<Integer> BucketSort(ArrayList<Integer> array, int bucketSize) {
if (array == null || array.size() < 2)
return array;
int max = array.get(0), min = array.get(0);
// 找到最大值最小值
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max)
max = array.get(i);
if (array.get(i) < min)
min = array.get(i);
}
int bucketCount = (max - min) / bucketSize + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
ArrayList<Integer> resultArr = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
bucketArr.add(new ArrayList<Integer>());
}
for (int i = 0; i < array.size(); i++) {
bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
}
for (int i = 0; i < bucketCount; i++) {
if (bucketSize == 1) { // 如果带排序数组中有重复数字时
for (int j = 0; j < bucketArr.get(i).size(); j++)
resultArr.add(bucketArr.get(i).get(j));
} else {
if (bucketCount == 1)
bucketSize--;
ArrayList<Integer> temp = BucketSort(bucketArr.get(i), bucketSize);
for (int j = 0; j < temp.size(); j++)
resultArr.add(temp.get(j));
}
}
return resultArr;
}
算法分析
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少,但相应的空间消耗就会增大。
时间复杂度
最佳情况:T(n) = O(n+k)
最差情况:T(n) = O(n+k)
平均情况:T(n) = O(n2)
空间复杂度
O(n+k)
稳定性
桶排序是一个稳定的排序算法。