排序算法整合
发布日期:2021-06-29 15:42:19 浏览次数:2 分类:技术文章

本文共 13639 字,大约阅读时间需要 45 分钟。

  排序算法种类繁多根据处理的数据规模与存储特点,可分为内部排序和外部排序:前者处理的数据规模不大,内存足以容纳;后者处理的数据规模较大,必须将数据存放于外部存储器中,每次排序的时候需要访问外存。根据输入的不同形式,分为脱机算法和在线算法:前者待排序的数据是以批处理的形式给出的;而在云计算之类的环境中,待排序的数据是实时生成的,在排序算法开始运行时,数据并未完全就绪,而是随着排序算法本身的进行而逐步给出的。另外,针对不同的体系结构,又分为串行和并行两大类排序算法。根据算法是否采用随机策略,还有确定式和随机式之分。

冒泡排序(O(n^2))

冒泡排序是比较相邻两数的大小来完成排序的。这里定义比较边界,也就是进行大小比较的边界。对于长度为n的数组,第一趟的比较边界为[0,n-1],也就是说从a[0]开始,相邻元素两两比较大小,如果满足条件就进行交换,否则继续比较,一直到最后一个比较的元素为a[n-1]为止,此时第一趟排序完成。以升序排序为例,每趟排序完成之后,比较边界中的最大值就沉入底部,比较边界就向前移动一个位置。所以,第二趟排序开始时,比较边界是[0,n-2]。对于长度为n的序列,最多需要n趟完成排序,所以冒泡排序就由两层循环构成,最外层循环用于控制排序的趟数,最内层循环用于比较相邻数字的大小并在本趟排序完成时更新比较边界。

具体代码如下:

1  //冒泡排序 2     public static void bubbleSort(int[] arr,int len){ 3         int temp=0; 4         int compareRange=len-1;//冒泡排序中,参与比较的数字的边界。 5         //冒泡排序主要是比较相邻两个数字的大小,以升序排列为例,如果前侧数字大于后侧数字,就进行交换,一直到比较边界。 6         for (int i = 0; i 
arr[j]){ 9 temp=arr[j-1];10 arr[j-1]=arr[j];11 arr[j]=temp;12 }13 }14 compareRange--;//每进行一趟排序,序列中最大数字就沉到底部,比较边界就向前移动一个位置。15 }16 System.out.println("排序后数组"+Arrays.toString(arr));17 }

  在排序后期可能数组已经有序了而算法却还在一趟趟的比较数组元素大小,可以引入一个标记,如果在一趟排序中,数组元素没有发生过交换说明数组已经有序,跳出循环即可。优化后的代码如下:

1 public static void bubbleSort2(int[] arr,int len){ 2         int temp=0; 3         int compareRange=len-1;//冒泡排序中,参与比较的数字的边界。 4         boolean flag=true;//标记排序时候已经提前完成 5         int compareCounter=0; 6         //冒泡排序主要是比较相邻两个数字的大小,以升序排列为例,如果前侧数字大于后侧数字,就进行交换,一直到比较边界。 7        while(flag) { 8            flag=false; 9             for (int j = 1; j <=compareRange ; j++) {10                 if(arr[j-1]>arr[j]){11                     temp=arr[j-1];12                     arr[j-1]=arr[j];13                     arr[j]=temp;14                     flag=true;15                 }16             }17            compareCounter++;18            compareRange--;//每进行一趟排序,序列中最大数字就沉到底部,比较边界就向前移动一个位置。19         }20         System.out.println("优化后排序次数:"+(compareCounter-1));21         System.out.println("排序后数组"+Arrays.toString(arr));22     }

  还可以利用这种标记的方法还可以检测数组是否有序,遍历一个数组比较其大小,对于满足要求的元素进行交换,如果不会发生交换则数组就是有序的,否则是无序的。

  两种方法的排序结果如下所示:

插入排序(O(n^2))

将待排序的数组划分为局部有序子数组subSorted和无序子数组subUnSorted,每次排序时从subUnSorted中挑出第一个元素,从后向前将其与subSorted各元素比较大小,按照大小插入合适的位置,插入完成后将此元素从subUnSorted中移除,重复这个过程直至subUnSorted中没有元素,总之就时从后向前,一边比较一边移动。

对应代码如下: 

1  //直接插入排序 2     public static void straightInsertSort(int[] arr,int len){ 3         int temp=0; 4         int j=0; 5         for (int i = 1; i 
=0; j--) {
//有序区间 7 if(arr[i]>arr[j]){ 8 break; 9 }10 }11 if(j!=i-1){12 temp=arr[i];13 for (int k =i-1; k >j ; k--) {14 arr[k+1]=arr[k];//从后向前移动数组15 }16 arr[j+1]=temp;17 }18 // System.out.println("直接插入排序后数组" + Arrays.toString(arr));19 20 }21 System.out.println("直接插入排序后数组" + Arrays.toString(arr));22 23 }24 //直接插入排序简洁版25 public static void straightInsertSort2(int[] arr,int len){26 int temp=0;27 int j=0;28 for (int i = 1; i
=0&&temp

添加一个易于理解的版本:

1  //插入排序易理解版 2     public static void straightInsertSort3(int[] arr,int len){ 3         int high=0;//有序区间的上界(包括) 4         int insertValue=0,i=0,j=0; 5         while (high
=0&&insertValue
=i+1 ; j--) {12 arr[j+1]=arr[j];13 }14 arr[i+1]=insertValue;15 ++high;16 }17 System.out.println("直接插入排序后数组" + Arrays.toString(arr));18 }

新版本: 

1  public static void insertSort(int arr[],int len){ 2         int tmp=-1; 3         int soretdIndex=0; 4         for (int i = 1; i 
=0&&arr[soretdIndex]>tmp){ 8 arr[soretdIndex+1]=arr[soretdIndex]; 9 soretdIndex--;10 }11 arr[soretdIndex+1]=tmp;12 }13 }

 哨兵版本:

1 public static void insertSort2(int[] arr,int len){
//arr[0]作为哨兵,arr[1...len]是待排序元素,len是其个数 2 for (int i = 2; i <=len ; i++) { 3 arr[0]=arr[i]; 4 int j; 5 for(j=i-1;arr[0]

希尔排序(O(n*(log n)^2)

由希尔在1959年提出,基于插入排序发展而来。希尔排序的思想基于两个原因:

1)当数据项数量不多的时候,插入排序可以很好的完成工作。

2)当数据项基本有序的时候,插入排序具有很高的效率。

基于以上的两个原因就有了希尔排序的步骤:

a.将待排序序列依据步长(增量)划分为若干组,对每组分别进行插入排序。初始时,step=len/2,此时的增量最大,因此每个分组内数据项个数相对较少,插入排序可以很好的完成排序工作(对应1)。

b.以上只是完成了一次排序,更新步长step=step/2,每个分组内数据项个数相对增加,不过由于已经进行了一次排序,数据项基本有序,此时插入排序具有更好的排序效率(对应2)。直至增量为1时,此时的排序就是对这个序列使用插入排序,此次排序完成就表明排序已经完成。

  可以看出,每次排序的步长逐渐缩小,新的一轮排序就是在上轮已排好序的分组中,添加一个新元素,然后对这个已基本有序的序列使用插入排序,这种条件下,插入排序具有最高的排序效率。实现代码如下:

1 //希尔排序 2     public static void shellSort(int[] arr,int len){ 3         int step=len/2;//step既是组数又是步长。 4         int temp=0; 5         int k=0; 6         while (step>0){ 7             for (int i = 0; i 
=0&&temp

  以上代码不够简洁,还可以进一步改进。事先不必分组,可以从第step个元素开始,从左向右扫描余下的序列,与索引值相差step的元素比较大小,也就是说将[step,2*step-1]与[0,step-1]区间内对应的元素比较,较大就保持不动,较小就移动至相关位置。然后再将[2*step,3*step-1]与[step,2*step-1]相比,依此类推,制止扫描到最后一个元素。实现代码如下:

1  public static void shellSort2(int[] arr,int len){ 2         int temp=0; 3         int step=len/2; 4         int j=0; 5         while (step>0){ 6             for (int i = step; i 
=0&&arr[j]>temp){11 arr[j+step]=arr[j];12 j-=step;13 }14 arr[j+step]=temp;15 }16 }17 step/=2;18 }19 //System.out.println("希尔排序2后的数组为:"+Arrays.toString(arr));20 }

新版本: 

1 public static void shellSort3(int[] arr,int len){ 2         int j=0,tmp=0; 3         for (int d = len/2; d >0 ; d/=2) {
//d是增量,也是排序时的分组数。 4 for (int i = d; i
=0&&arr[j]>tmp){ 8 arr[j+d]=arr[j]; 9 j-=d;10 }11 arr[j+d]=tmp;12 }13 }14 }

   希尔排序中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(n^2)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法

选择排序(O(n^2))

像插入排序那样,将待排序序列划分为有序区和无序区(整个待排序序列)。

1)不过不同的是,初始时,有序区为空,无序区是整个待排序序列。

2)通过比较在无序区中得到最小的记录值,将其与无序区第一个位置的元素交换,有序区就增加了一个元素,同时无序区减少了一个元素。

3)重复上述操作,直至无序区中元素个数为0。

实现代码如下:

1 public static void selectSort(int[] arr,int len){ 2         int temp=0; 3         int minIndex=-1; 4         for (int i = 0; i 
arr[j+1]){ 8 minIndex=j+1; 9 }10 }11 temp=arr[i];//有序区的最后一个位置的右侧12 arr[i]=arr[minIndex];//将最小值放至有序区的最后一个位置上的右侧,覆盖原先值。13 arr[minIndex]=temp;//将有序区最后一个位置的右侧的原先值赋值给无序区的最小值处。14 }15 System.out.println("选择排序后的数组为:"+Arrays.toString(arr));16 }

  这里补充不使用临时变量对两个数值进行交换的方法。实现代码如下:

1     //使用加减法来完成不使用临时变量进行交换的目的,不过当a、b很大时,可能会溢出。 2     public int[] swap1(int a,int b){ 3         a=a+b; 4         b=a-b; 5         a=a-b; 6         return new int[]{a,b}; 7     } 8     //使用异或运算完成不使用临时变量进行交换,使用异或进行两数交换时,两数不能相等 9     public int[] swap2(int a,int b){10         if(a!=b) {
//使用异或进行两数交换时,两数不能相等11 a ^= b;12 b ^= a;13 a ^= b;14 }15 return new int[]{a,b};16 }

  解释下使用异或进行交换的原理。异或位运算,当两位相同时,结果为1,否则为0。使用异或进行交换的原理如下图所示:

 

堆排序(O(n*log n))

堆的概述

堆排序是基于选择排序的改进,目的是较少比较次数。一趟选择排序中,仅保留了最小值,而堆排序排序不仅保留最小值,还把较小值保留下来,减少了比较小次数。

堆是这样一种完全二叉树:根节点的值大于等于左右孩子节点的值(最大堆)或者根节点的值小于等于左右孩子节点的值(最小堆)。堆也是递归定义的,即堆的孩子节点本身也是堆。使用数组存储堆。

堆具有以下性质:

1)完全二叉树A[0:n-1]中的任意节点,索引为i的节点,其左右孩子节点是2*i+1和2*i+2。

2)非叶子节点最大索引是⌊n/2⌋-1,叶子节点最小索引是⌊n/2⌋。

3)最大(最小)堆的左右子树也是最大(小)堆。

如果是升序排列,就使用最大堆,反之使用最小堆。以下假设是升序排列。

排序方法

堆排序可以分为两个过程:构建初始堆和重建堆。

构建初始堆

由于每个叶子节点本身就是以这个叶节点作为根节点的堆,而构建堆的目的就是使以每个节点作为根节点的树都满足堆的定义,因此从堆(完全二叉树)的最下侧非叶子节点开始构建初始堆,根据堆的性质,这个节点的索引是⌊n/2⌋-1。从下向上,一直到堆顶节点也满足堆的定义,表示完成堆的初始化。把以某个节点为根节点的树调整为堆的方法如下:

1)设这个节点为i,其左孩子为j(完全二叉树中某个节点如果只有一个子节点,那么一定是左节点)。

2)如果arr[j]<arr[j+1],那么++j(指向右孩子)。

3)如果arr[i]>arr[j],说明这个以节点为根的树已经满足堆的定义,算法结束。

4)否则,swap(arr[i],arr[j]),由于交换过程中破坏了原来以j为根节点的树的堆结构,所以以j为当前调整节点转步骤1,如果j为叶子节点则迭代结束(叶子节点本身就是堆)。

调整节点的方法接口是adjust(int[] arr,int k,int m),其中arr是待排序序列,k是待调整节点的索引值,m是堆的最大索引值。实现代码如下: 

1  public static void adjust(int[] arr,int k,int m){ 2         int tmp; 3         int i=k;//要调整的节点 4         int j=2*k+1;//调整节点的左孩子 5         while (j<=m){
//最新的调整节点的左孩子索引值不能超过堆的最大索引 6 if(j
arr[j]){ 9 break;10 }11 else {12 tmp = arr[i];13 arr[i] = arr[j];14 arr[j] = tmp;15 i = j;16 j = 2 * i + 1;17 }18 }19 }

 重建堆

完成了初始堆的创建之后,就可以通过不断的重建堆进行堆排序,每次重建堆就是一趟排序,每次重建时都将堆顶节点与堆无序区的最后一个元素交换,因此每趟堆排序后堆的有序区就增加了一个元素(从数组最后与各元素开始,向前排列),下轮就使用无序区组成的堆进行重建,每次重建都只是对堆顶节点的调整,因为初始堆建成之后,其他节点都满足堆的定义。实现代码如下:

1  //堆排序,m是待排序序列的大小 2     public static void heapSort(int [] arr,int m){ 3         //创建初始堆 4         int lastEleIndex=m-1;//无序区的最后一个元素的索引值 5         int tmp; 6         //从最下侧的非叶子节点开始,向上创建初始堆 7         for (int i = m/2-1; i >=0 ; i--) { 8             adjust(arr,i,lastEleIndex); 9         }10         //重建堆,每趟将堆顶节点与堆无序区最后一个元素交换,然后再调整新堆顶节点。每趟完成之后完成了一个元素的排序11         for (int i = 0; i 

堆排序特点

创建初始堆的时间复杂度是O(n),简单的解释是有n/2个节点需要调整,每次调整节点时只是上写移动常数个节点,因此创建初始堆的时间复杂度是O(n)。而实际进行堆排序时,需要进行n趟,每趟进行堆重建时就是调整堆顶节点,最多移动次数不会超过书的高度O(log n),因此时间复杂度是O(n*log n)。

堆排序对数据的原始排列状态并不敏感,所以其最坏时间复杂度、最好时间复杂度、平均时间复杂度均是O(n*log n),堆排序不是一种稳定的排序算法。

归并排序(O(n*log n))

概述

归并的含义就是将两个或多个有序序列合并成一个有序序列的过程,归并排序就是将若干有序序列逐步归并,最终形成一个有序序列的过程。以最常见的二路归并为例,就是将两个有序序列归并。归并排序由两个过程完成:有序表的合并和排序的递归实现。

 

有序表的合并

虽然说是两个有序表的合并,不过这里并不是使用两个数组进行合并,而是通过数组索引的形式“描述”两个待合并的有序表,合并的方法签名如右所示mergeArray(int arr[],int tmp,int low,int mid,int high),其中low是合并有序表t1的起始位置,mid是t1的终止位置,mid+1是t2的起始位置,high是t2的终止位置,最后tmp是存储合并后元素的临时数组。有序表合并完成后,将临时数组tmp中元素复制到原数组相应位置。两个有序数组合并,其代码实现如下: 

1 public static void mergeArray2(int[] arr,int tmp[],int low,int mid,int high){ 2         int i=low; 3         int j=mid+1; 4         int k=low; 5         //将合并后的元素存到临时数组中 6         while (i<=mid&&j<=high){ 7             if(arr[i]

 非递归形式

   非递归形式的归并排序的实现中,关键是假设每个part1都有一个与之对应的part2,以part2的右边界high为两序列进行合并的检测条件。以part2的左边界mid为判断整个待排序序列最尾部的part1是否有对应的part2的检测条件。对于没有对应part2的有序表不做任何处理,对应代码如下:

1  //二路归并排序,非递归版本 2     public static void mergeSort(int[] array,int len){ 3         int eachGroupNumbers=1; 4         int[] temp=new int[len]; 5         int high=-1; 6         int low; 7         while (eachGroupNumbers<=len){ 8             low=0; 9             high=low+2*eachGroupNumbers-1;10             //两两合并数组的两个有序序列11             //假设每个part1都有对应的part212 13             //以high作为边界检测条件,如果part2的右边界high小于整个待排序序列的右边界,则两个有序序列进行合并。14             for (; high
  

递归形式

将待排序序列分为A和B两部分,如果A和B都是有序的,只需要调用有序序列的合并算法mergeArray就完成了排序,可是A和B不是有序的,再分别将A和B一分为二,直至最终的序列只有一个元素,我们认为只有一个元素的序列是有序的,合并这些序列,就得到了新的有序序列,然后返回给上层调用者,上上层调用这再合并这些序列,得到更长的有序序列,这就是递归形式的归并排序,示意图如下图所示

 

图片来自:)。使用上述递归树分析归并排序的时间复杂度,以递归实现归并排序时,是自顶向下将待排序序列一分为二,直至每个子序列元素为1。所以递归树高度为log n。由于每层元素个数为n个,所以每层中,两个有序表合并为一个新有序表时的比较次数不超过n,因此归并排序的时间复杂度是O(n*log n),并且好像无所谓最好情况、最差情况,所有情况下时间复杂度都是O(n*log n)。

实现代码如下: 

1  public static void mergeSort2(int[] arr,int[] tmp,int low,int high){2         if(low

 归并排序是一种稳定的排序。 

快速排序(O(n*log n))

快速排序是图灵奖得主 C. R. A. Hoare 于 1960 年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。快速排序由分区和递归排序两个过程完成。

分区

分区分为三个步骤:

1.在数组中,选择一个元素作为“基准”(pivot),一般选择第一个元素作为基准元素。设置两个游标i和j,初始时i指向数组首元素,j指向尾元素。

2.从数组最右侧向前扫描,遇到小于基准值的元素停止扫描,将两者交换,然后从数组左侧开始扫描,遇到大于基准值的元素停止扫描,同样将两者交换。

3.i==j时分区完成,否则转2。()

  

分区的实现代码,如下: 

1 public static int partition2(int[] arr,int low,int high){ 2         int i=low;//左游标,选择数组第一个元素作为基准值 3         int j=high;//右游标 4         //i==j表示分区过程的结束 5             while (i 
=arr[i]) { 8 --j; 9 }10 //上述循环结束可能是因为i==j,原数组升序排列导致。如果是这样的话,分区就可以结束了11 if(i

 递归形式排序

   每次分区之后,基准值所处的位置(storeIndex)就是最终排序后它的位置,并且,一次分区之后,数据集一分为二,在分别对两侧的新分区进行分区,直至最后每个子数据集中只剩下一个元素,代码实现如下: 

1   public static void quickSort2(int [] arr,int low,int high){2         if(low

   快速排序最好情况是每次分区后,都将序列等分为两个长度基本相等的子序列(也就是分区后基准元素都位于序列中间位置)。第一次分区后,子序列长度为n/2,第二次分区后,子序列长度为n/4,第i次分区后子序列长度为n/(2^i),直到子序列长度为1。设经过x次分区后子序列长度为1,则有n/2^x=1,则x=log n,也就是说最好情况下经过log n次分区完成排序。使用递归树来理解快速排序的最好时间复杂度。递归树的高度就是分区次数,由上述计算可知,递归树的高度是log n。在递归树的每一层总共有n个节点,并且各子序列在分区的时候关键字的比较次数不超过n,所以就有基本操作次数不超过n*log n。所以,快排在理想情况下的时间复杂度是O(n*log n)。

           

最坏情况

  当我们每次进行分区划分时,如果每次选择的基准元素都是当前序列中最大或最小的记录,这样每次分区的时候只得到了一个新分区,另一个分区为空,并且新分区只是比分区前少一个元素,这是快速排序的最坏情况,时间复杂度上升为O(n^2),因为递归树的高度为n。所以,有人提出随机选择基准元素,这样在一定程度上可以避免最坏情况的发生,但是理论上最坏情况还是存在的。

  由于快速排序是使用递归实现的,所以其空间复杂度就是栈的开销,最坏情况下的递归树高度是n,此时空间复杂度是O(n),一般情况下递归树的长度是log n,此时空间复杂度是O(log n)。

快速排序适用于待排序记录个数很多且分布随机的情况,并且快拍是目前内排序中排序算法最好的一种。

总结

各排序方法接口形式  

冒泡排序、插入排序、希尔排序、选择排序和堆排序的排序方法接口均是sort(int[] arr,int len)的形式,其中len是序列长度。归并排序由于需要临时数组存放两有序表合并的结果,排序方法接口是sort(int[] arr,int [] tmp,int low,int high),而快速排序不需要临时数组,其排序方法接口是sort(int[] arr,int low,int high)。

各排序算法的比较 

排序算法的稳定性是指排序前后具有相同关键字的记录,相对顺序保持不变。形式化的定义就是,排序之前有ri=rj,ri在rj之前,而在排序之后ri仍然在rj之前,就说这种算法是稳定的。各排序算法的比较如下所示:

1)排序时,可以先尝试一种较慢但简单的排序,例如插入排序,如果还是慢,可以选择希尔排序(数据量在5000以下时很有用),还是慢的话,就使用快速排序,堆排序和归并排序在某些程度上较快速排序慢。

2)一般在不要求稳定性的场景下,使用快速排序就可以了。但是,当我们每次进行分区划分时,选择的基准元素都是最小(排序目的是升序)/最大(排序目的是降序),这是快速排序的最坏情况,时间复杂度上升为O(n^2),这时可以使用堆排序和归并排序。在n较大时,相对堆排序来说,归并排序使用时间较少,但辅助空间较多(合并两个有序序列为一个有序序列)。

3)序列基本有序且n较小时,插入排序具有很高的排序效率。因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。

4)可以基于决策树证明基于比较排序算法的时间下限为O(n*log n),也即是说比较排序算法最快就是时间复杂度为O(n*log n)。比较排序算法包括:冒泡排序、插入排序、选择排序、希尔排序、归并排序、快速排序。

最好情况和最坏情况

 如果是升序排列,那么排序的最好情况就是待排序序列是升序的,最坏情况待排序序列是降序的。如果目的是降序排列,情况正好相反。

参考

1)数据结构和算法C++版第二版 王红梅

2)

 

转载地址:https://codingchaozhang.blog.csdn.net/article/details/80965335 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Java程序员常见笔试题分析
下一篇:【01】Java面试----基础方面的陷阱

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月15日 17时39分03秒