11 - 排序(上):为什么插入排序比冒泡排序更受欢迎?
章节 | 排序算法 | 时间复杂度 | 是否基于比较 |
---|---|---|---|
11 | 冒泡、插入、选择 | O(n²) | 是 |
12 | 归并、快排 | O(nlogn) | 是 |
13 | 桶、计数、基数 | O(n) | 否 |
如何分析一个排序算法?
1.排序算法的执行效率
最好、最坏、平均情况时间复杂度
时间复杂度的系数、常数、低阶
比较次数和交换次数
2.排序算法的内存消耗(是否原地排序)
3.排序算法的稳定性(值相等的元素,排序后原有先后顺序不变)
冒泡排序(比较相邻元素,大的冒泡出来)
核心点:比较相邻的2个元素,看是否满足大小关系,不满足则对调。
表现上看,就是每次将最大/最小的数值冒出。(每次找最大)
1.冒泡排序是原地排序算法吗?
2.冒泡排序是稳定的排序算法吗?
3.冒泡排序的时间复杂度是多少?
插入排序(遍历index加入到排序区)
核心点:将序列分为排序区和非排序区。保证排序区始终有序。
1.冒泡排序是原地排序算法吗?
2.冒泡排序是稳定的排序算法吗?
3.冒泡排序的时间复杂度是多少?
选择排序(选最小的加到排序区)
核心点:与插入排序类似,也是分为排序区和非排序区。但是选择排序是从未排序区间找到最小的元素,将其放到已排序期间的末尾。(每次找最小)
1.冒泡排序是原地排序算法吗?
2.冒泡排序是稳定的排序算法吗?
3.冒泡排序的时间复杂度是多少?
为什么插入排序比冒泡排序更受欢迎呢?
插入排序的性能会优于冒泡排序。
12 - 排序(下):如何用快排思想在O(n)内查找第K大元素?
冒泡、插入、选择排序,适合小规模数据的排序。
归并排序、快速排序的时间复杂度为O(nlogn),更适合大规模的数据排序。
归并排序和快速排序,都用到了分治思想。
归并排序
核心点:将数组一分为二,分别排序,最后合并在一起。
分治是一种解决问题的处理思想,递归是一种编程技巧。
1.冒泡排序是原地排序算法吗?
2.冒泡排序是稳定的排序算法吗?
3.冒泡排序的时间复杂度是多少?
快速排序
核心点:选择任意一个元素作为分区点,分为三部分,递归划分,指到划分的小数组长度为1。
13 - 线性排序:如何根据年龄给100万用户数据排序?
三种时间复杂度是O(n)的排序算法:桶排序、计数排序、基数排序。重点是掌握这些排序算法的适用场景。
桶排序
例子:10GB订单数据,按订单金额排序
核心点:将有序的数据分到几个有序的桶里,每个桶里的数据再单独排序。
桶排序对数据的苛刻要求:
1.要排序的数据需要很容易划分成m个桶,桶之间有着天然的大小顺序。
2.数据在各个桶之间的分布比较均匀。
3.桶排序比较适合用在外部排序中。存储在外部磁盘中。
计数排序
计数排序其实是桶排序的一种特殊情况。
核心点:将数据划分成k个桶,每个桶的数据值相等。
例子:50w个考生成绩排序。
如何快速计算出,每个分数的考生在有序数组中对应的存储位置呢?
1.一个数组,存储每个分数对应的考生人数。然后对其进行顺序求和。(小于等于自己的人数)
计数排序,只能给非负整数排序,如果是其他类型,需要改为非负整数。例如,如果是小数的话乘以倍数转换成整数。如果是负数,加上一个正整数变为正数。
基数排序
例子:10w个手机号码排序。
先按照最后一位来排序,然后按倒数第二位重新排序,以此类推,最后按照最后一位重新排序。经过11次排序后,手机号码就有序了。
对于不等长的数据,需要补齐到等长。
对数据的要求:需要分割成独立的“位”来比较,位之间有递进的关系。每一位的数据范围不能太大,要可以用线性排序算法来排序,否则时间复杂度无法做到O(n)。
14 - 排序优化:如何实现一个通用的、高性能的排序函数?
如何选择合适的排序算法?
为了兼顾任意规模数据的排序,一般都会首选时间复杂度是O(nlogn)的排序算法来实现排序函数。
有归并排序、快速排序、堆排序。Java是基于堆排序实现,C是基于快排实现。
归并排序与之相比,最大的劣势就是空间复杂度高,非原地排序。
如何优化快速排序?
1.三数取中法
首、尾、中间,分别取一个数,取中间值作为分区点。数组太大时,可以五数取中、十数取中。
2.随机法
平均情况下,这样选的分区点是比较好的。
举例分析排序函数
在小规模数据面前,O(n²)时间复杂度的算法,并不一定比O(nlogn)的算法执行时间长。
15 - 二分查找(上):如何用最省内存的方式实现快速查找功能?
二分查找,针对的是有序集合,通过比较将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为0。
二分查找的递归与非递归实现
最简单的情况是不存在重复元素。
二分查找应用场景的局限性
1.依赖数据表结构,也就是数组
2.针对的是有序数据
3.数据量太小不适合
4.数据量太大不适合(数组需要申请连续的内存地址)
思考题:
1000w个整数数据,每个数据占8个字节,如何设计快速判断某个数是否在其中?内存不超过100MB
采用二分查找、散列表、二叉树都可以做,但是散列表、二叉树需要额外的内存存储数据结构,二分查找则只依赖数据,不需要额外内存。所以采用二分查找。
16 - 二分查找(下):如何快速定位IP对应的省份地址?
4种常见的二分查找变体问题
1.查找第一个值等于给定值的元素
2.查找最后一个值等于给定值的元素
3.查找第一个大于等于给定值的元素
4.查找最后一个小于等于给定值的元素
凡是用二分查找能解决的,绝大部分我们更倾向于用散列表或者二叉查找树。即便二分查找在内存使用上更节省,但是毕竟内存如此紧缺的情况并不多。
但是以上变体问题,使用二分查找则会更简单。
17 - 跳表:为什么Redis一定要用跳表来实现有序集合?
二分查找底层抵赖的是数组随机访问的特性,所以只能用数组来实现。如果数据存储在链表中,就真的没法用二分查找算法吗?
我们对链表稍加改造,就可以支持类似二分的查找算法。改造后的数据结构称为跳表。
跳表可以支持快速地插入、删除、查找操作,写起来也不复杂,甚至可以代替红黑树。
这种链表加多级索引的结构,就是跳表。
跳表的空间复杂度:O(n)
高效的动态插入和删除
跳表,不仅支持查找操作,还支持动态的插入、删除操作,而且时间复杂度都是O(logn)
跳表索引动态更新
作为一种动态数据结构,我们需要某种手段来维护索引和原始链表大小中间的平衡。也就是说,如果链表中结点多了,索引节点相应的增加,避免复杂度退化,以及查找、插入、删除操作性能下滑。
红黑树、AVL树这样平衡二叉树,是通过左右旋的方式保持左右子树的大小平衡。跳表是通过随机函数来维护平衡性。
当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。
如何选择加入哪些索引层?我们通过随机函数来决定。比如随机函数生成了值K,那我们就将这个结点添加到第一级到第K级这K级索引中。
跳表VS红黑树
1.跳表更容易代码实现,比起红黑树好懂、好写很多。简单意味着可读性好,不容易出错。
2.红黑树出现更早,很多编程语言的Map都是通过红黑树来实现的。跳表则没有现成的实现。
Redis的有序集合,一定要用跳表,因为在按区间查找时,跳表更有效率,会优于红黑树。