小言_互联网的博客

【08】排序(上):为什么插入排序比冒泡排序更受欢迎?

543人阅读  评论(0)

1. 排序方法与复杂度归类

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

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

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

  1. 什么是有序度?
    有序度是数组中具有有序关系的元素对的个数,比如[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,完全有序的情况称为满有序度。
  2. 什么是逆序度?
    逆序度的定义正好和有序度相反。核心公式:逆序度=满有序度-有序度。
    排序过程,就是有序度增加,逆序度减少的过程,最后达到满有序度,就说明排序完成了。
    冒泡排序包含两个操作原子,即比较和交换,每交换一次,有序度加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. 思考

  1. 冒泡排序和插入排序的时间复杂度相同,都是O(n^2),在实际的软件开发中,为什么我们更倾向于使用插入排序而不是冒泡排序算法呢?
    答:
  2. 相同点:插入排序和冒泡排序的平均时间复杂度都是O(n*n),都是稳定的排序算法,都是原地排序,元素交换的次数都是逆序度
  3. 插入比冒泡的优势:从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要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;
}
  1. 如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?
    答:觉得应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。

8. 自我复习提问

  1. 分析排序算法的三个维度都是什么?
  2. 从算法执行效率这个维度出发可以从哪三个方面进行衡量?
  3. 原地排序的概念是什么?
  4. 什么是排序的稳定性, 稳定性排序算法和不稳定排序算法的区别在哪里?
  5. 数组的满序度, 有序度, 逆序度概念各是什么? 如何计算?
  6. 冒泡排序的实现思路是怎样的, 请实现冒泡排序算法?
  7. 冒泡排序的为什么是原地排序算法, 为什么是稳定排序算法, 最好最坏,平均时间复杂度各是多少?
  8. 插入排序的实现思路是怎样的, 请实现插入排序算法?
  9. 插入排序的为什么是原地排序算法, 为什么是原地排序算法, 最好最坏,平均时间复杂度各是多少?
  10. 选择排序的实现思路是怎样的, 请实现选择排序算法?
  11. 选择排序的为什么是原地排序算法, 为什么不是稳定排序算法, 最好最坏,平均时间复杂度各是多少?
  12. 插入排序比冒泡排序的优势在哪里

9. 参考资料

  1. 王争老师在极客时间的专栏《数据结构与算法之美》
  2. 专栏下的所有评论

10. 声明

本文章是学习王争老师在极客时间专栏——《数据结构与算法之美》的学习总结,文章很多内容直接引用了专栏下的回复,推荐大家购买王争老师的专栏进行更加详细的学习。本文仅供学习使用,勿作他用,如侵犯权益,请联系我,立即删除。


转载:https://blog.csdn.net/qq_27283619/article/details/102220351
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场