《计算之魂》读书笔记 03
《计算之魂》读书笔记 03
- 1.4 关于排序的讨论
- 1.4.1 直观的排序算法时间到底浪费在哪里?
- 1.4.2 有效的排序算法效率来自哪里?
- 【思考题 1.4.1】
- 【思考题 1.4.2】
- 总结
- 参考资料
1.4 关于排序的讨论
- 排序算法在计算机算法中占有重要位置
- 根据时间复杂度,排序算法可分为两类:
- 复杂度为 O(n2)\bm{O(n^2)}O(n2):大多较直观
- 复杂度为 O(nlogn)\bm{O(nlogn)}O(nlogn):执行效率高
- 要理解排序算法,就要先掌握 递归和分治
1.4.1 直观的排序算法时间到底浪费在哪里?
假设序列有 N 个元素,存于一个数组 a[N]a[N]a[N] 中,如何对它做从小到大排序?
- 选择排序(Selection Sort):
- 每轮从序列中选出一个最大值放在最后,直到排序完毕
- 步骤1:从头到尾 (
N
) 比较相邻两个元素,小的放前面,大的放后面。第一轮扫描进行N-1
次比较和少于N-1
次交换,得到最末的最大元素 - 步骤2:从头到倒数第二个元素 (
N-1
) ,重复步骤1,每轮比之前少一个元素,以此类推 - 时间复杂度为 O(n2)\bm{O(n^2)}O(n2)
(N-1)+(N-2)+ … +1
- 动图演示(每轮选一个最小值放最前):
// 选择排序public static void selectionSort(Comparable[] data){int min;for(int index = 0 ; index < data.length - 1; index++) // 控制存储位置{min = index;for(int scan = index + 1; scan < data.length; scan++) // 查找余下数据if (data[scan].compareTo(data[min]) < 0) // 查找最小值min = scan;swap(data,min,index); // 交换赋值}}private static void swap(Comparable[] data,int index1,int index2){Comparable temp = data[index1]; // 引入一个临时变量data[index1] = data[index2];data[index2] = temp;}
- 选择排序 “太笨”,复杂度直接达到排序算法的上界
- 插入排序(Insert Sort):
- 类似打扑克 “一边抓牌 一边插牌” 的过程
- 步骤1:取最后一个元素 a[N]a[N]a[N],和第一个元素 a[1]a[1]a[1] 比较,插入并成为数组中第二个元素(需要给新元素 “留空位”),复杂度为 O(N)O(N)O(N)
- 步骤2:取第二个元素 a[N−1]a[N-1]a[N−1],依次和排好序的元素比较,找到位置后,先平移元素腾出位置再插入,以此类推,每轮排好一个数
- 时间复杂度依然是:O(n2)\bm{O(n^2)}O(n2)
- 优化:可用二分查找找到插入位置(lognlognlogn 次)
- 动图演示(从后向前扫描比较):
// 插入排序public static void insertionSort (Comparable[] data){for (int index = 1; index < data.length; index++){Comparable key = data[index];int position = index;// 哨兵临时存储while (position > 0 && data[position-1].compareTo(key) > 0){data[position] = data[position-1]; //位置全部后移position--;}data[position] = key; //合适位置}}
- 以上两种排序算法虽然直观,但无用功多,比如选择排序:
- 对所有元素都进行了两两比较
- 做了很多无用的元素位置互换
1.4.2 有效的排序算法效率来自哪里?
将序列 a[1…N] 划分成 B、C 两个子序列,每个子序列有 N/2N/2N/2 个元素,假设它们已排好序,即 b[1]b[1]b[1]、b[2]b[2]b[2]、b[N/2]b[N/2]b[N/2] 和 c[1]c[1]c[1]、c[2]c[2]c[2]、c[N/2]c[N/2]c[N/2] 均有序,如何对 B、C 排序?
- 归并排序(Merge Sort):
- 由冯·诺依曼发明,分治和递归的典型应用
- 步骤1:比较 b[1]b[1]b[1] 和 c[1]c[1]c[1],确定最小元素,赋给 a[1]a[1]a[1](合并)
- 步骤2:假设 a[1]=b[1]a[1]=b[1]a[1]=b[1],则 a[2]=min(b[2],c[1])a[2]=min(b[2],c[1])a[2]=min(b[2],c[1]),以此类推
- 步骤3:一个子序列被合并完后,另一个子序列的剩余部分直接加入序列
- B、C 子序列如何排序?重复上述过程,假定它们的前一半和后一半也分别排好了序,直到子序列中只剩一个元素(递归)
规律:第 K = logN 次排序,有 N 个子序列,每个长度是原始序列(n)的 1/N,每次排序的复杂度都是 O(n)\bm{O(n)}O(n)(最多进行 n 次比较;O(n/N∗N)O(n/N*N)O(n/N∗N))
- 时间复杂度为 O(nlogn)\bm{O(nlogn)}O(nlogn),空间复杂度为 O(n)\bm{O(n)}O(n)
- 动图演示(子序列递归合并为序列):
// 归并排序(伪代码)public static void mergeSort (Comparable[] data, int min, int max){if (min < max){int mid = (min + max) / 2;mergeSort (data, min, mid);mergeSort (data, mid+1, max);merge (data, min, mid, max);}}
- 归并排序虽然在时间复杂度上少做了无用功,但其空间复杂度不算太经济(需要两组存储空间来回替换使用)
- 后来,加拿大计算机科学家约翰·威廉斯提出 堆排序算法,拥有归并排序的时间复杂度,又不占用额外空间(就地特征;O(1)O(1)O(1))
- 接着,英国计算机科学家托尼·霍尔对相同数量级的算法做出改进,发明了比归并排序和堆排序快 2-3 倍的 快速排序算法,且空间复杂度为 O(logn)O(logn)O(logn)
- 堆排序(Heap Sort):
- 基本思想:利用堆的特性(近似完全二叉树的结构)所设计,其子节点的值总是小于(或者大于)它的父结点,以此递归地建立小顶堆或大顶堆,逐个存储有序元素
- 动图演示(建立大顶堆):
// 堆排序public static void heapSort(int[] arr){for (int i = arr.length/2-1; i >= 0 ; i--) {adjustHeap(arr,i,arr.length);}for (int j = arr.length-1; j > 0; j--) {int temp = arr[j];arr[j] = arr[0];arr[0] = temp;adjustHeap(arr,0,j);}}/*** 构建大顶堆:将以i对应的非叶子结点的子树调整成大顶堆*/public static void adjustHeap (int[] arr,int i,int length){/*取出当前非叶子结点的值保到临时变量中*/int temp = arr[i];/*j=i*2+1表示的是i结点的左子结点*/for (int j = i * 2 + 1; j < length ; j = j * 2 + 1) {if (j+1 < length && arr[j] < arr[j+1]){ //左子结点小于右子结点j++; //j指向右子结点}if (arr[j] > temp){ //子节点大于父节点arr[i] = arr[j]; //把较大的值赋值给父节点//arr[j] = temp; 这里没必要换i = j; //让i指向与其换位的子结点 因为}else{/*子树已经是大顶堆了*/break;}}arr[i] = temp;}
}
- 快速排序(Quick Sort):
- 基本思想:先选择一个数作为基准,根据基准移动交换元素,对序列分区(如小的在基准前,大的在基准后),再递归地在左右子区间选择基准进行分区,直到各区间只剩一个数
- 动图演示(选择第一个数作为 pivot):
// 快速排序public static void quickSort (Comparable[] data, int min, int max){int pivot;if (min < max){pivot = partition (data, min, max); // 选择基准quickSort(data, min, pivot-1); // 左分区排序quickSort(data, pivot+1, max); // 右分区排序}}private static int partition (Comparable[] data, int min, int max){Comparable partitionValue = data[min];int left = min;int right = max;while (left < right){while (data[left].compareTo(partitionValue) <= 0 && left < right)left++;while (data[right].compareTo(partitionValue) > 0)right--;if (left < right)swap(data, left, right);}swap (data, min, right);return right;}
- 但就 “稳定性 ”而言,归并排序比其他两种算法更胜一筹
- 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 前面
- 不稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 可能会出现在 b 后面
- 另外,在极端情况下,快速排序的时间复杂度并不高效
- 三种排序算法对比如下:
算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|
归并排序 | O(nlogn)O(nlogn)O(nlogn) | O(nlogn)O(nlogn)O(nlogn) | O(n)O(n)O(n) | √ |
堆排序 | O(nlogn)O(nlogn)O(nlogn) | O(nlogn)O(nlogn)O(nlogn) | O(1)O(1)O(1) | × |
快速排序 | O(nlogn)O(nlogn)O(nlogn) | O(n2)O(n^2)O(n2) | O(logn)O(logn)O(logn) | × |
【思考题 1.4.1】
【问】假定有 25 名短跑选手比赛争夺前三名,赛场上有五条赛道,一次可以有五名选手同时比赛。比赛并不计时,只看相应的名次。假设选手的发挥是稳定的,也就是说如果约翰比张三跑得快,张三比凯利跑得快,约翰一定比凯利跑得快。最少需要几次比赛才能决出前三名?
【答】在每位选手每次比赛都发挥稳定、不出意外、可分辨名次的情况下,最少需要 7 次比赛,步骤如下:
- 将 25 位选手分成 5 组(A、B、C、D、E)
- 组内选手先进行比赛,共 5 次,5 组选手组内排名如下(“>”表示速度):
- A1>A2>A3>A4>A5A1 > A2 > A3 > A4 > A5A1>A2>A3>A4>A5
- B1>B2>B3>B4>B5B1 > B2 > B3 > B4 > B5B1>B2>B3>B4>B5
- C1>C2>C3>C4>C5C1 > C2 > C3 > C4 > C5C1>C2>C3>C4>C5
- D1>D2>D3>D4>D5D1 > D2 > D3 > D4 > D5D1>D2>D3>D4>D5
- E1>E2>E3>E4>E5E1 > E2 > E3 > E4 > E5E1>E2>E3>E4>E5
- 每组第一名进行比赛,共 1 次,假设比赛结果为(“>”表示速度):
- A1>B1>C1>D1>E1A1 > B1 > C1 > D1 > E1A1>B1>C1>D1>E1
- 由此可决出第 1 名
- 在剩下的选手中,一共 5 人出线:
- D、E 组可直接淘汰(D1 已拿不到前3)
- C 组 C1 出线
- B 组 B1、B2 出线
- A 组 A2、A3 出线
- 这 5 人再进行 1 次 比赛,即可决出第 2、3 名(对应这次比赛第 1、2 名)
综上,要决出前三名,最少要进行 5+1+1=7 次比赛。
【思考题 1.4.2】
【问】如果有 N 个区间 [l1,r1],[l2,r2],...,[lN,rN][l_{1},r_{1}], [l_{2},r_{2}], ..., [l_{N},r_{N}][l1,r1],[l2,r2],...,[lN,rN],只要满足下面的条件我们就说这些区间是有序的:∃xi∈[li,ri]∃\,x_{i}∈[l_{i},r_{i}]∃xi∈[li,ri],其中 i=1,2,...,Ni=1,2,...,Ni=1,2,...,N
比如,[1, 4]、[2, 3] 和 [1.5, 2.5] 是有序的,因为我们可以从这三个区间中选择 1.1、2.1 和 2.2 三个数。同时 [2, 3]、[1, 4] 和 [1.5, 2.5] 也是有序的,因为我们可以选择 2.1、2.2 和 2.4。但是 [1, 2]、[2.7, 3.5] 和 [1.5, 2.5] 不是有序的。
对于任意一组区间,如何将它们进行排序?
【答】由题意可知,对于任意三个区间,如果它们有共同的交集,可互换顺序,且一直保持有序状态,因为每个区间里的浮点数是无限的,对于 N 个区间,只要有交集,总是可以从它们的交集中选择 N 个有序排列的点,例如:[1, 4]、[2, 3] 和 [1.5, 2.5] 的交集是 [2, 2.5](如下图),因此不论怎么排列有交集的三个区间,都可以从三个区间任意找出 3 个有序的点,即这三个区间有序。
按照上面的思路,每个区间都可以简化成一个随机点。对于 [1, 2]、[2.7, 3.5] 和 [1.5, 2.5],这三个区间并没有交集,但 [1, 2] 和 [1.5, 2.5] 存在交集 [1.5, 2],我们可以观察它们并集(端点)和无交集区间 [2.7, 3.5] 的位置决定顺序(如下图),我们发现:[2.7, 3.5] 在并集右端点的右侧,因此在排序时,将 [2.7, 3.5] 放在最后即可,有交集的两个区间则不需要排序。
对于三个区间,除以上两种情形外,还有一种情况(三个区间相互没有交集,但区间1和区间2、区间2和区间3有交集,如下图):
这时,如果需要排序,就需要固定三个区间的顺序了,对它们的左端点或右端点统一排序,三个点的顺序即为区间的顺序。
- 总结一下给三个区间排序的情况:
- 三个区间有交集,则三个区间自然有序(不需要排序);
- 三个区间没有交集,但其中两个区间有 1 个交集,则需要考虑有交集区间的并集之间的相对位置,即给 “没有交集区间” 和 “有交集区间的并集” 的端点排序;
- 三个区间没有交集,但其中两个区间有 2 个交集,则需要给三个区间的左端点或右端点统一排序,端点顺序即可代表区间顺序。
- 再将三个区间延伸到 N 个区间的情况,可得:
- N 个区间有交集,则这 N 个区间自然有序;
- N 个区间没有交集,但其中至少存在两个区间有 1 个交集(共有 1 ~ N-2 个交集),则需要给 “没有交集区间” 和 “有交集区间的并集” 的左端点或右端点统一排序,端点顺序即可表示区间顺序,存在交集的区间自然有序;
- N 个区间没有交集,但其中两两区间之间均有交集(共有 N-1 个交集),则需要给这 N 个区间的左端点或右端点统一排序,端点顺序即可表示区间顺序
这里,判断 N 个区间的交集个数,可能需要穷举法,确定对应情况后,给端点排序则可以使用归并排序等算法。
总结
- 通过阅读学习前两小节 5 种直观和高效的排序算法,我们基本认识了分治和递归思想及其在排序算法中的重要性,也总结出在计算机领域做事的两个原则:
- 尽可能避免做大量无用功(选择排序、插入排序)
- 在多个考量维度下,接近最优的算法可能有很多种
参考资料
- 《计算之魂》第一章
- 十大经典排序算法(动图)
- 堆排序实现
《计算之魂》读书笔记 03相关推荐
- 《Head First设计模式》 读书笔记03 装饰对象
<Head First设计模式>读书笔记03 装饰对象 问题引入 咖啡店的类设计: 一个饮料基类,各种饮料类继承这个基类,并且计算各自的价钱. 饮料中需要加入各种调料,考虑在基类中加入一些 ...
- 数据之道读书笔记-03差异化的企业数据分类管理框架
数据之道读书笔记-03差异化的企业数据分类管理框架 不同的企业或组织基于不同的目的,可以从多个角度对数据进行分类,如结构化数据和非结构化数据.内部数据和外部数据.原始数据和衍生数据.明细数据和汇总数据 ...
- 大数据之路读书笔记-03数据同步
大数据之路读书笔记-03数据同步 如第一章所述,我们将数据采集分为日志采集和数据库数据同步两部分.数据同步技术更通用的含义是不同系统间的数据流转,有多种不同的应用场景.主数据库与备份数据库之间的数据备 ...
- 构建之法读书笔记03
构建之法读书笔记03 阅读之前: 我发现这本书我越往后读越是后期软件方面的东西,好多东西因为我之前没有接触过软件,所以都变得晦涩难懂,但是大体意思我也应该明白.我知道微软但是不曾设想过他的工作体系 ...
- lz0-007 读书笔记03
03.Single-Row Functions 1.SQL 函数 •函数是SQL的一个非常强有力的特性,函数能够用于下面的目的: 执行数据计算 修改单个数据项 操纵输出进行行分组 格式化显示的日期和数 ...
- 架构之美读书笔记03
1. 系统的伸缩性需求.如大型在线游戏,需要满足大量用户.在线用户数量短时间内可能有很大的变化. 这其中隐含的需求是: 多用户并行分布式系统,系统运行在多台机器上 高可扩展性(用于加入新的故事情节,意 ...
- 硬盘和显卡的访问与控制(三)(含多彩的Hello)——《x86汇编语言:从实模式到保护模式》读书笔记03
上一篇博文我们用了很大的篇幅说了加载器,这一篇我们该说说用户程序了. 先看作者的源码吧. ;代码清单8-2;文件名:c08.asm;文件说明:用户程序 ;创建日期:2011-5-5 18:17;=== ...
- 《计算广告》读书笔记——第一章 在线广告综述
在线广告, 也称为网络广告. 互联网广告, 指的是在线媒体上投放的广告. 形成了以人群为投放目标. 以产品为导向的技术型投放模式. 在线广告开启了大规模. 自动化地利用数据改善产品和提高收入的先河. ...
- 《计算广告》读书笔记
刘鹏讲座---一文搞懂互联网广告的计算原理 DMP的用户数据从何而来 第一部分 广告市场与背景 什么是广告? 广告的根本目的是广告主通过媒体达到低成本的用户接触.----<当代广告学> 什 ...
- APUE读书笔记-03文件输入输出(2)
转载于:https://blog.51cto.com/quietheart/759609
最新文章
- 个人网站第四次改版了
- Bundle Identifier
- 最长高地(51Nod-2509)
- 商品WEB开发的商品定单与存储过程的应用
- 向github传项目
- 在java.time.LocalDateTime和java.util.Date之间进行转换
- Scratch 3.0 指令大全-“运动”类别的详细介绍②
- [Minitab]如何製作柏拉圖(Pareto chart)?
- cordova下使用高德地图js api在4g流量下定位失败问题的解决
- Zeal 面向开发者的离线文档查看工具
- 伴随着Web标准发展
- linux python 路径获取
- 如何创建项目管理工作流程?
- 转炉炼钢计算机仿真实验报告,计算机仿真、实验报告.docx
- 持续盈利背后,水滴“新增长”难寻?
- Python Selenium + PhantomJS爬取考拉海购商品数据
- 致远的OA软件有什么特点?
- 笔记本重装后回收站文件怎么恢复
- 【office培训】【王佩丰】Excel2010视频教程第4讲:排序与筛选
- 看过AI学霸们的笔记,你就明白:他们拿这样的工资有道理!| 附送六套学霸笔记...