11 | 排序(上):为什么插入排序比冒泡排序更受欢迎?
王争
该思维导图由 AI 生成,仅供参考
排序对于任何一个程序员来说,可能都不会陌生。你学的第一个算法,可能就是排序。大部分编程语言中,也都提供了排序函数。在平常的项目中,我们也经常会用到排序。排序非常重要,所以我会花多一点时间来详细讲一讲经典的排序算法。
排序算法太多了,有很多可能你连名字都没听说过,比如猴子排序、睡眠排序、面条排序等。我只讲众多排序算法中的一小撮,也是最经典的、最常用的:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。我按照时间复杂度把它们分成了三类,分三节课来讲解。
带着问题去学习,是最有效的学习方法。所以按照惯例,我还是先给你出一个思考题:插入排序和冒泡排序的时间复杂度相同,都是 O(n2),在实际的软件开发里,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?
你可以先思考一两分钟,带着这个问题,我们开始今天的内容!
如何分析一个“排序算法”?
学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。那分析一个排序算法,要从哪几个方面入手呢?
排序算法的执行效率
对于排序算法执行效率的分析,我们一般会从这几个方面来衡量:
1. 最好情况、最坏情况、平均情况时间复杂度
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
排序算法的时间复杂度分析及性能比较 本文深入介绍了冒泡排序、插入排序和选择排序三种时间复杂度为O(n^2)的排序算法。作者首先通过具体的例子生动地展示了插入排序和选择排序的操作过程,然后针对这三种排序算法的特点提出了三个问题,分别是排序算法是否为原地排序算法、是否为稳定的排序算法以及其时间复杂度。作者通过对有序度和逆序度的概念进行解释,结合具体例子对这三种排序算法的时间复杂度进行了推导和分析。文章还对排序算法的执行效率、内存消耗和稳定性进行了分析,指出插入排序在性能优化方面具有较大的优势。此外,作者还对排序算法在不同数据结构下的适用性进行了思考。 总的来说,本文通过对三种排序算法的原理、实现和性能进行全面的分析和解释,为读者提供了对排序算法的深入了解和评价方法。文章内容详实,对排序算法进行了全面的分析和解释,对读者了解排序算法具有一定的指导意义。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《数据结构与算法之美》,新⼈⾸单¥68
《数据结构与算法之美》,新⼈⾸单¥68
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(443)
- 最新
- 精选
- 双木公子置顶对于老师所提课后题,觉得应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。
作者回复: 👍 回答的很好 可以作为标准答案了 同学们把这条顶上去吧
2018-10-15471874 - myrabbit王老师,我发现你文章中的图画的很漂亮,字也写得很漂亮,图文结合的形式对于表达的帮助真的很大!有时候做笔记也可以用此方法,请问你的图文是用什么软件画的?
作者回复: 不是我画的 大编辑画的
2018-10-159105 - 流风之回雪a[j+1] = value; // 插入数据,这条语句弄了好久才明白,一直以为 j的值最小为0,那么a[j+1]最小就是a[1],不过这样赋值逻辑上就有问题,后来debug了一下,发现j是可以为-1的,a[j+1]最小为a[0],这样逻辑上就通了,果然多敲代码才能弄明白勒
作者回复: 👍钻研精神
2018-11-021560 - allean每一次看文章都要至少看三遍,代码实现也至少写三遍,不是追求量,是真的感觉每一次的体会都更加不一样😁
作者回复: 👍
2018-11-13258 - 姜威总结: 一、几种经典排序算法及其时间复杂度级别 冒泡、插入、选择 O(n^2) 基于比较 快排、归并 O(nlogn) 基于比较 计数、基数、桶 O(n) 不基于比较 二、如何分析一个排序算法? 1.学习排序算法的思路?明确原理、掌握实现以及分析性能。 2.如何分析排序算法性能?从执行效率、内存消耗以及稳定性3个方面分析排序算法的性能。 3.执行效率:从以下3个方面来衡量 1)最好情况、最坏情况、平均情况时间复杂度 2)时间复杂度的系数、常数、低阶:排序的数据量比较小时考虑 3)比较次数和交换(或移动)次数 4.内存消耗:通过空间复杂度来衡量。针对排序算法的空间复杂度,引入原地排序的概念,原地排序算法就是指空间复杂度为O(1)的排序算法。 5.稳定性:如果待排序的序列中存在值等的元素,经过排序之后,相等元素之间原有的先后顺序不变,就说明这个排序算法时稳定的。 三、冒泡排序 1.排序原理 1)冒泡排序只会操作相邻的两个数据。 2)对相邻两个数据进行比较,看是否满足大小关系要求,若不满足让它俩互换。 3)一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。 4)优化:若某次冒泡不存在数据交换,则说明已经达到完全有序,所以终止冒泡。 2.代码实现(见下一条留言) 3.性能分析 1)执行效率:最小时间复杂度、最大时间复杂度、平均时间复杂度 最小时间复杂度:数据完全有序时,只需进行一次冒泡操作即可,时间复杂度是O(n)。 最大时间复杂度:数据倒序排序时,需要n次冒泡操作,时间复杂度是O(n^2)。 平均时间复杂度:通过有序度和逆序度来分析。 什么是有序度? 有序度是数组中具有有序关系的元素对的个数,比如[2,4,3,1,5,6]这组数据的有序度就是11,分别是[2,4][2,3][2,5][2,6][4,5][4,6][3,5][3,6][1,5][1,6][5,6]。同理,对于一个倒序数组,比如[6,5,4,3,2,1],有序度是0;对于一个完全有序的数组,比如[1,2,3,4,5,6],有序度为n*(n-1)/2,也就是15,完全有序的情况称为满有序度。 什么是逆序度?逆序度的定义正好和有序度相反。核心公式:逆序度=满有序度-有序度。 排序过程,就是有序度增加,逆序度减少的过程,最后达到满有序度,就说明排序完成了。 冒泡排序包含两个操作原子,即比较和交换,每交换一次,有序度加1。不管算法如何改进,交换的次数总是确定的,即逆序度。 对于包含n个数据的数组进行冒泡排序,平均交换次数是多少呢?最坏的情况初始有序度为0,所以要进行n*(n-1)/2交换。最好情况下,初始状态有序度是n*(n-1)/2,就不需要进行交互。我们可以取个中间值n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。 换句话说,平均情况下,需要n*(n-1)/4次交换操作,比较操作可定比交换操作多,而复杂度的上限是O(n^2),所以平均情况时间复杂度就是O(n^2)。 以上的分析并不严格,但很实用,这就够了。 2)空间复杂度:每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法。 3)算法稳定性:如果两个值相等,就不会交换位置,故是稳定排序算法。 四、插入排序 1.算法原理 首先,我们将数组中的数据分为2个区间,即已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想就是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间中的元素一直有序。重复这个过程,直到未排序中元素为空,算法结束。 2.代码实现(见下一条留言) 3.性能分析 1)时间复杂度:最好、最坏、平均情况 如果要排序的数组已经是有序的,我们并不需要搬移任何数据。只需要遍历一遍数组即可,所以时间复杂度是O(n)。如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,因此时间复杂度是O(n^2)。而在一个数组中插入一个元素的平均时间复杂都是O(n),插入排序需要n次插入,所以平均时间复杂度是O(n^2)。 2)空间复杂度:从上面的代码可以看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),是原地排序算法。 3)算法稳定性:在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现的元素的后面,这样就保持原有的顺序不变,所以是稳定的。
作者回复: 👍
2018-10-2247 - 王木公感觉有个问题始终没有解决。前人是如何想出的这些算法?或者说是在怎样的环境下,作者经历了怎样的心路历程想出了这个算法。我认为知道这个很重要,尽管现在学这些算法觉得理所应当,但当时间久了仍然会忘记,尤其是那些细节临界点,人的大脑适合记忆有关联性的东西,这些算法则属于不擅长记忆的创造性内容,如果没有历史那些前提,相信很难根本性掌握。
作者回复: 很难知道人家是怎么想到的,你要求有点高了,说不定灵机一动就想到了。
2019-08-081121 - 姜威五、选择排序 1.算法原理 选择排序算法也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,并将其放置到已排序区间的末尾。 2.代码实现(见下一条留言) 3.性能分析 1)时间复杂度:最好、最坏、平均情况 选择排序的最好、最坏、平均情况时间复杂度都是O(n^2)。为什么?因为无论是否有序,每个循环都会完整执行,没得商量。 2)空间复杂度: 选择排序算法空间复杂度是O(1),是一种原地排序算法。 3)算法稳定性: 选择排序算法不是一种稳定排序算法,比如[5,8,5,2,9]这个数组,使用选择排序算法第一次找到的最小元素就是2,与第一个位置的元素5交换位置,那第一个5和中间的5的顺序就变量,所以就不稳定了。正因如此,相对于冒泡排序和插入排序,选择排序就稍微逊色了。 六、思考 1.冒泡排序和插入排序的时间复杂度都是 O(n^2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢? 冒泡排序移动数据有3条赋值语句,而选择排序的交换位置的只有1条赋值语句,因此在有序度相同的情况下,冒泡排序时间复杂度是选择排序的3倍,所以,选择排序性能更好。 2.如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢? 代码实现: /** * 冒泡排序 * @param a 待排序数组 * @param n 数组长度 */ public static void bubbleSort(int[] a, int n) { if(n<=0) return ; for (int i = 0; i < n; i++) { //标记一次冒泡是否存在数据交换,若存在,则改为true boolean tag = false; for (int j = 0; j < n-1-i; j++) { if(a[j] > a[j+1]){ int temp = a[j]; a[j] = a[j+1]; a[j+1] = temp; tag = true; } } //若本次冒泡操作未发生数据交换,则终止冒泡操作 if (tag == false) break; } } /** * 插入排序 * @param a 待排序数组 * @param n 表示数组大小 */ public static void insertSort(int[] a, int n) { if(n<=1) return; for(int i=1;i<n;i++){ int value=a[i]; int j=i-1; //找到插入位置 for(;j>0;j--){ if(a[j]>value){ a[j+1]=a[j];//移动数据 } else { break; } } a[j+1]=value;//插入数据 } } /** * 选择排序 * @param a 待排序数组 * @param n 数组长度 */ public static void selectSort(int[] a, int n) { if(n<=0) return; for(int i=0;i<n;i++){ int min=i; for(int j=i;j<n;j++){ if(a[j] < a[min]) min=j; } if(min != i){ int temp=a[i]; a[i]=a[min]; a[min]=temp; } } }
作者回复: 👍
2018-10-22516 - ILoveKindness老师您好,我不是很懂您所置顶的答案中插入排序后要倒置链表的意思,请求解答。
作者回复: 应该是不需要倒置的。
2019-06-03314 - z老师,可不可以将排序章节的示例图,换成动态的图,那样更形象些,还能加深印象
作者回复: 同学要求太高了,整不来,凑活着看吧
2018-10-1546 - zj订单那个题目为什么要先按照订单时间排序,再按照金额呢?我先按照金额,再按订单时间有什么不一样么
作者回复: 我不是解释了嘛 先金额再时间 代码写起来比较麻烦
2018-11-0765
收起评论