数据结构与算法之美
王争
前Google工程师
立即订阅
71638 人已学习
课程目录
已完结 75 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 从今天起,跨过“数据结构与算法”这道坎
免费
入门篇 (4讲)
01 | 为什么要学习数据结构和算法?
02 | 如何抓住重点,系统高效地学习数据结构与算法?
03 | 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
04 | 复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度
基础篇 (38讲)
05 | 数组:为什么很多编程语言中数组都从0开始编号?
06 | 链表(上):如何实现LRU缓存淘汰算法?
07 | 链表(下):如何轻松写出正确的链表代码?
08 | 栈:如何实现浏览器的前进和后退功能?
09 | 队列:队列在线程池等有限资源池中的应用
10 | 递归:如何用三行代码找到“最终推荐人”?
11 | 排序(上):为什么插入排序比冒泡排序更受欢迎?
12 | 排序(下):如何用快排思想在O(n)内查找第K大元素?
13 | 线性排序:如何根据年龄给100万用户数据排序?
14 | 排序优化:如何实现一个通用的、高性能的排序函数?
15 | 二分查找(上):如何用最省内存的方式实现快速查找功能?
16 | 二分查找(下):如何快速定位IP对应的省份地址?
17 | 跳表:为什么Redis一定要用跳表来实现有序集合?
18 | 散列表(上):Word文档中的单词拼写检查功能是如何实现的?
19 | 散列表(中):如何打造一个工业级水平的散列表?
20 | 散列表(下):为什么散列表和链表经常会一起使用?
21 | 哈希算法(上):如何防止数据库中的用户信息被脱库?
22 | 哈希算法(下):哈希算法在分布式系统中有哪些应用?
23 | 二叉树基础(上):什么样的二叉树适合用数组来存储?
24 | 二叉树基础(下):有了如此高效的散列表,为什么还需要二叉树?
25 | 红黑树(上):为什么工程中都用红黑树这种二叉树?
26 | 红黑树(下):掌握这些技巧,你也可以实现一个红黑树
27 | 递归树:如何借助树来求解递归算法的时间复杂度?
28 | 堆和堆排序:为什么说堆排序没有快速排序快?
29 | 堆的应用:如何快速获取到Top 10最热门的搜索关键词?
30 | 图的表示:如何存储微博、微信等社交网络中的好友关系?
31 | 深度和广度优先搜索:如何找出社交网络中的三度好友关系?
32 | 字符串匹配基础(上):如何借助哈希算法实现高效字符串匹配?
33 | 字符串匹配基础(中):如何实现文本编辑器中的查找功能?
34 | 字符串匹配基础(下):如何借助BM算法轻松理解KMP算法?
35 | Trie树:如何实现搜索引擎的搜索关键词提示功能?
36 | AC自动机:如何用多模式串匹配实现敏感词过滤功能?
37 | 贪心算法:如何用贪心算法实现Huffman压缩编码?
38 | 分治算法:谈一谈大规模计算框架MapReduce中的分治思想
39 | 回溯算法:从电影《蝴蝶效应》中学习回溯算法的核心思想
40 | 初识动态规划:如何巧妙解决“双十一”购物时的凑单问题?
41 | 动态规划理论:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题
42 | 动态规划实战:如何实现搜索引擎中的拼写纠错功能?
高级篇 (9讲)
43 | 拓扑排序:如何确定代码源文件的编译依赖关系?
44 | 最短路径:地图软件是如何计算出最优出行路径的?
45 | 位图:如何实现网页爬虫中的URL去重功能?
46 | 概率统计:如何利用朴素贝叶斯算法过滤垃圾短信?
47 | 向量空间:如何实现一个简单的音乐推荐系统?
48 | B+树:MySQL数据库索引是如何实现的?
49 | 搜索:如何用A*搜索算法实现游戏中的寻路功能?
50 | 索引:如何在海量数据中快速查找某个数据?
51 | 并行算法:如何利用并行处理提高算法的执行效率?
实战篇 (5讲)
52 | 算法实战(一):剖析Redis常用数据类型对应的数据结构
53 | 算法实战(二):剖析搜索引擎背后的经典数据结构和算法
54 | 算法实战(三):剖析高性能队列Disruptor背后的数据结构和算法
55 | 算法实战(四):剖析微服务接口鉴权限流背后的数据结构和算法
56 | 算法实战(五):如何用学过的数据结构和算法实现一个短网址系统?
加餐:不定期福利 (6讲)
不定期福利第一期 | 数据结构与算法学习书单
不定期福利第二期 | 王争:羁绊前行的,不是肆虐的狂风,而是内心的迷茫
不定期福利第三期 | 测一测你的算法阶段学习成果
不定期福利第四期 | 刘超:我是怎么学习《数据结构与算法之美》的?
总结课 | 在实际开发中,如何权衡选择使用哪种数据结构和算法?
《数据结构与算法之美》学习指导手册
加餐:春节7天练 (7讲)
春节7天练 | Day 1:数组和链表
春节7天练 | Day 2:栈、队列和递归
春节7天练 | Day 3:排序和二分查找
春节7天练 | Day 4:散列表和字符串
春节7天练 | Day 5:二叉树和堆
春节7天练 | Day 6:图
春节7天练 | Day 7:贪心、分治、回溯和动态规划
加餐:用户学习故事 (2讲)
用户故事 | Jerry银银:这一年我的脑海里只有算法
用户故事 | zixuan:站在思维的高处,才有足够的视野和能力欣赏“美”
结束语 (3讲)
结束语 | 送君千里,终须一别
第2季回归 | 这一次,我们一起拿下设计模式!
打卡召集令 | 60 天攻克数据结构与算法
免费
数据结构与算法之美
登录|注册

11 | 排序(上):为什么插入排序比冒泡排序更受欢迎?

王争 2018-10-15
排序对于任何一个程序员来说,可能都不会陌生。你学的第一个算法,可能就是排序。大部分编程语言中,也都提供了排序函数。在平常的项目中,我们也经常会用到排序。排序非常重要,所以我会花多一点时间来详细讲一讲经典的排序算法。
排序算法太多了,有很多可能你连名字都没听说过,比如猴子排序、睡眠排序、面条排序等。我只讲众多排序算法中的一小撮,也是最经典的、最常用的:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。我按照时间复杂度把它们分成了三类,分三节课来讲解。
带着问题去学习,是最有效的学习方法。所以按照惯例,我还是先给你出一个思考题:插入排序和冒泡排序的时间复杂度相同,都是 O(n2),在实际的软件开发里,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?
你可以先思考一两分钟,带着这个问题,我们开始今天的内容!

如何分析一个“排序算法”?

学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。那分析一个排序算法,要从哪几个方面入手呢?

排序算法的执行效率

对于排序算法执行效率的分析,我们一般会从这几个方面来衡量:
1. 最好情况、最坏情况、平均情况时间复杂度
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《数据结构与算法之美》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(267)

  • 双木公子 置顶
    对于老师所提课后题,觉得应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。

    作者回复: 👍 回答的很好 可以作为标准答案了 同学们把这条顶上去吧

    2018-10-15
    25
    906
  • Monday
    本节从昨天更新到今天,一共前前后后认认真真听了五遍,再到今天晚上花3小时把3个排序算法实现,做了冒泡排序与插入排序的测试实验。随机生成二维数组a[200][10000]和b[200][10000](a,b数组数据一致),然后在我的机器上分别用冒泡和插入排序算法来排序(a数组冒泡,b数组插入),冒泡排序算法大约 16332ms 才能执行完成,而插入排序只需要 2228ms 左右。
    总结一句:听五遍不如敲一遍!
    2018-10-16
    1
    164
  • 德拉
    有同学提到的算法过程动态图,可以看看这个https://visualgo.net/
    2018-10-27
    2
    117
  • myrabbit
    王老师,我发现你文章中的图画的很漂亮,字也写得很漂亮,图文结合的形式对于表达的帮助真的很大!有时候做笔记也可以用此方法,请问你的图文是用什么软件画的?

    作者回复: 不是我画的 大编辑画的

    2018-10-15
    4
    67
  • Smallfly
    二刷了下排序,有了一些新的体会。

    冒泡、插入、选择排序都有一个共同点,将待排序数列分为已排序和未排序两部分。在未排序的部分中查找一个最值,放到已排序数列的恰当位置。

    具体到代码层面,外层循环的变量用于分割已排序和未排序数,内层循环的变量用于在未排序数中查找。从思路上看,这三种算法其实是一样的,所以时间复杂度也相同。
    2019-03-02
    2
    50
  • 靑城
    总结

    一、排序方法与复杂度归类
    (1)几种最经典、最常用的排序方法:冒泡排序、插入排序、选择排序、快速排序、归并排序、计数排序、基数排序、桶排序。
    (2)复杂度归类
    冒泡排序、插入排序、选择排序 O(n^2)
    快速排序、归并排序 O(nlogn)
    计数排序、基数排序、桶排序 O(n)

    二、如何分析一个“排序算法”?
    <1>算法的执行效率
    1. 最好、最坏、平均情况时间复杂度。
    2. 时间复杂度的系数、常数和低阶。
    3. 比较次数,交换(或移动)次数。
    <2>排序算法的稳定性
    1. 稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
    2. 稳定性重要性:可针对对象的多种属性进行有优先级的排序。
    3. 举例:给电商交易系统中的“订单”排序,按照金额大小对订单数据排序,对于相同金额的订单以下单时间早晚排序。用稳定排序算法可简洁地解决。先按照下单时间给订单排序,排序完成后用稳定排序算法按照订单金额重新排序。
    <3>排序算法的内存损耗
    原地排序算法:特指空间复杂度是O(1)的排序算法。

    三、冒泡排序
           冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求,如果不满足就让它俩互换。
    稳定性:冒泡排序是稳定的排序算法。
    空间复杂度:冒泡排序是原地排序算法。
    时间复杂度:
    1. 最好情况(满有序度):O(n)。
    2. 最坏情况(满逆序度):O(n^2)。
    3. 平均情况:
           “有序度”和“逆序度”:对于一个不完全有序的数组,如4,5,6,3,2,1,有序元素对为3个(4,5),(4,6),(5,6),有序度为3,逆序度为12;对于一个完全有序的数组,如1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是15,称作满有序度;逆序度=满有序度-有序度;冒泡排序、插入排序交换(或移动)次数=逆序度。
           最好情况下初始有序度为n*(n-1)/2,最坏情况下初始有序度为0,则平均初始有序度为n*(n-1)/4,即交换次数为n*(n-1)/4,因交换次数<比较次数<最坏情况时间复杂度,所以平均时间复杂度为O(n^2)。

    四、插入排序
           插入排序将数组数据分成已排序区间和未排序区间。初始已排序区间只有一个元素,即数组第一个元素。在未排序区间取出一个元素插入到已排序区间的合适位置,直到未排序区间为空。
    空间复杂度:插入排序是原地排序算法。
    时间复杂度:
    1. 最好情况:O(n)。
    2. 最坏情况:O(n^2)。
    3. 平均情况:O(n^2)(往数组中插入一个数的平均时间复杂度是O(n),一共重复n次)。
    稳定性:插入排序是稳定的排序算法。

    五、选择排序
           选择排序将数组分成已排序区间和未排序区间。初始已排序区间为空。每次从未排序区间中选出最小的元素插入已排序区间的末尾,直到未排序区间为空。
    空间复杂度:选择排序是原地排序算法。
    时间复杂度:(都是O(n^2))
    1. 最好情况:O(n^2)。
    2. 最坏情况:O(n^2)。
    3. 平均情况:O(n^2)。
    稳定性:选择排序不是稳定的排序算法。

    思考
           选择排序和插入排序的时间复杂度相同,都是O(n^2),在实际的软件开发中,为什么我们更倾向于使用插入排序而不是冒泡排序算法呢?
           答:从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个,所以在对相同数组进行排序时,冒泡排序的运行时间理论上要长于插入排序。
    2018-10-15
    48
  • 陈问渔
    https://mp.weixin.qq.com/s/HQg3BzzQfJXcWyltsgOfCQ

    这里面的图解排序算法,很形象。java实现的代码
    2018-11-22
    36
  • 姜威
    总结:
    一、几种经典排序算法及其时间复杂度级别
    冒泡、插入、选择 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-22
    27
  • 流风之回雪
    a[j+1] = value; // 插入数据,这条语句弄了好久才明白,一直以为 j的值最小为0,那么a[j+1]最小就是a[1],不过这样赋值逻辑上就有问题,后来debug了一下,发现j是可以为-1的,a[j+1]最小为a[0],这样逻辑上就通了,果然多敲代码才能弄明白勒

    作者回复: 👍钻研精神

    2018-11-02
    6
    18
  • allean
    每一次看文章都要至少看三遍,代码实现也至少写三遍,不是追求量,是真的感觉每一次的体会都更加不一样😁

    作者回复: 👍

    2018-11-13
    17
  • 醉比
    大家多思考多吸收吧。。。。我得多吸收一会
    2018-10-15
    13
  • oldman
    我用python实现了冒泡排序,插入排序,选择排序。地址如下,欢迎大家一起探讨:
    冒泡排序:https://github.com/lipeng1991/testdemo/blob/master/40_bubble_sort.py
    插入排序:https://github.com/lipeng1991/testdemo/blob/master/41_insert_sort.py
    选择排序: https://github.com/lipeng1991/testdemo/blob/master/42_select_sort.py
    2018-10-16
    12
  • 三种排序算法不涉及随机读取,所以链表是可以实现的,而且时间复杂度空间空间复杂度和数组一样,O(n*n),O(1).
    2018-10-15
    10
  • Jo
    冒泡排序的外层循环次数只需要n-1次,此时第1个数字在上一次已经比较过,肯定比第2个小(或大),所以第n次没必要比较了
    2018-10-15
    9
  • 姜威
    五、选择排序
    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-22
    4
    7
  • 安静
    每天坐地铁看一节都有点坚持不下来了,加油。
    2018-11-09
    6
  • 冒泡排序的应该重复n-1次就有序了
    2018-12-22
    5
  • zj
    订单那个题目为什么要先按照订单时间排序,再按照金额呢?我先按照金额,再按订单时间有什么不一样么

    作者回复: 我不是解释了嘛 先金额再时间 代码写起来比较麻烦

    2018-11-07
    2
    5
  • 王木公
    感觉有个问题始终没有解决。前人是如何想出的这些算法?或者说是在怎样的环境下,作者经历了怎样的心路历程想出了这个算法。我认为知道这个很重要,尽管现在学这些算法觉得理所应当,但当时间久了仍然会忘记,尤其是那些细节临界点,人的大脑适合记忆有关联性的东西,这些算法则属于不擅长记忆的创造性内容,如果没有历史那些前提,相信很难根本性掌握。

    作者回复: 很难知道人家是怎么想到的,你要求有点高了,说不定灵机一动就想到了。

    2019-08-08
    2
    4
  • ILoveKindness
    老师您好,我不是很懂您所置顶的答案中插入排序后要倒置链表的意思,请求解答。

    作者回复: 应该是不需要倒置的。

    2019-06-03
    4
收起评论
99+
返回
顶部