排序算法整合

DNA排序算法--图文 DNA排序算法,简单来说就是通过DNA工作原理实现排序功能的算法。本篇文章从整合排序算法的角度切入这个算法。 首先,收集现有的经典排序算法,它们分别有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序等。 然后,通过某种方式对它们进行归类,从搜索的结果来看,可将它们分成基于比较的排序算法和基于分配的排序算法两大类,分类如下: 再就是找到一个视角,描绘这些算法的共同之处,为下一步的整合铺路。这个视角如同动画的关键帧,起到以点带面的作用。在这里,它就是反映算法分类 阅读详情


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

冒泡排序(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 <len ; i++) {//n个数使用冒泡排序,最多需要n趟完成排序。最外层循环用于控制排序趟数
 7             for (int j = 1; j <=compareRange ; j++) {
 8                 if(arr[j-1]>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 <len ; i++) {//待插入的数字
 6             for (j =i-1; j>=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 <len ; i++) {
29             if(arr[i]<arr[i-1]){
30                 temp=arr[i];
31                 for (j = i-1; j>=0&&temp<arr[j] ; j--) {
32                     arr[j+1]=arr[j];//从后向前移动数组
33                 }
34                 arr[j+1]=temp;
35             }
36         }
37         System.out.println("直接插入排序后数组" + Arrays.toString(arr));
38     }
复制代码

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

复制代码
 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<len-1){
 6             i=high;
 7             insertValue=arr[i+1];
 8             while (i>=0&&insertValue<arr[i]){
 9                 --i;
10             }
11             for (j =high; j>=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 <len ; i++) {
 5             tmp=arr[i];
 6             soretdIndex=i-1;
 7             while (soretdIndex>=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]<arr[j];--j){
 6                 arr[j+1]=arr[j];
 7             }
 8             arr[j+1]=arr[0];
 9         }
10     }
复制代码

希尔排序(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 <step ; i++) {//将待排序序列分组
 8 
 9                 for (int j = i+step; j <len;j+=step) {//每个分组使用直接插入排序
10                     if(arr[j]<arr[j-step]){
11                         temp=arr[j];//待插入元素
12                         for (k =j-step;k>=0&&temp<arr[k];k-=step) {//后移较大的元素
13                             arr[k+step]=arr[k];
14                         }
15                         arr[k+step]=temp;
16                     }
17                 }
18             }
19             step/=2;//更新步长。
20         }
21         //System.out.println("希尔排序后的数组为:"+Arrays.toString(arr));
22     }
复制代码

  以上代码不够简洁,还可以进一步改进。事先不必分组,可以从第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 <len ; i++) {//从第setp个元素开始,将其与之前的元素相比
 7                 if (arr[i]<arr[i-step]){//使用直接插入排序
 8                     temp=arr[i];
 9                     j=i-step;
10                     while (j>=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 <len; i++) {//0~d-1是各分组的第一个元素,作为初始时插入排序的有序序列。
 5                 j=i-d;      //得到i所在的分组中,其前一个元素(有序的)
 6                 tmp=arr[i];
 7                 while (j>=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 <len ; i++) {//i是有序区最后一个位置的右侧
 5             minIndex=i;
 6             for (int j =i; j <len-1 ; j++) {//无序区
 7                 if(arr[minIndex]>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<m&&arr[j]<arr[j+1])
 7              ++j;//得到左右孩子中的最大值节点
 8          if(arr[i]>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 <m ; i++) {
12             tmp=arr[0];
13             arr[0]=arr[lastEleIndex];
14             arr[lastEleIndex]=tmp;
15             adjust(arr,0,--lastEleIndex);
16         }
17         System.out.println();
18     }
复制代码

堆排序特点

创建初始堆的时间复杂度是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]<arr[j]){
 8                 tmp[k++]=arr[i++];
 9             }
10             else {
11                 tmp[k++]=arr[j++];
12             }
13         }
14         while (i<=mid){
15             tmp[k++]=arr[i++];
16         }
17         while (j<=high){
18             tmp[k++]=arr[j++];
19         }
20         //将临时数组中内容赋值给原数组
21         for (int l =low ; l <=high ; l++) {
22             arr[l]=tmp[l];
23         }
24     }
复制代码

 非递归形式

   非递归形式的归并排序的实现中,关键是假设每个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都有对应的part2
12 
13             //以high作为边界检测条件,如果part2的右边界high小于整个待排序序列的右边界,则两个有序序列进行合并。
14             for (; high<len ; high=low+2*eachGroupNumbers-1) {//以high作为边界检测条件
15                 mergeArray(array,low,low+eachGroupNumbers-1,high,temp);
16                 low=high+1;
17             }
18             /*
19             跳出循环,说明part2的右边界已经超出了整个待排序序列的右边界。
20             如果part2的左边界mid还在整个序列的右边界内,将两序列进行合并,
21              */
22             if(low+eachGroupNumbers-1<len){//以mid作为边界检测条件
23                 mergeArray(array,low,low+eachGroupNumbers-1,len-1,temp);
24             }
25             /*
26             如果part2的左边界也不在整个序列的右边界范围内,说明这个part1并没有对应的part2,不做任何处理。
27              */
28             //本轮合并完成,继续划分数组
29             eachGroupNumbers=eachGroupNumbers<<1;
30             //System.out.println("本轮的结果:"+Arrays.toString(array));
31             }
32         System.out.println("归并排序后的数组为:"+Arrays.toString(array));
33     }
复制代码
  

递归形式

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

 

(图片来自:http://alinuxer.sinaapp.com/?p=141)。使用上述递归树分析归并排序的时间复杂度,以递归实现归并排序时,是自顶向下将待排序序列一分为二,直至每个子序列元素为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<high){
3             int mid=low+(high-low)/2;
4             mergeSort2(arr,tmp,low,mid);
5             mergeSort2(arr,tmp,mid+1,high);
6             mergeArray2(arr,tmp,low,mid,high);
7         }
8     }
复制代码

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

快速排序(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 <j) {
 6                 //从右侧向左扫描,找到小于基准值的元素,使用i<j防止数组越界(原数组可能升序排列)
 7                 while (i<j&&arr[j] >=arr[i]) {
 8                     --j;
 9                 }
10                 //上述循环结束可能是因为i==j,原数组升序排列导致。如果是这样的话,分区就可以结束了
11                if(i<j)
12                 {
13                     arr[i] ^= arr[j];
14                     arr[j] ^= arr[i];
15                     arr[i] ^= arr[j];
16                     ++i;
17                 }
18                 //从左侧向右扫描,找到大于基准值arr[j]的元素,使用i<j防止数组越界(只是此时原数组可能降序排列)
19                 while (i<j&&arr[i] <= arr[j]) {
20                     i++;
21                 }
22                 //上述循环结束可能是因为i==j,原数组降序排列导致。如果是这样的话,分区就可以结束了
23                 if(i<j)
24                 {
25                     arr[i] ^= arr[j];
26                     arr[j] ^= arr[i];
27                     arr[i] ^= arr[j];
28                     --j;
29                 }
30             }
31 
32         return i;
33     }
复制代码

 递归形式排序

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

复制代码
1   public static void quickSort2(int [] arr,int low,int high){
2         if(low<high){
3             int mid=partition2(arr,low,high);
4             quickSort2(arr,low,mid-1);
5             quickSort2(arr,mid+1,high);
6         }
7     }
复制代码

   快速排序最好情况是每次分区后,都将序列等分为两个长度基本相等的子序列(也就是分区后基准元素都位于序列中间位置)。第一次分区后,子序列长度为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)http://wuchong.me/blog/2014/02/09/algorithm-sort-summary/

 

常见排序算法总结——10常用+4整活+2整合 常见排序算法总结 阅读详情

相关推荐

数据结构课程设计(内部排序算法比较_C语言)

数据结构课程设计(内部排序算法比较_C语言) 数据结构课程设计(内部排序算法比较_C语言)

数组常用方法简单梳理

用Xmind简单记录一下数组的常用方法

weixin_57044893的博客 269

基于遗传算法的内容编排模型设计

在支撑内容型业务及移动广告业务的内容管理系统中,内容编排是基于订单的业务媒介资源规划利益最大化的热点问题。将媒介资源构建成以空间模板分屏、时序资源、受众分组3个维度的载体业务模型,并且提出基于订单需求约束,以实现订单高效资源利用率、精确投放时间为目标的多目标资源优化(MOP)模型——内容编排模型,设计了并行遗传算法的模型求解算法并进行仿真,验证了模型的有效性。

七大排序算法

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

weixin_30372371的博客 253

STM32F103模拟SSI时序解析:实现高精度绝对式编码器位置读取

本文详细解析了如何在STM32F103微控制器上通过软件模拟SSI协议时序,实现高精度绝对式编码器的位置读取。文章从项目驱动出发,深入剖析了SSI通信原理、硬件电平转换连接、核心软件模拟代码实现及关键时序参数调试方法,并提供了使用示波器进行波形对齐与优化的实战指南,为成本敏感型工业控制与机器人项目提供了高性价比的解决方案。

weixin_29196315的博客 141

保姆级教程:手把手配置AutoSar诊断复位(Dcm 0x11服务联动BswM/EcuM)

本文提供AutoSar诊断复位(Dcm 0x11服务联动BswM/EcuM)的保姆级配置教程,详细讲解从诊断服务基础配置到BswM规则引擎搭建、服务端口映射及EcuM复位执行的完整流程。通过Vector配置工具的实际截图和代码片段,帮助开发者高效实现远程复位功能,并分享调试技巧与性能优化实践。

weixin_30756499的博客 544

冒泡排序 BubbleSort easy 20220127

* 思路: * 横向:n个元素,只需要跑n-1趟,最后一趟只剩一个数,不需要比较 * 竖向:n个元素,只需要比较n-1-i次,减i是因为最后i个数已经排好序了,减1是因为这里是和下一个数比较 public class BubbleSort { public static void bubbleSort(int[] array){ for (int i=0;i<array.length-1;i++){ //最后一趟只剩一个数,不需要比较 .

你好啊cbw 444

AI全景之第八章第三节:时间序列分析与预测技术

摘要 时间序列分析与预测技术从传统统计模型发展到深度学习方法。传统方法包括ARIMA模型(处理趋势和季节性)、指数平滑(加权平均预测)和状态空间模型(概率框架分解)。机器学习方法通过特征工程和树模型提升非线性建模能力。深度学习方法中,RNN/LSTM处理序列依赖,TCN利用因果卷积,Transformer通过注意力机制捕获长程依赖。当前研究趋势结合神经符号方法和生成模型,以提高预测精度和可解释性。不同方法各具优势,需根据数据特性和预测需求选择。

技术引领业务创新 990

C语言项目:数组与函数实践:扫雷游戏

1.如果位置不是雷,就显示周围有几个雷2.如果位置是雷,就炸死结束游戏3.把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束。

weixin_64484421的博客 937

打通Creo与Matlab的桥梁:Simscape Multibody Link插件安装与配置全攻略

本文详细介绍了Simscape Multibody Link插件的安装与配置全流程,旨在帮助机械工程师和开发者打通Creo与Matlab/Simulink之间的桥梁,实现CAD模型到多体动力学仿真的高效转换。文章重点解析了版本兼容性、管理员权限、注册文件配置等关键步骤与常见故障排查方法,助力用户快速建立设计与仿真一体化工作流。

rrr55的博客 634

NLP自然语言处理:神经网络语言模型(NNLM)

目录 一、传统语言模型 1.1 稀疏性 1.2泛化能力差 二、神经网络语言模型 2.1 前馈神经网络模型(FFLM) 2.2 循环神经网络模型(RNNLM) 2.2.1循环神经网络模型示例 2.2.2RNN语言模型训练过程 2.2.3RNN语言模型反向传播 2.2.4语言模型评估 语言模型的构建目的是为了对语言中的各要素的分布进行估计,具有较长的研究历史和广泛...

陈宸的博客 1万+

PTA 6-2 冒泡排序 (10分)

编程实现冒泡排序函数。void bubbleSort(int arr[], int n);。其中arr存放待排序的数据,n为数组长度(1≤n≤1000)。 函数接口定义: /* 对长度为n的数组arr执行冒泡排序 */ void bubbleSort(int arr[], int n); 请实现bubbleSort函数,使排序后的数据从小到大排列。 裁判测试程序样例: #include <stdio.h> #define N 1000 int arr[N]; /* 对长度为n的数组arr执

Captain0827的博客 1万+

Kong API Gateway 配置与深入

Kong是一个开源的、高性能的API网关,提供了一种简单易用的服务间路由、流量管理和安全防护方案,其最初起源于Nginx Inc.,但后迁移至Apache Software Foundation。Kong支持RESTful、GraphQL、TCP/UDP等协议,可与AWS Lambda、Azure Functions、Google Cloud Functions等云函数平台集成。其主要功能包括:服务发现与负载均衡、请求代理、身份认证与授权、缓存、日志记录、监控指标、灰度发布、多种插件等。

AI天才研究院 2283

C语言排序算法整合

学习了排序算法与查找算法,所以利用C语言实现了下,并整合起来。用管理系统的形式进行编写。 程序运行截图如下: 代码如下: #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <windows.h> #define N 1000 int Arr[N] = { 0 }; int iNumber = 0; //记录元素个数 //==================

Hermit的博客 289

CloudCompare 和 PCL:点云网格化分析与处理

点云网格化是将点云数据转化为规则网格的过程,其主要思想是将离散的点云投影到一个平面上,然后对该平面进行划分,形成一系列小区域。在点云网格化方面,PCL 提供了各种算法和工具,可以实现高效的点云网格化操作。以上示例代码中,首先加载了输入的点云数据,然后通过设置合适的网格化算法参数,如搜索半径和最大三角形边长等,执行了网格化操作,最后将网格化结果保存到输出文件中。以上示例代码中,首先加载了输入的点云数据,然后通过设置合适的网格化参数,如分辨率等,执行了网格化操作,最后将网格化结果保存到输出文件中。

TechPulseZ的博客 1291

Microsemi Libero系列教程(全网首发)

关于Libero IDE和Libero SoC Libero分为Libero IDE和Libero SoC,Libero IDE最新版本是v9.2 SP3(2016年4月4日)已经不再更新维护了,而Libero SoC是Libero IDE的升级版,官方推荐的开发工具,文中的Libero指的是Libero SoC。 Libero SoC Libero® SoC是Microsemi(美高森美)官方...

whik1194的博客 2万+

『Java』排序算法整合

直接插入排序 直接插入排序(Straight Insertion Sort)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程。 public static void insertSort(int[] a...

Nono 285

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

最全排序算法整合回顾

最全排序算法回顾冒泡排序插入排序选择排序归并排序快速排序堆排序非比较排序额外部分 工程上使用的综合排序 最近打算重新过一遍算法,所以在此汇总一下常用的算法。 这次会更加注重算法的原理,剔除杂七杂八的东西,用最简单的代码体现算法的原理,希望大家可以和我一样有所收获。 冒泡排序 冒泡排序是我们接触到的第一个排序算法,原理如下: 比较相邻的元素,如果第一个比第二个大,就交换位置。 从开始一对到末尾一对...

qq_41981045的博客 342

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

java排序算法整合

小白学到的几种排序算法,记录一下方便复习 1.选择排序 public static void XuanZhe() { int min,i; for(i=0;i<a.length-1;i++) { min=i; //把选择的每一个都看作最小,然后跟后面的每一个比较, for(int j=i+1;j<a.length;j++) if(a[min]>a[j]) min=j;

weixin_44919936的博客 132

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

各种排序算法的整合

**相关代码: #include<iostream> using namespace std; //内部排序 插、交、选、归、基 /** 一、插入排序 1)直接插入排序,找到插入位置,插入位置的元素往后移动 2)折半插入排序,是对寻找插入位置的时间复杂度进行的优化,用到分治的思想 相应的每一趟中比较的次数会减少,每趟比较的次数为O(log2n); 3)希尔排序(缩小增量排序),先追求表中的元素部分有序,在逐渐逼近全局有序 */ /** 1、直接插入排序 */ void InsertSort(i

为了守护世界的和平 250

排序算法整合(冒泡,快速,希尔,拓扑,归并)

冒泡排序介绍 冒泡排序(Bubble Sort),又被称为气泡排序或泡沫排序。 它是一种较简单的排序算法。它会遍历若干次要排序的数列,每次遍历时,它都会从前往后依次的比较相邻两个数的大小;如果前者比后者大,则交换它们的位置。这样,一次遍历之后,最大的元素就在数列的末尾! 采用相同的方法再次遍历时,第二大的元素就被排列在最大元素之前。重复此操作,直到整个数列都有序为止! 冒泡排序图文说明...

Angus博客 2万+

常见排序算法总结【转载+整合】

常见排序算法总结【转载+整合】 http://hi.baidu.com/hpmm2007/item/b803125556d1caa8adc857e6 1、稳定排序和非稳定排序 简单地说就是所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,我们就说这种排序方法是稳定的。反之,就是非稳定的。要注意的是,排序算法的稳定性是针对所有输入实例而言的。即在所有可能的输入实例中,只

javaACMer的专栏 917

排序算法整合详解

快排、归并、计数排序、基数排序

weixin_44857939的博客 234

排序算法整合(C++)

排序算法整合:冒泡排序、插入排序、希尔排序、归并排序、快速排序、堆排序

linyibin_123的博客 132

排序算法 总结 整合与实践

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。* 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。快速排序 不断的根据选取的值 将元素放置在此值前后 不断操作子数组 直至子数组长度为零。// 设定一个标记,表示此次循环有没有进行交换,如果有序,跳出。//递归处理数组成两两一对,并依次返回拼起。//和有序数列作比较(倒序)//重复递归过滤大小值。

布布熊的博客 265
上一篇: 【01】Java面试----基础方面的陷阱
下一篇: Java程序员常见笔试题分析
mind_programmonkey
mind_programmonkey 领域专家: 算法与数据结构技术领域 领域专家: 算法与数据结构技术领域
博客等级 码龄9年 4435粉丝 1111原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值