影响排序的三个主要因素:时间复杂度
空间复杂度
稳定性
并且排序又分为内部排序和外部排序。
内部排序:待排序元素全部调入到内存中进行的排序。
外部排序:待排序的数据元素很大,需要分批次的导入内存中,需要借用外部内存或者磁盘空间。
1:插入排序
核心思想:类似与扑克牌,假设原来已经排好序的集合中只有一个元素a[0],对于新插入的元素,如果大于a[0],则将新插入的元素放在左边,否则放在右边。
比如对于已经排好的a[0],a[1],如果新插入a[2],首先比较a[1]和a[2]的大小,如果a[2]>a[1],则直接将a[2]放在a[1]的左边; 否则,再将a[2]和a[0]做比较,如果a[2]>a[0]
将a[2]放在a[0]右边,否则放在a[0]左边。依次类推。。
1 for(int i=1 ; i2 //将刚进来的数交给temp){
如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,
所以插入排序是稳定的。
时间复杂度:(1):最好情况是原来的数据元素都已经排好序,此时移动环节则不需要了,只是执行前面的遍历过程。因此时间复杂度为:O(n)
(2):最坏情况原来的数据元素反序排序,则需要执行O(n2)
(3):原来的数据元素都是随机排列。比较+移动次序为n2/4,时间复杂度为O(n2)
核心思想:把待排的数据元素分为若干个组,对每一组都进行直接插入的排序方法,然后依次减少数组的个数,继续调用直接插入排序,当最后所有的数据都在一个数组中时,则所有的元素都已经排列完毕。又称为减少增量的排序。
1 /*8 int temp=a[i];
2 * 第一层循环主要是控制分组的情况,通俗的话说就是控制间隔数
3 * 第二层循环保证了在其实为d的时候后面所有的数据都能够比较,所有的元素都是从后面开始向前面比较
4 * 第三层循环主要是交换数据
5 */
6 for (int d = a.length/2; d > 0 ; d=d/2) {
7 for (int i = d; i) {
全部代码:
1 package com.hone.Shell;8 System.out.print(a[i]+" ");
2
3 public class TestShell {
4 public static void main(String[] args) {
5 int a[]={2,1,7,6,89,34,45,23,24,88};
6 System.out.print("排序之前: ");
7 for (int i = 0; i) {
时间复杂度:希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。
稳定性:希尔排序方法是按照增量分组来进行的排序,两个相同的数据元素可能在不同的分组中,因此相同的数据元素的位置有可能发生变化。因此希尔是一个不稳定的排序方法。
2:选择排序
核心思想:从待排序的数据元素中选取最小的数据元素并将它和元素数据中的第一个元素相交换,然后从除了第一个元素的其他的数据集合中选择最小数据元素并将它与第二个元素相交换位置,直到元素集合中剩下最后一个元素为止。。他的大致排序的方法与冒泡排序相反。
代码描述:
1 /*9 //假设最小数的都是每次循环的第一个数
2 * 选择排序中从数据元素集合中找出最小值,并且将该值与第一个数相交换,
3 * 然后再从除了第一个数之外的数中找出最小值,并且与第二个数相交换。
4 * 依次类推直到,所有的数据都交换完毕。
5 * 选择排序和冒泡排序是逆着进行的,选择排序首先是将最小的元素先排到最前面(并且不具有稳定性),冒泡排序是将最大的元素先排到末端
6 */
7 int temp;
8 for (int i = 0; i) {
时间复杂度:直接选择排序的时间复杂度为O(n2)
稳定性:不稳定。因为直接选择排序的核心思想是将数据集合中的最小元素与第一个元素相交换,假如第一个元素和第二个元素相同,则会使原数据的位置发生改变,不具备稳定性。
但是,如果每次不是直接交换而是采用插入的方式呢?
将需要插入的位置的元素的后面元素都往后移动一个,再将该元素插入到有序区的后面。这样就可以保证稳定性。
稳定的直接选择排序:
1 package com.hone.Select;8 System.out.print(a[i]+" ");
2
3 public class TestSelectForSteable {
4 public static void main(String[] args) {
5 int a[]={2,1,7,6,89,34,45,23,24,88};
6 System.out.print("排序之前: ");
7 for (int i = 0; i) {
堆排序的核心思想:创建最大堆,然后将根结点元素(最大元素)取出再将剩余的二叉树创建成最大堆。
首先创建最大堆:从叶子结点往根节点调整。
该部分参考 http://blog.csdn.net/hguisu/article/details/7776068
堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足
时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:
(a)大顶堆序列:(96, 83,27,38,11,09)
(b) 小顶堆序列:(12,36,24,85,47,30,53,91)
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。
首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:
1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
称这个自根结点到叶子结点的调整过程为筛选。如图:
再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。
1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。
2)筛选从第个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
代码描述:
1 package com.hone.Select;19 //寻找左右结点的较大者,j为其下标
2
3 public class HeapSort {
4 /*
5 * 定义一个方法用于创建最大堆
6 */
7 public static void createHeap(int[] a, int n,int h){
8 int i,j,flag;
9 int temp;
10
11 i=h; //i为要建堆的二叉树根结点下标
12 j= 2 * i+ 1; //j为i结点的左孩子结点的下标
13 temp=a[i];
14 flag=0;
15
16 //沿着左右子树中值较大的重复向下删选
17 //当 j 小于叶子节点的时候,并且该结点没有被访问过
18 while(j){
3:交换排序
核心思想:冒泡排序的类似于简单的选择排序,每次遍历都挑出最大值,将最大值放到数组的末端,然后在除了最大数之外的其他数中挑选出最大值,放在倒数第二个的位置。
冒泡排序算法简单,但是效率不高。
代码描述:
1 /*7 if(a[j]>a[j+1]){
2 * 冒泡排序的思路:首先对相邻的两个数进行比较,第一轮可以将数组中最大的树排到数组的末端
3 */
4 for (int i = a.length-1; i >= 0; i--) { //最多做n-1次排序
5 int flag=0;
6 for (int j = 0; j ) {
时间复杂度:最好的情况是原数据元素都已经有序,因此做n-1次检查但是没有交换。因此T=O(N)
最坏情况是原数据都是倒叙,需要检查也需要交换。 T=O(n2)
稳定性:稳定
核心思想:
1)选择一个基准元素,通常选择第一个元素或者最后一个元素,或者是中间元素
2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小,放在基准值的左边,另一部分记录的 元素值比基准值大,放在基准值的右边。
3)此时基准元素在其排好序后的正确位置。
4)然后分别对这两部分记录用递归方法继续进行排序,直到整个序列有序。
快速排序的示例:
(a)一趟排序的过程:
(b)排序的全过程
代码描述:
1 /*27 if (i<j) {
2 * 快速排序类似于建立哨兵的方式,现在数组中随便定一个 “ 哨 兵 ”,将比哨兵小的数排列在左边
3 * 将比哨兵大的元素排序在数组的右边,然后在分别对左边,右边的数组利用相同的方法
4 */
5 public class QuickSort {
6 public static void quickSort(int[] a, int low,int high){
7 int i,j;
8 int temp;
9
10 i=low;
11 j=high;
12 temp=a[low]; //取第一个元素为标准数据元素
13 //下面的对左边和右边的扫描定位反复进行,直到左边的下标i大于或者等于右边元素的下标为止
14 while(i<j){
15 //在数组的右边扫描,如果数大于哨兵,则不改变位置,否则将j上的元素马上移动到i位置,
16 //并且马上扫描左边
17 while( i=temp) j--;
18 if (i<j) {
19 a[i]=a[j];
20 i++;
21 }
22
23
24 //在数组的左边扫描,如果数小于哨兵,则不改变位置,否则将左边i处的位置交换到
25 //右边j处的位置,并且转回扫描右边
26 while( i;
时间复杂度:快速排序的时间复杂度和各次标准数据元素的取法有关系。如果每次选取的元16:00:36素都能够将数据均分,则这样的快速排序完全类似于一个完全二叉树,分解次数等于完全二叉树的深度 logn ,每次比较的次数都接近n次,因此总的时间复杂度为 T=O(nlogn)
最坏的情况:所有的数据元素都已经正序或者反序。此时情况最坏为O(n2)。
4:归并排序
核心思想:将原始的数据n分为n个长度为1的数组元素,然后从第一个数组元素开始把相邻的子数组两两合并,得到n/2个长度为2的有序子数组,如此反复合并知道最后合并为一个数组,并且该数组个数为n个为止。
归并排序示例:
代码描述:
1 package com.hone.Merge;53 swap[m]=a[i];
2
3 public class MergeSort {
4 /*
5 * 定义一个函数包含三个参数,第一个参数表示传入的数组a,第二个参数表示临时储存的数组s
6 * k,表示当前需要合并的数组的原始长度
7 */
8 public static void merge(int[] a ,int[] swap,int k){
9 int n=a.length;
10 int m=0;
11 int i,j;
12 int s1,s2,e1,e2; //变量分别表示两个数组的收尾坐标
13
14 /*
15 * 定义两个数组坐标间的关系
16 */
17 s1=0;
18 while(s1+k <= n-1){
19 s2=s1+k;
20 e1=s2-1;
21 e2=(s2+k-1 <= n-1)?s2+k-1:n-1;
22
23 for ( i = s1,j=s2; i <=e1 && j<= e2 ; m++) {
24 if (a[i]<=a[j]) {
25 swap[m]=a[i];
26 i++;
27 }else {
28 swap[m]=a[j];
29 j++;
30 }
31 }
32
33 //如果出现了数组2中元素已经归并完毕,数组1仍然未归并完毕,直接将剩下的所有元素直接
34 //赋值给swap
35 while(i<=e1){
36 swap[m]=a[i];
37 i++;
38 m++;
39 }
40
41 //如果出现了数组1中元素已经归并完毕,数组2仍然未归并完毕,直接将剩下的所有元素直接
42 //赋值给swap
43 while(j<=e2){
44 swap[m]=a[j];
45 j++;
46 m++;
47 }
48 s1=e2+1; //形成收尾连接
49 }
50
51 //如果某些集合不能划分为两个数组,则直接全部复制给swap
52 for(i=s1; i)
时间复杂度:对于任何的归并排序,归并的次数为n次,任何一次归并排序元素的比较次数都约为 Log n次,所以,归并排序算法的时间复杂度永远都是:O(nlogn)
空间复杂度:但是因为归并排序每次都需要用新的空间来存放n个数据元素,因此需要的空间复杂度为 O(n)
稳定性:稳定
特点:归并排序时间效率高,但是需要额外的储存空间,因此,归并函数适合于数据较少的排序。
5:基数排序
(该部分转载与:http://blog.csdn.net/hguisu/article/details/7776068 )
说基数排序之前,我们先说桶排序:
基本思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是 比较排序,他不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
例如要对大小为[1..1000]范围内的n个整数A[1..n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1..10]的整数,集合B[2]存储 (10..20]的整数,……集合B[i]存储( (i-1)*10, i*10]的整数,i = 1,2,..100。总共有 100个桶。
然后,对A[1..n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。 再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任 何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这 样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果
对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn - nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
2)其次待排序的元素都要在一定的范围内等等。
桶式排序是一种分配排序。分配排序的特定是不需要进行关键码的比较,但前提是要知道待排序列的一些具体情况。
分配排序的基本思想:说白了就是进行多次的桶式排序。
基数排序过程无须比较关键字,而是通过“分配”和“收集”过程来实现排序。它们的时间复杂度可达到线性阶:O(n)。
实例:
扑克牌中52 张牌,可按花色和面值分成两个字段,其大小关系为:
花色: 梅花<方块<红心<黑心
面值: 2 <3 <4 <5 <6 <7 <8 <9 <10
若对扑克牌按花色、面值进行升序排序,得到如下序列:
即两张牌,若花色不同,不论面值怎样,花色低的那张牌小于花色高的,只有在同花色情况下,大小关系才由面值的大小确定。这就是多关键码排序。
为得到排序结果,我们讨论两种排序方法。
方法1:先对花色排序,将其分为4 个组,即梅花组、方块组、红心组、黑心组。再对每个组分别按面值进行排序,最后,将4 个组连接起来即可。
方法2:先按13 个面值给出13 个编号组(2 号,3 号,...,A 号),将牌按面值依次放入对应的编号组,分成13 堆。再按花色给出4 个编号组(梅花、方块、红心、黑心),将2号组中牌取出分别放入对应花色组,再将3 号组中牌取出分别放入对应花色组,……,这样,4 个花色组中均按面值有序,然后,将4 个花色组依次连接起来即可。
设n 个元素的待排序列包含d 个关键码{k1,k2,…,kd},则称序列对关键码{k1,k2,…,kd}有序是指:对于序列中任两个记录r[i]和r[j](1≤i≤j≤n)都满足下列有序关系:
其中k1 称为最主位关键码,kd 称为最次位关键码 。
多关键码排序按照从最主位关键码到最次位关键码或从最次位到最主位关键码的顺序逐次排序,分两种方法:
最高位优先(Most Significant Digit first)法,简称MSD 法:
1)先按k1 排序分组,将序列分成若干子序列,同一组序列的记录中,关键码k1 相等。
2)再对各组按k2 排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd 对各子组排序后。
3)再将各组连接起来,便得到一个有序序列。扑克牌按花色、面值排序中介绍的方法一即是MSD 法。
最低位优先(Least Significant Digit first)法,简称LSD 法:
1) 先从kd 开始排序,再对kd-1进行排序,依次重复,直到按k1排序分组分成最小的子序列后。
2) 最后将各个子序列连接起来,便可得到一个有序的序列, 扑克牌按花色、面值排序中介绍的方法二即是LSD 法。
基于LSD方法的链式基数排序的基本思想
“多关键字排序”的思想实现“单关键字排序”。对数字型或字符型的单关键字,可以看作由多个数位或多个字符构成的多关键字,此时可以采用“分配-收集”的方法进行排序,这一过程称作基数排序法,其中每个数字或字符可能的取值个数称为基数。比如,扑克牌的花色基数为4,面值基数为13。在整理扑克牌时,既可以先按花色整理,也可以先按面值整理。按花色整理时,先按红、黑、方、花的顺序分成4摞(分配),再按此顺序再叠放在一起(收集),然后按面值的顺序分成13摞(分配),再按此顺序叠放在一起(收集),如此进行二次分配和收集即可将扑克牌排列有序。
基数排序:
是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
总结:
各种排序的稳定性,时间复杂度和空间复杂度总结:
我们比较时间复杂度函数的情况:
时间复杂度来说:
(1)平方阶(O(n2))排序
各类简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlog2n))排序
快速排序、堆排序和归并排序;
(3)O(n1+§))排序,§是介于0和1之间的常数。
希尔排序
(4)线性阶(O(n))排序
基数排序,此外还有桶、箱排序。
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
稳定性:
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;
若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
选择排序算法准则:
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
选择排序算法的依据
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 如果内存空间允许且要求稳定性的,
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2) 当n较大,内存空间允许,且要求稳定性 =》归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5)一般不使用或不直接使用传统的冒泡排序。
6)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。