最详细的排序解析,理解七大排序

mp.weixin.qq.com

点击上方“方志朋”,选择“置顶或者星标”

你的关注意义重大!

注:

  1. lgN在这里为1og2N简写

  2. 为了方便描述,本文默认用int类型比较,从小到大排序

  3. 本文排序算法以java语言实现

  4. 本文的排序都是比较排序

  5. 比较次数和赋值和交换次数有的排序不好分析,可能不准确

一.插入排序

对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入

  1. 从第一个元素开始,该元素认为已经被排序;

  2. 取出下一个元素,在已排序的元素序列中从后向前扫描;

  3. 如果已排序元素大于新元素,新元素继续比较前一个元素,直到找到已排序的元素小于或者等于新元素的位置;

  4. 将新元素插入到该位置后;

  5. 重复步骤2~4。

java实现

public static void insertionSort(int[] a){int insertIndex,insertElement;for(int i = 1; i < a.length; i++){ //外层循环,默认第一个元素有序,从第二个元素开始,时间复杂度NinsertIndex = i - 1; //插入的位置,默认有序数列的最后一个元素的位置insertElement = a[i]; //新插入的元素,默认外层循环的元素while(insertIndex >= 0 && a[insertIndex] > insertElement){ //内层循环,只要新元素比待插入位置的元素小就继续,时间复杂度Na[insertIndex + 1] = a[insertIndex]; //比待插入元素大的元素后移一位insertIndex--; //插入位置前移一位}a[insertIndex + 1] = insertElement; //内层循环结束,把新元素放到插入位置后面}
}

插入排序为两层循环嵌套,时间复杂度O(N2),插入排序的while循环是先比较,移动待插入的位置,循环结束才真正交换数据位置。这里需要注意,常用的for循环嵌套进行插入排序会导致每次插入一直和前面元素交换直到插入到待插入位置,上面的内循环用while寻找待插入位置,把其他元素后移的算法更合理,每次插入只一次进行一次交换。因插入排序每次只比较一位,对于逆序较多的数组效率较低,衍生算法希尔排序,会大幅加快逆序交换,后面详细介绍。

二.选择排序

在未排序序列中找到最小元素,放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾,循环直到所有元素均排序完毕。

  1. 初始状态:无序区为R[1..n],有序区为空;

  2. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R[i..n]。该趟排序从当前无序区中选出最小的记录 R[k],将它与无序区的第1个记录R[i]交换,使R[1..i]和R[i+1..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

  3. 循环n-1次,排序完成。

java实现

public static void selectionSort(int[] a){int minIndex,temp;for(int i = 0; i < a.length - 1; i++){ //外层循环,从无序区第一个元素开始到数组倒数第二个元素,时间复杂度NminIndex = i; //每次外层循环假设无序区第一个元素是最小元素for(int j = i + 1; j < a.length; j++){  //内层循环,从假设的最小元素的后一个位置开始,到数组最后一个元素,时间复杂度Nif(a[j] < minIndex){ //判断内层循环的元素是否小于假设的最小元素 minIndex = j; //如果比最小元素小,标记该元素的位置为新的最小元素的位置,内层循环完毕,会找出无序区的最小值 }} temp = a[i];        a[i] = a[minIndex];a[minIndex] = temp;    //无序区真正最小值和第一个元素交换位置,下一次循环无序区从下一个值开始}
}

选择排序为两层for循环嵌套,内层循环始终去找最小值,放到最前面。交换次数比冒泡排序少很多,所以实际执行效率比冒泡排序快。 衍生算法,双向选择排序(每次循环,同时选出最大值放在末尾,最小值放在前方),可以提高选择效率。

三.冒泡排序

重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换

  1. 初始状态:无序区为R[1..n],有序区为空;

  2. 第i趟排序(i=0,1,2…n-1)开始时,当前无序区和有序区分别为R[0..n-i]和R[n-i+1..n]。对每一对相邻元素进行比较,从开始第一对到结尾的最后一对,如果第一个比第二个大,就交换它们两个,这样在最后的元素应该会是最大的数,使R[1..n-i-1]和R[n-i..n]分别变为记录个数减少1个的新无序区和记录个数增加1个的新有序区;

  3. 循环n-1次,直到排序完成。

java实现

public static void bubbleSort(int[] a){int temp;for(int i = 0; i < a.length - 1; i++){ //外层循环,从数组第一个元素到倒数第二个元素,时间复杂度为Nfor(int j = 0; j < a.length - 1 -i; j++){ //内层循环,从数组第一个元素到剩余的元素(减去有序区的元素)if(a[j] > a[j+1]){ temp = a[j+1];a[j+1] = a[j];a[j] = temp; //相邻元素只要前面的比后面的大,就交换位置}}}
}

冒泡排序在代码实现上是最简单的,不需要什么思考,两层for循环嵌套,比大小交换。因为冒泡通常的例子都是让大的往后移,对于刚接触排序的人来说看来上面可能认为冒泡排序与选择排序是反向操作,其实冒泡排序也可以把小数向前移,这样能明显的看出冒泡排序和选择的排序的不同,针对无序区的元素,冒泡排序总是不断地交换,而选择排序是先找出最小的元素再做一次交换。 衍生算法,鸡尾酒排序,该排序从左往右找出最大值后,再从右往左,找出最小值,类似鸡尾酒搅拌左右循环。在某些情况下,优于冒泡排序,以序列(2,3,4,5,1)为例,鸡尾酒排序只需要访问两次(升序降序各一次 )次序列就可以完成排序,但如果使用冒泡排序则需要四次。

四.希尔排序

插入排序的改进版,优先比较距离远的元素,减少交换次数

1.选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1; 2.按增量序列个数k,对序列进行k 趟排序; 3.每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

java实现

public static void shellSort(int[] a){int h = 1; //希尔排序是使间隔为h的元素有序int temp;while(h < a.length/3) { //while循环,扩大hh = 3*h + 1; //这里用3倍作为希尔排序的间隔,是常用的值,加1是为了防止排序的都是3的倍数}while(h >= 1){ //while循环让h从大到小插入排序for(int i = h; i < a.length; i++){  //从h位置开始,对整个数组遍历,i为插入元素的位置temp = a[i]; 把i当前值保存到temp中for(int j = i - h; j > 0 && a[j] > temp ;j -= h){ //遍历把i每隔h位进行比较,确定插入的位置a[j + h] = a[j];  //如果前面的元素大于temp,把前面元素的值放在后面}a[j + h] = temp; //把原来i的值赋值给前面元素,此时发生了j -=h,实际上j + h等于循环里的j}h = h/3; //更大间隔的插入完成,缩小插入间隔}
}

上面是常用的写法,每次比较,如果前面的更大都会交换,可以优化一下,直接把上面插入算法嵌入内循环,比较的间隔由1改为h,比较的时候只移动插入位置,比较完只需交换一次 java实现

public static void shellSort(int[] a){int h = 1; //希尔排序是使间隔为h的元素有序int insertIndex,insertElement;while(h < a.length/3) { //while循环,扩大hh = 3*h + 1; //这里用3倍作为希尔排序的间隔,是常用的值,加1是为了防止排序的都是3的倍数}while(h >= 1){ //while循环让h从大到小插入排序for(int i = h; i < a.length; i++){  //从h位置开始,对整个数组遍历,i为插入元素的位置insertIndex = i - h; //插入的位置,默认前面间隔h的位置insertElement = a[i]; //新插入的元素,默认外层循环的最后一个元素while(insertIndex >= 0 && a[insertIndex] > insertElement){ //内层循环,只要新元素比待插入位置的元素小就继续a[insertIndex + h] = a[insertIndex]; //比待插入元素大的元素后移h位insertIndex -= h; //插入位置前移h位}a[insertIndex + h] = insertElement; //内层循环结束,把新元素放到插入位置后面}h = h/3; //更大间隔的插入完成,缩小插入间隔}
}

希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(N²),而Hibbard增量的希尔排序的时间复杂度为O(N1.5),希尔排序时间复杂度的下界是Nlg2N。希尔排序没有快速排序算法快 O(N(lgN)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O(N²)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。

五.堆排序

堆就是完全二叉树,分为最大堆和最小堆,最大堆要求节点的元素都要不小于其孩子(最小堆要求节点元素都不大于其左右孩子),对左右孩子的大小关系不做要求,所以处于最大堆的根节点的元素一定是这个堆中的最大值。堆排序算法就是抓住了堆的这一特点,每次都取堆顶的元素,将其放在序列最后面,然后将剩余的元素重新调整为最大堆,依次类推,最终得到排序的序列。

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,则整个排序过程完成。

java实现

public static void heapSort(int[] a){int N = a.length;for(int k = N/2; k >= 1; k--){ //for循环用来构造堆,最终生成最大堆sink(a,k,N);}while(N > 1){ //循环排序无序区exch(a,1,N--); //堆顶a[1]与堆的最后一个元素a[N]交换位置,并且去掉最后一个元素到有序区,减小新堆sink(a,1,N); //重新生成为最大堆}
}/*** 从上至下堆有序化*/
private static void sink(int[] a,int k,int N){ while(2*k <= N) {int j = 2*k;if(j < N && a[j] < a[j+1]){ //j<n保证j+1不越界,a[j]和a[j+1]是a[k]的左右子节点,这里是为了选取两个子节点较大的一个,a[j]大取a[j],a[j]小取a[j++]>= a[j]) { //如果父节点大于等于值大的子节点,堆有序,终止循环break;  }if(a[k] >= a[j]) { //如果父节点大于等于值大的子节点,堆有序,终止循环break;   }exch(a,k,j); //交换值大的子节点和父节点的值,达到堆有序k = j; //子节点,作为下一个循环的父节点,继续下沉}
}/*** 交换两个元素*/
private static void exch(int[] a,int i,int j){int temp = a[i];a[i] = a[j];a[j] = temp;
}}</n保证j+1不越界,a[j]和a[j+1]是a[k]的左右子节点,这里是为了选取两个子节点较大的一个,a[j]大取a[j],a[j]小取a[j++]>

因为堆的父节点k的子节点为2k和2k+1,下标为0会导致父节点和左子节点都是0,所以上述排序的数组从下标从1开始比较方便。堆排序只需要NlgN的时间,而且算法稳定,只需要少量辅助空间,是最优的利用时间和空间的方法,但因为它的缓存命中率低,应用很少用它,多用于嵌入式。

六.归并排序

递归的把已有序列均分为两个子序列,使子序列有序,合并子序列 1.把长度为n的输入序列分成两个长度为n/2的子序列; 2.对这两个子序列分别采用归并排序; 3.将两个排序好的子序列合并成一个最终的排序序列。

java实现

private static int[] aux; //归并所需的辅助数组
public static void mergeSort(int[] a){aux = new int[a.length];sort(a,0,a.length-1); //开始递归排序
}/*** 递归的排序归并*/
private static void sort(int[] a,int left,int right){if(right <= left){ //排序从左到右,确保右边比左边大return;}int mid = (left + right)/2; //找出中间位置sort(a,left,mid); //将左半边排序sort(a,mid+1,right); //将右半边排序merge(a,left,mid,right); //归并结果
}/*** 原地归并方法*/
private static void merge(int[] a,int left,int mid,int right){ //将a[left..mid]和a[mid+1..right]归并int i = left,j = mid + 1;  //左右半边起始位置for(int k = left; k <= right; k++){ //将a[left..right]复制到aux[left..right]aux(k) = a(k);}for(int k = left; k <= right; k++){ //归并回到a[left..right]if(i > mid){  //i比mid大代表左半边用完,只有右半边有元素a[k] = aux[j++]; //右边元素给a[k]}else if(j > right){ //j比right大代表右半边用完,只有左半边有元素a[k] = aux[i++]; //左边元素给a[k]}else if(aux[j] < aux[i]){ //如果右边元素大于左边a[k] = aux[j++]; //右边元素给a[k] }else{  //否则左边大于等于右边a[k] = aux[i++]; //左边元素给a[k]}}
}

归并排序是分治法的典型应用,高效稳定,但是归并排序需要一个数组长度的辅助空间,在空间成本高的时候不适合使用

七.快速排序

通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

1.从数列中挑出一个元素,称为 “基准”(pivot); 2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作; 3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

java实现

public static void quickSort(int[] a){sort(a,0,a.length-1);
}/*** 递归进行快速排序*/
private static void sort(int[] a,int left,int right){if(right <= left){ //排序从左到右,确保右边比左边大return;}int j = partition(a,left,right); //切分sort(a,left,j-1); //将左半边排序sort(a,j+1,right); //将右半边排序
}/*** 快速排序切分*/
private static int partition(int[] a,int left,int right){ int i = left,j = right + 1; //左右扫描指针int v = a[left]; //选取切分元素,这里选第一个,实际数据可以自行挑选while(true){while(a[++i] < v){ //a[i]<v时增大i,只要比v小继续往右扫描 i="=" v="">< a[--j]){ //a[j]>v时减小j,只要比v大继续往左扫描if(j == left){ //扫描到左边则终止break;}}while(v < a[--j]){ //a[j]>v时减小j,只要比v大继续往左扫描if(j == left){ //扫描到左边则终止break;}}if(i >= j){ //如果左右指针交叉,终止循环break; }exch(a,i,j); //不满足上述条件(左边比v大,右边比v小,指针未交叉),左右元素交换位置}exch(a,left,j); //将切分元素v放入正确的位置return j; //a[left..j-1]<=a[j]<=a[j+1..right],并返回j
}/*** 交换两个元素*/
private static void exch(int[] a,int i,int j){int temp = a[i];a[i] = a[j];a[j] = temp;
}</v时增大i,只要比v小继续往右扫描>

快速排序是通常情况下的最优选择,高效,只需要lgN级别的辅助空间,但是快速排序不稳定,受切分点的影响很大

七种排序总结

上面详细介绍了七种排序的实现细节和特点,下面的表格总结了七种排序的各种特征。

其中插入排序,选择排序,冒泡排序都是简单排序,时间复杂度是O(N2),其中插入排序和冒泡排序适合原始序列有序的数组,选择排序的交换和赋值次数会比较少,可以根据不同环境和数据的实际情况和长度选择具体的排序。整体插入排序优于选择排序优于冒泡排序。希尔排序是插入排序的优化,突破了前三个排序O(N2)的时间复杂度,但是本质还是插入排序,突破比较相邻元素的惯性思维,直接比较一定间隔的元素,大幅度减少了逆序调整的比较次数和交换次数,从而达到比较理想的算法复杂度,适合对中等规模数组进行排序。堆排序是利用了最大堆的特点,始终把堆顶元素(最大元素)和最后一个元素替换,再重新构造最大堆,重复执行达到排序的效果。堆结构的特性让算法的复杂度降低到NlgN级别,但是有不方便索引元素的确定,缓存命中率较低。而归并排序则是充分运用了分治原理,把大问题不断的拆分成小问题,进行排序,再合并小数组达到整体排序的目标,归并排序即高效又可靠,唯一的缺点是需要数组长度的辅助空间,在空间成本低的时候适合使用。快速排序则解决了归并排序占用空间的问题,在数组内部用很小的辅助栈,即可完成对元素的分离,再去解决分离后的更小的数组,正常情况下拥有和归并相同级别的时间复杂度,但是得注意选取好切分元素。 实际上一个复杂的序列可能用不止一种排序,例如分治和快速排序在分割到很小的序列时再进行分割反而效率不如插入排序等简单排序,可以设置一定的阈值,先用分治或者快速排序的方式分割数组,再转换成插入等简单排序完成最终的排序。

希望本文大家加深对排序的理解有所帮助。我的微信公众号:程序之路,会持续发布技术成长文章,欢迎长按下图关注

转载于:https://www.cnblogs.com/bigben0123/p/10414202.html

最详细的排序解析,理解七大排序相关推荐

  1. 【排序】什么都能排的C语言qsort排序详解【超详细的宝藏级别教程】深度理解qsort排序

    [排序]什么都能排的C语言qsort排序详解[超详细的宝藏级别教程]深度理解qsort排序 作者: @小小Programmer 这是我的主页:@小小Programmer 在食用这篇博客之前,博主在这里 ...

  2. Java七大排序(详细总结)

    目录 注意都是从小到大排序 1. 直接插入排序 2. 希尔排序 3. 选择排序 4. 堆排序 5. 冒泡排序 6. 快速排序 7. 归并排序 8. 基数排序(了解) 总结 注意都是从小到大排序 1. ...

  3. 七大排序的个人总结(二) 归并排序(Merge

    七大排序的个人总结(二) 归并排序(Merge  归并排序(Merge Sort): 归并排序是一个相当"稳定"的算法对于其它排序算法,比如希尔排序,快速排序和堆排序而言,这些算法 ...

  4. 万字手撕七大排序(代码+动图演示)

    万字拿捏七大排序 1.排序的概念及其运用 1.1排序的概念 1.2 排序的运用 1.3 常见的排序算法 2. 常见排序算法的实现 2.1 插入排序 2.1.1 基本思想 2.1.2直接插入排序 2.1 ...

  5. 七大排序算法大汇总(上)

    目录 一.[前言]排序的稳定性: 二.七大排序总览 三.插入排序 1.1直接插入排序 1.2直接插入排序优化版--折半插入排序: 2.希尔排序 四.选择排序 1.1选择排序 1.2进阶版选择排序 2. ...

  6. 白话经典算法之七大排序

    本篇转至白话经典算法之七大排序 MoreWindows 白话经典算法之七大排序 这是本人在研一上课时所整理的文档,包括冒泡排序,直接排序这七种常用的排序方法,这些文章不仅使我在考试中取了不 错的成绩, ...

  7. Java排序(七大排序合集)

    七大排序 1.冒泡排序 1.1.排序过程图 1.2.排序思想 1.3.排序代码 1.4.代码改进 2.选择排序 2.1.排序过程图 2.2.排序思想 2.3.排序代码 2.4.代码改进--双向选择排序 ...

  8. 理解NDCG排序指标

    理解NDCG排序指标 关于NDCG,wiki给点解释很详细,这里我谈谈我的理解. NDCG(Normalized discounted cumulative gain):是用来衡量排序质量的指标. 其 ...

  9. 七大排序算法—图文详解(插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序,归并排序)

    作者:渴望力量的土狗 博客主页:渴望力量的土狗的博客主页 专栏:数据结构与算法 工欲善其事必先利其器,给大家介绍一款超牛的斩获大厂offer利器--牛客网 点击免费注册和我一起刷题吧 目录 插入排序: ...

最新文章

  1. 《iOS 6高级开发手册(第4版)》——1.11节秘诀:获取和使用设备姿势
  2. python的tkinter编写计算器_Python+Tkinter 实现计算器功能
  3. spring学习(45):util名称空间注入
  4. MSPlus DatePicker WebControl FreeVersion 1.1.0906 发布啦!
  5. 【转】一颗小小的退耦电容,引发的纠结!
  6. 知识图谱、Gremlin Traversal Language、RDF、Amazon Nepture 图数据库介绍
  7. loadrunner回放时IE模拟器弹出windows安全警告
  8. 虚拟机安装SQLService启用windows功能NetFx3时出错
  9. ise许可证怎么解决_ISE 2.4升级警报:安装的少量VM许可证比配置的VM节点
  10. Git代码提交,固定日志模板
  11. torch.distributed多卡/多GPU/分布式DPP(二)—torch.distributed.all_reduce(reduce_mean)barrier控制进程执行顺序seed随机种子
  12. Ubuntu16.04下网易云音乐点击图标打不开——已解决
  13. C++ priority_queue的使用及模拟实现
  14. 在线Java/C++/C语言/Python/web网页编辑器(IDE)整理
  15. 你别不信,安卓机用户才是苹果机涨价的最大受害者
  16. 正则表达式反解析测试工具
  17. 该网页无法正常运作localhost 目前无法处理此请求。
  18. java graphics2d 绘图_java GUI Graphics2D 绘图
  19. Ubuntu下PX4飞控开发环境搭建
  20. 收集了几个有趣的代码注释

热门文章

  1. 电脑技巧:如何彻底关闭电脑右下角闪烁弹窗广告?
  2. 学编程一定要掌握的186个关键单词!
  3. 盘点计算机世界那些经典谣言
  4. Linux文件查询笔记
  5. c#中常用集合类和集合接口之集合类系列
  6. android 属性动画变大,Android PropertyAnimation 属性动画(一)初探
  7. mysql 判断字段为null表示 false 其它为true_日拱一卒,MySQL数据库 常用SQL优化技巧 十一式...
  8. ux和ui_他们说,以UX / UI设计师的身份加入一家初创公司。 他们说,这会很有趣。
  9. java_二进制的前导的零
  10. thread线程栈size及局部变量最大可分配size【转】