11 | 排序(上)
1. 三种排序法比较
是否原地排序 | 是否稳定 | 最好 | 最坏 | 平均 | |
冒泡排序 | 是 | 是 | O(n) | O(n平方) | O(n平方) |
插入排序 | 是 | 是 | O(n) | O(n平方) | O(n平方) |
选择排序 | 是 | 否 | O(n平方) | O(n平方) | O(n平方) |
冒泡排序:从数组的头部开始,相邻两个元素做大小比较和左右互换,找出最大或最小的数,放到数组尾部。
插入排序:从数组的第二个元素开始作为目标元素,往前找自己应该处在的位置,找到后,该位置到目标元素之前的元素后移,然后目标元素插入。 (注意不是从0位置开始找,是从目标元素往前找)
选择排序:从数组的头部开始,往后遍历,找出最大或最小的元素,放在数组头部。
2. 插入排序只需要一个赋值操作,但冒泡排序需要三个赋值操作。所以插入排序更优。
3. 可以学习下排序的代码实现 https://mp.weixin.qq.com/s/v-evJ_3s787mjW43c7xrHQ,自己写着练习下。
4. 小规模数据可以使用这三种排序法,大规模数据还是使用O(nlogn) 的排序算法比较好。
12 | 排序(下)
1. 大规模的数据排序,需要归并排序和快速排序算法,它们的时间复杂度为 O(nlogn)
2. 归并排序
2.1 归并排序采取的思路是分治和递归。
(1) 将数组不断二分,分成n/2 组数据,最终每组只有2个数据
(2) 分别对每组的2个数据排序。
(3) 逐层合并各组数据。合并的方法是,两组数据进行按顺序KOF(游戏拳皇)制,放入另一个数组。
2.2 归并排序的性能分析:
(1) 是稳定的
(2) 时间复杂度都是 O(nlogn)
(3) 空间复杂度是 O(n)
3. 快速排序
3.1 快速排序采取的思路也是分治和递归
(1) 取第一个数作为pivot,把数组里比它小的放它左边,比它大的放它右边。那么数组就被分成三组,一组pivot自己,一组比它的,一组比它大的。
(2) 分别对另外两组再做一遍(1)的过程,完成后即排序完成。
3.2 快速排序的性能分析:
(1) 是不稳定的
(2) 时间复杂度都是 O(nlogn)
(3) 原地排序,空间复杂度是 O(1)
3.3 快速排序的代码实现方式(本人的描述):
(1) 取第一个数作为pivot,那么第一个数就视为一个空白格。
(2) 从右往左查找比pivot小的数 a,放入(1)中的空白格。a 之前所在的格视为新的空白格。
(3) 从左往右查找比pivot大的数 b,放入(2)里新的空白格处。b之前所在的格视为新的空白格。
(4) 重复(2)和(3),起始位置继承上一次操作的位置,直至左右指标相遇,视为结束一轮。
(5) 此pivot的左右两边视为两个小组,每个小组重复(2)至(4),直至结束。
4. 这两种排序的对比:
4.1 归并排序的处理过程是由下到上的,先处理子问题,然后再合并。
快排正好相反,它的处理过程是由上到下的,先分区,然后再处理子问题。
4.2 归并排序稳定,快排不稳定。
4.3 归并排序需要占用内存,快排原地排序。
5. 原帖地址 数据结构与算法之美_算法实战_算法面试-极客时间
13 | 线性排序
1. 如果大规模的数据符合某些要求的话,可以有几种更高效率的排序方法,时间复杂度为O(n)。分别是桶排序、计数排序、基数排序。 (时间复杂度是线性O(n),所以叫线性排序)
极客时间版权所有: https://time.geekbang.org/column/article/42038
2. 桶排序:
2.1 桶排序思想
(1) 将数据的值划分成N段,每段一个桶。
(2) 遍历数组,根据每个数的值,分到对应的桶里。
(3) 对每个桶里的数,使用快速排序法,排好序。
(4) 把所有桶的数,按顺序依次合并起来。
2.2 桶排序对数据的要求:
(1) 数据需要很容易就能划分成 N段
(2) 数据在每个桶里大致能均匀分布(否则就退化为O(N*logN))
2.3 优化: 可以动态对每个桶再进一步细分。
3. 记数排序
3.1 记数排序思想
(1) 大致上和 上面桶排序一样,也是要数据划分,但每个桶的范围不是一段,而是一个具体的数值,而且是整数。
(2) 遍历数组,根据每个数的值,分到对应的桶里。由于数值都是一样的,所以每个桶只记录一个次数就可以了。
(3) 再从后遍历数组,每个数,根据(2)中记录的次数,倒推排序时应该处在的位置。遍历完成后排序就完成了。
(4) 具体细节查看原帖地址。
3.2 记数排序对数据的要求:
(1) 只能用在数据范围不大的场景中
(2) 只能是非负整数。小数的话可以乘以10。
4. 基数排序
4.1 基数排序思想
(1) 基数排序是记数排序的进化版,处理数据范围大,不方便两数直接比较(比如字符串比较)的场景。
(2) 如果数据很大,可以把数据划分"位",比如万、千、百位。
(3) 先对"万"位进行排序(使用记数排序),再"千"位排序。必须使用稳定的排序法。
(4) 排完个位后,就完成排序了。
4.2 基数排序对数据的要求:
(1) 需要可以分割出独立的“位”来比较
(2) 位之间有递进的关系,比如"万"位比"千"位大,有递进的逻辑关系。
14 | 排序优化:如何实现一个通用的、高性能的排序函数?
1. 线性排序算法适用场景比较特殊,不能用。
2. 小规模数据,选择时间复杂度是 O(n2) 的算法.
3. 大规模数据进行排序,选择时间复杂度是 O(nlogn) 的算法.
4. 所以,选择O(nlogn)的排序算法作为通用排序函数.
15 | 二分查找(上)
1. 二分查找,时间复杂度O(logn) ,速度惊人
2. 局限性
(1) 必须是顺序表结构(即数组)
(2) 必须是有序数组(先排好序)
(3) 插入和删除操作不能太频繁
(4) 不能过于庞大,因为需要占用连续内存
16 | 二分查找(下)
1. 凡是用二分查找能解决的,大部分更倾向于用散列表或者二叉树。
但 “近似”查找的时候,用二分查找更适合。
2. 二分查找可以实现以下“近似”查找的需求:
(1) 查找第一个等于给定值的元素
(2) 查找最后一个等于给定值的元素
(3) 查找第一个大于等于给定值的元素
(4) 查找最后一个小于等于给定值的元素
17 | 跳表
1. 为对链表数据进行二分查找,可以使用跳表(Skip list)。
2. 跳表就是一个多级索引结构.
如上图所示,查询时,从第五级索引开始,往下查找。
3. 跳表的插入
(1) 不需严格按照每级索引2个数据的格式,可以直接插入成3个数据。
(2) 为避免某级索引数据过多使跳表退化成单向链表,可以使用 随机选择在第几级索引插入的方式。
4. 跳表的删除
(1) 必须要同时删除前驱结点。
(2) 使用双向链表(指向前驱结点),就比较方便
5. 跳表对比红黑树的优势
按照区间查找数据(比如查找值在 [100, 356] 之间的数据),并输出有序序列。跳表更方便。
18 | 散列表(上)
1. 把标识号转化为数组下标的方法,就叫作散列函数。
2. 散列函数计算出来的值,叫散列值。
3. 散列函数设计的好坏决定了散列冲突的概率,也就决定散列表的性能。
4. 解决散列冲突的方法:
(1) 开放寻址法: 冲突的话就拿下一个来代替。
(2) 链表法:冲突时建立一个链接,根据链表做二次查找。
19 | 散列表(中)如何打造一个工业级水平的散列表?
1. 设计一个合适的散列函数: 不能太复杂,生成的值要尽可能随机并且均匀分布
2. 定义装载因子阈值,并且设计动态扩容策略。(负载因子:装载数据数目除以总容量)
(1) 内存空间不紧张,执行效率要求高,可以降低负载因子的阈值
(2) 内存空间紧张,执行效率要求不高,可以增加负载因子的值,甚至可以大于 1
(3) 为避免扩容操作时间长影响单次插入时间,可以将扩容操作分步进行。即:
每插入一个新数据,就从旧表搬移一个数据到新表。查询时,先查新表再查旧表。
3. 选择合适的散列冲突解决方法
(1) 数据量比较小、装载因子小的时候,适合采用开放寻址法.
数据放在连续数组中,能利用CPU缓存
(2) 数据量比较大、装载因子大的时候的散列表,适合采用链表法。
这时采用开放寻址法的话,因为装载因子大,容易导致大量的散列冲突。
链表法为解决优化散列冲突,可以采用红黑树。
20 | 散列表(下):为什么散列表和链表经常会一起使用?
答: 为了同时支持下面两个需求:
1. 高效的数据插入、删除、查找操作
2. 按照某种顺序快速地遍历数据(比如插入顺序)