一、排序的概念:
1、设 n 个记录的序列为 { R1 , R2 , R3 , . . . , Rn}
其相应的关键字序列为 { K1 , K2 , K3, . . . , Kn }
若规定 1 , 2 , 3 , . . . , n 的一个排列 p1 , p2 , p3 , . . . , pn,使得相应的关键字满足如下非递减关系:
Kp1 ≤ Kp2 ≤ Kp3≤ . . . ≤ Kpn
则原序列变为一个按关键字有序的序列:
Rp1 , Rp2 , Rp3 , . . . , Rpn
此操作过程称为排序。
2、排序问题一般分为内排序( internal sorting )和外排序( external sorting )两类:
2.1. 内排序:待排序的表中记录个数较少,整个排序过程中所有的记录都可以保留在内存中;按照排序过程中所依据的原则的不同可以分类为:
Ø 插入排序(直接插入排序、折半插入排序、希尔排序)
Ø 交换排序(快速排序)(冒泡泡排序、快速排序)
Ø 选择排序(直接选择排序、堆排序)
Ø 归并排序
Ø 基数排序
Ø 二叉排序树排序
2.2.外排序:待排序的记录个数足够多,以至于他们必须存储在磁带、磁盘上组成外部文件,排序过程中需要多次访问外存。
3、排序的时间复杂性:
排序过程主要是对记录的排序码进行比较和记录的移动过程。因此排序的时间复杂性可以算法执行中的数据比较次数及数据移动次数来衡量。当一种排序方法使排序过程在最坏或平均情况下所进行的比较和移动次数越少,则认为该方法的时间复杂性就越好,分析一种排序方法,不仅要分析它的时间复杂性,而且要分析它的空间复杂性、稳定性和简单性等。
二、各种排序算法及代码详解:
1、插入类排序--直接插入排序
插入类排序算法思想:主要就是对于一个已经有序的序列中,插入一个新的记录,但要求插入后此数据序列仍然有序,这个时候就要用到插入排序法。它包括:直接插入排序,折半插入排序和希尔排序。
1.1、直接插入排序的基本思想是,经过i-1遍处理后,L[1..i-1]己排好序。第i遍处理仅将L[i]插入L[1..i-1]的适当位置,使得L[1..i]又是排好序的序列。要达到这个目的,直接插入排序用顺序比较的方法。首先比较L[i]和L[i-1],如果L[i-1]≤ L[i],则L[1..i]已排好序,第i遍处理就结束了;否则交换L[i]与L[i-1]的位置,继续比较L[i-1]和L[i-2],直到找到某一个位置j(1≤j≤i-1),使得L[j] ≤L[j+1]时为止。
算法描述
1.2、一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
1. 从第一个元素开始,该元素可以认为已经被排序,
2. 取出下一个元素,在已经排序的元素序列中从后向前扫描,
3. 如果该元素(已排序)大于新元素,将该元素移到下一位置,
4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置,
5. 将新元素插入到下一位置中,
6. 重复步骤2;
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一种,称为二分查找排序。
1.3、算法代码实现:
public class DirectInserSort { public static void main(String[] args) { int count1 = 0, count2 = 0;// 复制次数,比较次数 long begin = System.currentTimeMillis(); System.out.println("插入前时间为:" + begin); // TODO Auto-generated method stub int data[] = { 2, 6, 10, 3, 9, 80, 1, 16, 27, 20 }; int temp, j; for (int i = 1; i < data.length; i++) { temp = data[i]; j = i - 1; // 每次比较都是对于已经有序的 while (j >= 0 && data[j] > temp) { data[j + 1] = data[j]; j--; count1++; } data[j + 1] = temp; count2++; } long end = System.currentTimeMillis(); System.out.println("插入后时间为:" + end); System.out.println("插入法用时为:" + (end - begin)); // 输出排序好的数据 for (int k = 0; k < data.length; k++) { System.out.print(data[k] + " "); } System.out.println("复制次数为:" + count1 + " 比较次数为:" + count2); } }
插入排序法在数据已有一定顺序的情况下,效率较好。但如果数据无规则,则需要移动大量的数据,其效率就与冒泡排序法和选择排序法一样差了。因此插入排序是一个不稳定的排序方法,插入效率与数组初始顺序息息相关。一般情况下,插入排序的时间复杂度和空间复杂度分别为 O(n2 ) 和 O(1) 。
2、插入类排序--二分法排序
2.1、二分法排序算法思想:折半插入排序记录的比较次数与初始序列无关,折半插入就是首先将队列中取最小位置low和最大位置high,然后算出中间位置mid,将中间位置mid与待插入的数据data进行比较,如果mid大于data,则就表示插入的数据在mid的左边,high=mid-1,如果mid小于data,则就表示插入的数据在mid的右边,low=mid+1。
2.2、具体算法描述如下:
1.确定查找范围front=0,end=N-1,计算中项mid=(front+end)/2。
2.若data [mid]=x或front>=end,则结束查找;否则,向下继续。
3.若data [mid]<x,说明待查找的元素值只可能在比中项元素大的范围内,则把mid+1的值赋给front,并重新计算mid,转去执行步骤2;若data [mid]>x,说明待查找的元素值只可能在比中项元素小的范围内,则把mid-1的值赋给end,并重新计算mid,转去执行步骤2。
2.3、算法代码实现:
public class BinInsertSort { //折半插入排序 public static void main(String[] args) { int count1 = 0, count2 = 0;// 复制次数,比较次数 long begin = System.currentTimeMillis(); System.out.println("插入前时间为:" + begin); int data[] = { 2, 6, 10, 3, 9, 80, 1, 16, 27, 20 }; // 存放临时要插入的元素数据 int temp; int low, mid, high; for (int i = 1; i < data.length; i++) { temp = data[i]; // 在待插入排序的序号之前进行折半插入 low = 0; high = i - 1; while (low <= high) { mid = (low + high) / 2; if (temp < data[mid]) high = mid - 1; else // low=high的时候也就是找到了要插入的位置, // 此时进入循环中,将low加1,则就是要插入的位置了 low = mid + 1; count2++; } // 找到了要插入的位置,从该位置一直到插入数据的位置之间数据向后移动 for (int j = i; j >= low + 1; j--){ data[j] = data[j - 1]; count1++; } // low已经代表了要插入的位置了 data[low] = temp; } long end = System.currentTimeMillis(); System.out.println("插入后时间为:" + end); System.out.println("插入法用时为:" + (end - begin)); for (int k = 0; k < data.length; k++) { System.out.print(data[k] + " "); } System.out.println("复制次数为:" + count1 + " 比较次数为:" + count2); }
折半查找是一种高效的查找方法。它可以明显减少比较次数,提高查
找效率。但是,折半查找的先决条件是查找表中的数据元素必须有序。二分插入算法与直接插入算法相比,需要辅助空间与直接插入排序基本一致;时间上,前者的比较次数比直接插入查找的最坏情况好,最好的情况坏,两种方法的元素的移动次数相同,因此二分插入排序的时间复杂度仍为O(n2)。二分插入算法与直接插入算法的元素移动一样是顺序的,因此该方法也是稳定的。
3、插入类排序--希尔排序
3.1、对于插入排序算法来说,如果原来的数据就是有序的,那么数据就不需要移动,而插入排序算法的效率主要消耗在数据的移动中。因此可知:如果数据的本身就是有序的或者本身基本有序,那么效率就会得到提高。
希尔排序的基本思想是:将需要排序的序列划分成为若干个较小的子序列,对子序列进行插入排序,通过则插入排序能够使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。
在希尔排序中首先解决的是子序列的选择问题。对于子序列的构成不是简单的分段,而是采取相隔某个增量的数据组成一个序列。一般的选择原则是:去上一个增量的一般作为此次序列的划分增量。首次选择序列长度的一般为增量。
3.2算法步骤:
Step1 将n个元素个数列分为比如5个小组,在每个小组内按直接插入法排序;
step2 在第i步,分组个数取 di+1 =(di +1)/2 {9,5,3,2,1};相临两组之间的对应元素进行比较,如果ai>aj,则交换它们的位置;
Step3 当dK = 1的循环过程完成后,排序过程结束。
3.3、算法代码实现:
import java.util.Random; class ShellSort{ public void sort(int[] resource){ int h = 1; int temp; while (h*3+1<resource.length+1){ h = h*3+1; } while (h != 0){ for(int i=0; i<h; i++){ for (int j=i; j<resource.length-h; j+=h){ temp = resource[j+h]; int k; for (k=j; k>i-h; k-=h){ if (temp<resource[k]){ resource[k+h] = resource[k]; } else { break; } } resource[k+h] = temp; } } h = (h-1)/3; } } public static void main(String[] args){ ShellSort shell = new ShellSort(); Random random = new Random(); int[] test = new int[10000]; for (int i=0; i<test.length; i++){ test[i] = random.nextInt(10000); } for(int i : test){ System.out.print(i+","); } System.out.println(); long s = System.currentTimeMillis(); shell.sort(test); long e = System.currentTimeMillis(); for(int i : test){ System.out.print(i+","); } System.out.println("\n"+(e-s)/1000.0+"秒"); } }
3.4、算法讨论:
Shell排序算法的时间复杂度分析比较复杂,实际所需的时间取决于各次排序时增量的个数和增量的取值。研究证明,若增量的取值比较合理,Shell排序算法的时间复杂度约为O(n(ldn)2)。由于Shell排序算法是按增量分组进行的排序,所以Shell排序算法是一种不稳定的排序算法。也称为递减增量排序算法,各种实现在如何进行递减上有所不同。不稳定,不需要辅助空间。希尔排序几乎没有最坏情况,无论是正序、逆序、乱序,所用时间都不是很多,附加储存是O(1),的确非常不错。
4、 交换类排序--冒泡排序
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
4.1、冒泡排序(Bubble Sort)是一种最直观的排序方法,在排序过程中,将相邻的记录的关键字进行比较,若前面记录的关键字大于后面记录的关键字,则将它们交换,否则不交换。或者反过来,使较大关键字的记录后移,像水中的气泡一样,较小的记录向前冒出,较大的记录像石头沉入后部。故称此方法为冒泡排序法。
算法的基本思想为:首先在n个元素中,若ai>ai+1(i=1..n-1)则交换,得到一个最大元素放于an;其次在n-1个元素中,若ai>ai+1(i=1..n-2)则交换,这样得到的一个次大元素放于an-1,以此类推,直到选出n-1个元素,排序完成。
4.2、算法代码实现:
public class BubbleSort { public static void main(String[] args) { int vec[] = new int[] { 37, 46, 33, -5, 17, 51 }; int temp; long begin = System.currentTimeMillis(); begin = System.currentTimeMillis(); for (int k = 0; k < 1000000; k++) { for (int i = 0; i < vec.length; i++) { for (int j = i; j < vec.length - 1; j++) { if (vec[j + 1] < vec[j]) { temp = vec[j + 1]; vec[j + 1] = vec[j]; vec[j] = temp; } } } } long end = System.currentTimeMillis(); System.out.println("冒泡法用时为:" + (end - begin)); //打印排序好的结果 for (int i = 0; i < vec.length; i++) { System.out.println(vec[i]); } } }
4.3、算法讨论:冒泡排序算法稳定,O(1)的额外的空间,比较和交换的时间复杂度都是O(n^2),自适应,对于已基本排序的算法,时间复杂度为O(n)。冒泡算法的许多性质和插入算法相似,但对于系统开销高一点点。使用冒泡排序法对n个数据进行排序,共需要进行n-1次的比较。如果本来就是有顺序的数据,也需要进行n-1次比较。冒泡排序法的算法很简单,效率也较差。
5、交换类排序--快速排序
5.1、快速排序(Quick Sorting)是对冒泡排序的一种改进。在冒泡排序中,记录的比较和交换是在相邻的单元中进行的,记录每次交换只能上移或下移一个单元,因而总的比较和移动次数较多。而在快速排序中,记录的比较和交换是从两端向中间进行的,关键字较小的记录一次就能从后面单元交换到前面去,而关键字较大的记录一次就能从前面的单元交换到后面的单元,记录每次移动的记录较远,因此可以减少记录总的比较和移动次数。
快速排序的基本做法是:任取待排序的n个记录中的某个记录作为基准(一般选取第一个记录),通过一趟排序,将待排序记录分成左右两个字序列,左子序列记录的关键字均小于或等于该基准记录的关键字,右子序列记录的关键字均大于或等于该基准记录的关键字,从而得到该记录最终排序的位置,然后该记录不再参加排序,此一趟排序成为第一趟快速排序。然后对所分得左右子序列分别重复上述方法,直到所有的记录都处在他们的最终位置,此时排序完成。在快速排序中,有时把待排序序列按照基准记录的关键字分为左右两个子序列的过程称为一次划分。
5.2、快速排序的过程为:
设待排序序列为r[s..t],为实现一次划分,可设置两个指针low和high,他们的初值分别为s和t。以r[s]为基准,在划分的过程中:
(1)从high端开始,依次向前扫描,并将扫描到的每一个记录的关键字同r[s](即基准记录)的关键字进行比较,直到r[high].key<r[s].key时,将r[high]赋值到low所指的位置。
(2)从low端开始,依次向后扫描,并将扫描到的每一个记录的关键字同r[s](即基准记录)的关键字进行比较,直到r[low].key>r[s].key时,将r[low]赋值到high所指的位置。
(3)如此交替改变扫描方向,重复上述两个步骤从两端各自向中间位置靠拢,直到low等于或大于high。经过此次划分后得到的左右两个子序列分别为r[s..low-1]和r[low+1..t]。然后对这两个子序列按上述方法进行再次划分,依次重复,直到每个序列只剩一个元素为止。
5.3算法实现:
public class QuickSort { public void swap(int a[], int i, int j) { int tmp = a[i]; a[i] = a[j]; a[j] = tmp; } public int partSort(int a[], int low, int high) { int pivot, p_pos, i; p_pos = low; pivot = a[p_pos]; for (i = low + 1; i <= high; i++) { if (a[i] > pivot) { p_pos++; swap(a, p_pos, i); } } swap(a, low, p_pos); return p_pos; } public void quicksort(int a[], int low, int high) { int pivot; if (low < high) { pivot = partSort(a, low, high); quicksort(a, low, pivot - 1); quicksort(a, pivot + 1, high); } } public static void main(String[] args) { // 快速排序法(Quick Sort) int vec[] = new int[] { 37, 46, 33, -5, 17, 51 }; QuickSort s = new QuickSort(); long begin = System.currentTimeMillis(); for (int k = 0; k < 1000000; k++) { s.quicksort(vec, 0, 5); } long end = System.currentTimeMillis(); System.out.println("快速法用时为:" + (end - begin)); // 打印排序好的结果 for (int i = 0; i < vec.length; i++) { System.out.println(vec[i]); } } }
5.4算法讨论:在快速排序中,若把每次划分所用的基准记录看作根节点,把划分得到的左子序列和右子序列分别看成根节点的左、右子树,那么整个排序过程就对应着一颗具有n个节点的二叉排序树,所需划分的层数等于二叉树的深度,所需划分的所有子序列数等于二叉树分枝结点数,而在快速排序中,记录的移动次数通常小于记录的比较次数。因此,讨论快速排序的时间复杂度时,仅考虑记录的比较次数即可。
若快速排序出现最好的情况(左、右子序列的长度大致相等),则结点数n与二叉树深度h应满足log2(n)<=h<=log2(n+1),所以总的比较次数不会超过(n+1)log2(n).因此,快速排序的最好时间复杂度应为O(nlog2(n))。若快速排序出现最坏的情况(每次能划分成两个子序列,但是其中一个为空),则此时得到的二叉树是一棵单枝树,得到的非空子序列包含有n-i(i代表二叉树的层数),每层划分需要比较n-i+2次,所以总的比较次数为(n^2+3n-4)/2.因此,快速排序的最坏时间复杂度为O(n^2).
快速排序所占用的辅助空间为递归时所需栈的深度,故空间复杂度为O(log2(n))。同时,快速排序是不稳定的排序。
6、选择类排序--简单选择排序
6.1、选择排序(Selection Sort)的基本思想是:每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。如对于一组关键字{K1,K2,…,Kn},首先从K1,K2,…,Kn中选择最小值,假如它是 Kz,则将Kz与 K1对换;然后从K2,K3,…,Kn中选择最小值 Kz,再将Kz与K2对换。如此进行选择和调换n-2趟,第(n-1)趟,从Kn-1、Kn中选择最小值 Kz将Kz与Kn-1对换,最后剩下的就是该序列中的最大值,一个由小到大的有序序列就这样形成。
6.2、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(1≤i≤n-1)。该趟排序从当前无序区中选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。
这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。
6.3、算法具体实现:
public class SelectSort { public static void main(String[] args) { int vec[] = new int[] { 37, 46, 33, -5, 17, 51 }; int temp; //选择排序法(Selection Sort) long begin = System.currentTimeMillis(); for (int k = 0; k < 1000000; k++) { for (int i = 0; i < vec.length; i++) { for (int j = i; j < vec.length; j++) { if (vec[j] > vec[i]) { temp = vec[i]; vec[i] = vec[j]; vec[j] = temp; } } } } long end = System.currentTimeMillis(); System.out.println("选择法用时为:" + (end - begin)); //打印排序好的结果 for (int i = 0; i < vec.length; i++) { System.out.println(vec[i]); } } }
选择排序法与冒泡排序法一样,最外层循环仍然要执行n-1次,其效率仍然较差。该算法的时间复杂度为 O(n2)。并且排序是稳定的。
7、八 选择类排序--堆排序
7.1、堆的概念: 一棵完全二叉树,任一个非终端结点的值均小于等于(或大于等于)其左、右儿子结点的值。堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
例:
7.2、用大根堆排序的基本思想
①先将初始文件R[1..n]建成一个大根堆,此堆为初始的无序区;
②再将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1..n-1]和有序区R[n],且满足R[1..n-1].keys≤R[n].key;
③由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1..n-1]调整为堆。然后再次将R[1..n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1..n-2]和有序区R[n-1..n],且仍满足关系R[1..n-2].keys≤R[n-1..n].keys,同样要将R[1..n-2]调整为堆。
……
直到无序区只有一个元素为止。
(2)大根堆排序算法的基本操作:
①初始化操作:将R[1..n]构造为初始堆;
②每一趟排序的基本操作:将当前无序区的堆顶记录R[1]和该区间的最后一个记录交换,然后将新的无序区调整为堆(亦称重建堆)。
7.3、基本排序过程:
1. 将序列构造成一棵完全二叉树 ;
2. 把这棵普通的完全二叉树改造成堆,便可获取最小值 ;
3. 输出最小值或者最大值;
4. 删除根结点,继续改造剩余树成堆,便可获取次小值 ;
5. 输出次小值 ;
6. 重复改造,输出次次小值、次次次小值,直至所有结点均输出,便得到一个排序 。
7.4、算法基本实现(看了很多人写的算法,本人觉得这种算法蛮好):
public class HeapSort { public static int heap_size; // 左孩子编号 public static int leftChild(int i) { return 2 * i+1; } // 右孩子编号 public static int rightChild(int i) { return 2 * i + 2; } /** * 保持最大堆的性质 * 堆中的数组元素 * 对以该元素为根元素的堆进行调整,假设前提:左右子树都是最大堆 * 由于左右孩子都是最大堆,首先比较根元素与左右孩子,找出最大值,假如大于根元素,则调整两个元素的值; * 由于左孩子(右孩子)的值与根元素交换,有可能打破左子树(右子树)的最大堆性质,因此继续调用,直至叶子元素。 */ public static void max_heapify(int[] a, int i) { int left = leftChild(i); int right = rightChild(i); int largest = 0; if (left < heap_size && a[i] < a[left]) { largest = left; } else { largest = i; } if (right < heap_size && a[right] > a[largest]) { largest = right; } if (largest == i) { return; } else { int temp = a[i]; a[i] = a[largest]; a[largest] = temp; max_heapify(a, largest); } } /** * 建立最大堆。在数据中,下标a.length/2+1一直到最后的元素a.length-1都是叶子元素 * 因此从其前一个元素开始,一直到 * 第一个元素,重复调用max_heapify函数,使其保持最大堆的性质 */ public static void build_max_heap(int[] a) { //从0~a.length/2中建立最大堆 for (int i = a.length / 2; i >= 0; i--) { max_heapify(a, i); } } /** * 堆排序:首先建立最大堆,然后将堆顶元素(最大值)与最后一个值交换,同时使得 堆的长度减小1 * 调用保持最大堆性质的算法调整,使得堆顶元素成为最大值,此时最后一个元素已被排除在外、 */ public static void heapSort(int[] a) { //构建最大堆 build_max_heap(a); for (int i = a.length - 1; i >= 0; i--) { //将第一个元素和最后一个元素进行互换 int temp = a[0]; a[0] = a[i]; a[i] = temp; heap_size--; //调整堆为最大堆 max_heapify(a, 0); } } public static void main(String[] args) { int a[] = {5, 4, 1, 3, 2, 16, 9, 10, 14, 8, 7}; long begin = System.currentTimeMillis(); for (int k = 0; k < 1000000; k++) { heap_size = a.length;//最大数 heapSort(a); //输出结果 } long end = System.currentTimeMillis(); System.out.println("选择法用时为:" + (end - begin)); for (int i = 0; i < a.length; i++) { System.out.print(a[i] + " "); } } }
7.5、算法讨论:从上述过程可知,堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1...n]中选择最大记录,需比较n-1次,然后从R[1...n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。
8、二路归并排序
8.1、归并排序(Merge)是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并---合并两个有序的序列假设有两个已排序好的序列A(长度为n1),B(长度为n2),将它们合并为一个有序的序列C(长度为n=n1+n2)
方法很简单:把A,B两个序列的最小元素进行比较,把其中较小的元素作为C的第一个元素;在A,B剩余的元素中继续挑最小的元素进行比较,确定C的第二个元素,依次类推,就可以完成对A和B的归并,其复杂度为O(n)。
例如:
A: 1 3 8 9 11
B: 2 5 7 10 13
C: 1 2 3 5 7 8 9 10 11 13
8.2、归并排序:
1、递归基础:若序列只有一个元素,则它是有序的,不执行任何操作
2、递归步骤:
先把序列划分成长度基本相等的两个序列
对每个子序列递归排序
把排好序的子序列归并成最后的结果
例如:
初始序列: [8,4,5,6,2,1,7,3]
分解: [8,4,5,6] [2,1,7,3]
分解: [8,4] [5,6] [2,1] [7,3]
分解: [8] [4] [5] [6] [2] [1] [7] [3]
归并: [4,8] [5,6] [1,2] [3,7
归并: [4,5,6, 8] [1,2,3,7]
归并: [1,2,3, 4,5,6,7,8]
8.3、算法具体实现:
import java.util.Arrays; //二路归并排序主要分为 //分割和合并 public class MergeSort { public static void main(String[] args) { int data[] = { 2, 6, 10, 3, 9, 80, 1, 16, 27, 20 }; mergeSort(data,0,data.length-1); //直接打印 System.out.println(Arrays.toString(data)); } //二路归并的分割处理 public static void mergeSort(int[] array,int start,int end) { if(start<end) { //划分为两部分,每次两部分进行归并 int mid=(start+end)/2; //两路归并 //先递归处理每一个部分 mergeSort(array,start,mid); mergeSort(array,mid+1,end); //然后将已经排序好的,两两归并排序再进行合并处理 merge(array,start,mid,mid+1,end); } } //二路归并两个部分的时候进行排序 public static void merge(int[] array,int start1,int end1,int start2,int end2) { int i=start1;//左路起始索引 int j=start2;//右路起始索引 int k=0; //归并的时候,会将两个数组数据按照大小输入到一个临时数组中 //建立临时长度为两个子列表长度的数组 int[] temp=new int[end2-start1+1]; //循环遍历,按顺序找出两个表中的最小数据依次放入临时表中 //注意此时左路和右路已经是有序的了。 //当一路有一个小的,则会索引加1,继续喝另外一路的上次索引进行比较 while(i<=end1&&j<=end2) { //这里确定归并的次序大小 if(array[i]>array[j]) temp[k++]=array[j++]; else temp[k++]=array[i++]; } //把剩下的元素放入临时数组中,只有一路的 while(i<=end1) temp[k++]=array[i++]; while(j<=end2) temp[k++]=array[j++]; k=start1; for(int item:temp) array[k++]=item; } }
8.4、算法分析:归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。
尽管归并排序最坏情况的比较次数比快速排序少,但它需要更多的元素移动,因此,它在实用中不一定比快速排序快
9、基数排序
9.1基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。最典型的应该就是扑克牌问题。
基数排序就是借助于“分配”和“收集”两种操作实现对单逻辑关键字的排序。
首先,单逻辑关键字通常都可以看作是由若干关键字复合而成。
例,若关键字是数值,且值域为 0≤K≤999,故可以将 K 看作是由 3 个关键字 K0 K1 K2组成;
例,603是由 6 0 3 组成。
其次,利用 LSDF 法实现对若干关键字的排序。
例,序列 278 109 063 930 589 184 505 269 008 083
一、时间性能
按平均的时间性能来分,有三类排序方法:
时间复杂度为O(nlogn)的方法有:快速排序、堆排序和归并排序,其中以快速排序为最好;
时间复杂度为O(n2)的有:直接插入排序、起泡排序和简单选择排序,其中以直接插入为
最好,特别是对那些对关键字近似有序的记录序列尤为如此;
当待排记录序列按关键字顺序有序时,直接插入排序和起泡排序能达到O(n)的时间复杂
度;而对于快速排序而言,这是最不好的情况,此时的时间性能蜕化为O(n2),因此是应
该尽量避免的情况。
简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
二、空间性能
指的是排序过程中所需的辅助空间大小。
1. 所有的简单排序方法(包括:直接插入、起泡和简单选择)和堆排序的空间复杂度为O(1);
2. 快速排序为O(logn),为栈所需的辅助空间;
3. 归并排序所需辅助空间最多,其空间复杂度为O(n);
三、排序方法的稳定性能
1. 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在
排序之前和经过排序之后,没有改变。
2. 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
3. 对于不稳定的排序方法,只要能举出一个实例说明即可。
4. 希尔排序、快速排序和堆排序是不稳定的排序方法
四、应用
归并排序很难用于主存排序,主要问题在于:合并两个排序的表需要线性附加内存,在整个算法中还要花费将数据拷贝到临时数组再拷贝回来这样一些附加的工作,其结果严重放慢了排序的速度。
对于重要的内部排序应用而言,应选择快速排序。
快排和归并排序都是分治的递归算法。
对于很小的数组(N<=20),快速排序不如插入排序好。
1 快速排序(QuickSort)
快速排序是一个就地排序,分而治之,大规模递归的算法。从本质上来说,它是归并排序的就地版本。快速排序可以由下面四步组成。
(1) 如果不多于1个数据,直接返回。
(2) 一般选择序列最左边的值作为支点数据。
(3) 将序列分成2部分,一部分都大于支点数据,另外一部分都小于支点数据。
(4) 对两边利用递归排序数列。
快速排序比大部分排序算法都要快。尽管我们可以在某些特殊的情况下写出比快速排序快的算法,但是就通常情况而言,没有比它更快的了。快速排序是递归的,对于内存非常有限的机器来说,它不是一个好的选择。
2 归并排序(MergeSort)
归并排序先分解要排序的序列,从1分成2,2分成4,依次分解,当分解到只有1个一组的时候,就可以排序这些分组,然后依次合并回原来的序列中,这样就可以排序所有数据。合并排序比堆排序稍微快一点,但是需要比堆排序多一倍的内存空间,因为它需要一个额外的数组。
3 堆排序(HeapSort)
堆排序适合于数据量非常大的场合(百万数据)。
堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。
堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。
4 Shell排序(ShellSort)
Shell排序通过将数据分成不同的组,先对每一组进行排序,然后再对所有的元素进行一次插入排序,以减少数据交换和移动的次数。平均效率是O(nlogn)。其中分组的合理性会对算法产生重要的影响。现在多用D.E.Knuth的分组方法。
Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。
5 插入排序(InsertSort)
插入排序通过把序列中的值插入一个已经排序好的序列中,直到该序列的结束。插入排序是对冒泡排序的改进。它比冒泡排序快2倍。一般不用在数据大于1000的场合下使用插入排序,或者重复排序超过200数据项的序列。
6 冒泡排序(BubbleSort)
冒泡排序是最慢的排序算法。在实际运用中它是效率最低的算法。它通过一趟又一趟地比较数组中的每一个元素,使较大的数据下沉,较小的数据上升。它是O(n^2)的算法。
7 交换排序(ExchangeSort)和选择排序(SelectSort)
这两种排序方法都是交换方法的排序算法,效率都是 O(n2)。在实际应用中处于和冒泡排序基本相同的地位。它们只是排序算法发展的初级阶段,在实际中使用较少。
8 基数排序(RadixSort)
基数排序和通常的排序算法并不走同样的路线。它是一种比较新颖的算法,但是它只能用于整数的排序,如果我们要把同样的办法运用到浮点数上,我们必须了解浮点数的存储格式,并通过特殊的方式将浮点数映射到整数上,然后再映射回去,这是非常麻烦的事情,因此,它的使用同样也不多。而且,最重要的是,这样算法也需要较多的存储空间。
9 总结
下面是一个总的表格,大致总结了我们常见的所有的排序算法的特点。
排序法 |
平均时间 |
最差情形 |
稳定度 |
额外空间 |
备注 |
冒泡 |
O(n2) |
O(n2) |
稳定 |
O(1) |
n小时较好 |
交换 |
O(n2) |
O(n2) |
不稳定 |
O(1) |
n小时较好 |
选择 |
O(n2) |
O(n2) |
不稳定 |
O(1) |
n小时较好 |
插入 |
O(n2) |
O(n2) |
稳定 |
O(1) |
大部分已排序时较好 |
基数 |
O(logrd) |
O(logrd) |
稳定 |
O(n) |
d是关键字项数(0-9), r是基数(个十百) |
Shell |
O(nlogn) |
O(ns) 1<s<2 |
不稳定 |
O(1) |
s是所选分组 |
快速 |
O(nlogn) |
O(n2) |
不稳定 |
O(nlogn) |
n大时较好 |
归并 |
O(nlogn) |
O(nlogn) |
稳定 |
O(1) |
n大时较好 |
堆 |
O(nlogn) |
O(nlogn) |
不稳定 |
O(1) |
n大时较好 |