08排序(上):为什么插入排序比冒泡排序更受欢迎?
1. 排序方法与复杂度归类
(1)几种最经典、最常用的排序方法:冒泡排序、插入排序、选择排序、快速排序、归并排序、计数排序、基数排序、桶排序。
(2)复杂度归类
冒泡、插入、选择 O(n^2) 基于比较
快排、归并 O(nlogn) 基于比较
计数、基数、桶 O(n) 不基于比较
2. 如何分析一个“排序算法”?
- 算法的执行效率
1)最好情况、最坏情况、平均情况时间复杂度
2)时间复杂度的系数、常数、低阶:排序的数据量比较小时考虑
3)比较次数和交换(或移动)次数 - 排序算法的稳定性
- 稳定性概念:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,就说明这个排序算法时稳定的。
- 稳定性重要性:可针对对象的多种属性进行有优先级的排序。
- 举例:给电商交易系统中的“订单”排序,按照金额大小对订单数据排序,对于相同金额的订单以下单时间早晚排序。用稳定排序算法可简洁地解决。先按照下单时间给订单排序,排序完成后用稳定排序算法按照订单金额重新排序。
- 排序算法的内存损耗
通过空间复杂度来衡量。针对排序算法的空间复杂度,引入原地排序的概念,原地排序算法就是指空间复杂度为O(1)的排序算法。
3. 冒泡排序
3.1. 排序原理
1)冒泡排序只会操作相邻的两个数据。
2)对相邻两个数据进行比较,看是否满足大小关系要求,若不满足让它俩互换。
3)一次冒泡会让至少一个元素移动到它应该在的位置,重复n-1次,就完成了n个数据的排序工作。
4)优化:若某次冒泡不存在数据交换,则说明已经达到完全有序,所以终止冒泡。
3.2. 代码实现(Python)
def bubble_sort(collection):
"""冒泡排序"""
length = len(collection)
for i in range(length-1): # 只要冒泡length-1轮就可以排好序
swapped = False # 记录是否发生交换
for j in range(length-1-i):
if collection[j] > collection[j+1]:
swapped = True
collection[j], collection[j+1] = collection[j+1], collection[j]
if not swapped: # 没有发生交换,说明已经排好序
break
return collection
if __name__ == '__main__':
collection = list(map(int, input().split()))
print('排序前:', end='')
for i in collection:
print(i, end=' ')
collection = bubble_sort(collection)
print('\n排序后:', end='')
for i in collection:
print(i, end=' ')
3.3. 性能分析
1)执行效率:最好时间复杂度、最坏时间复杂度、平均时间复杂度
最好时间复杂度:数据完全有序时,只需进行一次冒泡操作即可,时间复杂度是O(n)。
最坏时间复杂度:数据倒序排序时,需要n次冒泡操作,时间复杂度是O(n^2)。
平均时间复杂度:通过有序度和逆序度来分析,时间复杂度是O(n^2)。
2)空间复杂度:每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法。
3)算法稳定性:如果两个值相等,就不会交换位置,故是稳定排序算法。
4. 有序度&无序度&满有序度
- 什么是有序度?
有序度是数组中具有有序关系的元素对的个数,比如[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(n2),所以平均情况时间复杂度就是O(n2)。以上的分析并不严格,但很实用,这就够了。
5. 插入排序
5.1. 插入排序原理
插入排序将数组数据分成已排序区间和未排序区间。初始已排序区间只有一个元素,即数组第一个元素。在未排序区间取出一个元素插入到已排序区间的合适位置,直到未排序区间为空。
5.2. 代码实现
def insertion_sort(collection):
"""插入排序"""
length = len(collection)
for i in range(1, length): # 第一个数作为初始有序序列,所以不用比较
j = i - 1
while j >= 0 and collection[j + 1] < collection[j]:
collection[j], collection[j + 1] = collection[j + 1], collection[j]
j -= 1
return collection
def insertion_sort2(collection):
"""插入排序"""
length = len(collection)
for i in range(1, length): # 第一个数作为初始有序序列,所以不用比较
j = i - 1
tmp = collection[i]
while j >= 0 and collection[j] > tmp: # 插入合适的位置
collection[j + 1] = collection[j]
j -= 1
collection[j+1] = tmp
return collection
if __name__ == '__main__':
collection = list(map(int, input().split()))
print('排序前是:', end='')
for i in collection:
print(i, end=' ')
collection = insertion_sort2(collection)
print('\n排序后是:', end='')
for i in collection:
print(i, end=' ')
5.3. 性能分析
1)执行效率:最好时间复杂度、最坏时间复杂度、平均时间复杂度
最好时间复杂度:数据完全有序时,只需进行一次冒泡操作即可,时间复杂度是O(n)。
最坏时间复杂度:数据倒序排序时,时间复杂度是O(n^2),往数组中插入一个数的平均时间复杂度是O(n),一共重复n次。
平均时间复杂度:通过有序度和逆序度来分析,时间复杂度是O(n^2)。
2)空间复杂度:每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法。
3)算法稳定性:如果两个值相等,就不会交换位置,故是稳定排序算法。
6. 选择排序
6.1. 选择排序原理
选择排序将数组分成已排序区间和未排序区间。初始已排序区间为空。每次从未排序区间中选出最小的元素插入已排序区间的末尾,直到未排序区间为空。
6.2 代码实现
def selection_sort(collecion):
"""选择排序"""
length = len(collection)
for i in range(length -1): # 比较length-1轮就可以排好序
least = i # 保存每轮比较中的最小数
for j in range(i+1, length):
if collection[j] < collection[least]:
least = j
collection[i], collection[least] = collection[least], collection[i]
return collection
if __name__ == '__main__':
collection = list(map(int, input().split()))
print('排序前:', end='')
for i in collection:
print(i, end=' ')
collection = selection_sort(collection)
print('\n排序后:', end='')
for i in collection:
print(i, end=' ')
6.3 选择排序性能分析
1)执行效率:最好时间复杂度、最坏时间复杂度、平均时间复杂度
最好时间复杂度、最坏时间复杂度、平均时间复杂度都是O(n^2)。
2)空间复杂度:每次交换仅需1个临时变量,故空间复杂度为O(1),是原地排序算法。
3)算法稳定性:如果两个值相等,就可能会交换位置,故是不稳定的排序算法。
7. 思考
- 冒泡排序和插入排序的时间复杂度相同,都是O(n^2),在实际的软件开发中,为什么我们更倾向于使用插入排序而不是冒泡排序算法呢?
答: - 相同点:插入排序和冒泡排序的平均时间复杂度都是O(n*n),都是稳定的排序算法,都是原地排序,元素交换的次数都是逆序度
- 插入比冒泡的优势:从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个,所以在对相同数组进行排序时,冒冒泡排序时间复杂度是选择排序的3倍,而且插入排序还有很大的优化空间,所以插入排序性能更好。
// 冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
// 插入排序中数据的移动操作:
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
- 如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?
答:觉得应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。
8. 自我复习提问
- 分析排序算法的三个维度都是什么?
- 从算法执行效率这个维度出发可以从哪三个方面进行衡量?
- 原地排序的概念是什么?
- 什么是排序的稳定性, 稳定性排序算法和不稳定排序算法的区别在哪里?
- 数组的满序度, 有序度, 逆序度概念各是什么? 如何计算?
- 冒泡排序的实现思路是怎样的, 请实现冒泡排序算法?
- 冒泡排序的为什么是原地排序算法, 为什么是稳定排序算法, 最好最坏,平均时间复杂度各是多少?
- 插入排序的实现思路是怎样的, 请实现插入排序算法?
- 插入排序的为什么是原地排序算法, 为什么是原地排序算法, 最好最坏,平均时间复杂度各是多少?
- 选择排序的实现思路是怎样的, 请实现选择排序算法?
- 选择排序的为什么是原地排序算法, 为什么不是稳定排序算法, 最好最坏,平均时间复杂度各是多少?
- 插入排序比冒泡排序的优势在哪里
9. 参考资料
- 王争老师在极客时间的专栏《数据结构与算法之美》
- 专栏下的所有评论
10. 声明
本文章是学习王争老师在极客时间专栏——《数据结构与算法之美》的学习总结,文章很多内容直接引用了专栏下的回复,推荐大家购买王争老师的专栏进行更加详细的学习
。本文仅供学习使用,勿作他用,如侵犯权益,请联系我,立即删除。
转载:https://blog.csdn.net/qq_27283619/article/details/102220351