冒泡排序、插入排序、选择排序

这篇博客是对极客时间上王争课程 —— 数据结构与算法之美的个人学习总结。文章中的图出自课程中。我会对课程中的 Java 代码用 Python 来实现,所有的代码会放在我的 GitHub上。


排序

经典排序:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。

奇葩排序(名字没听过,所以感觉有点奇葩):猴子排序、睡眠排序、面条排序。

排序算法优越评价有三个指标,执行效率、内存消耗、稳定性,一般来讲,在分析效率时会从几个方面来衡量:

  1. 时间复杂度。会从最好、最坏和平均情况三个来分析;
  2. 时间复杂度的系数、常数 、低阶。在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
  3. 比较次数和交换(或移动)次数。

内存消耗:原地排序(Sorted in place)。 原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。

稳定性: 这个是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

1. 冒泡排序(Bubble Sort)

冒泡排序是最基本最简单的排序了,在大家刚开始学习 C 语言的时候就会接触到。基本的思想就是,对于一个数比较与之相邻的数字,例如要把一个数列按从小到大的顺序排列,就拿左边第一个数,和第二的比,若小于第二个数两个交换,否则不换,再比较第二个和第三个,按照同样的规则,继续第三第四…直到最后。这样就算一次冒泡,每次冒泡都会有一个数被放到了最终的位置。就如下图中这样,图中的下边的数就是我所说的左边的数。

在这里插入图片描述

所以一个 n 长度的数列,至多就只要进行 n 次的冒泡就可以了。实际上,当其中一次冒泡操作没有数据交换时,就可以结束排序了。

# Copyright (c) strongnine

def bubbleSort(a):
    ## 读取输入数组的大小
    n = len(a)

    if n <= 1:
        return

    for i in range(n):
        ## 提前退出冒泡循环的标志位
        flag = False

        for j in range(n - i -1):
            ## 交换
            if (a[j] > a[j + 1]):
                a[j], a[j + 1] = a[j + 1], a[j]
                ## 为 True 表示这次冒泡有数据交换
                flag = True

        ## 没有数据交换,提前退出
        if not flag:
            break

if __name__ == '__main__':
    ## 对上面说的两个数据进行测试
    a = [4, 5, 6, 3, 2, 1]
    b = [3, 5, 4, 1, 2, 6]
    bubbleSort(a)
    bubbleSort(b)
    print('a = ', a , '\nb = ', b)

冒泡排序是原地排序的稳定的算法,时间复杂度为 O(n2)

2. 插入排序(Insertion Sort)

插入排序把数组分为已排序区和未排序区。取未排序区的元素,在已排序区上找到一个正确的位置插上去。还是希望对一个数据进行从小到大的排序。我们从未排序区上拿一个元素,按从右到左与已排序区的元素对比,如果如果当前元素 A 小于已排序区中的元素 B,让 B 往后移,即让 B 后面的位置等于 B,继续比 B 前面的数,也叫它为 B,它是新的一个 B,重复操作直到 A 大于 B,就让 A 插进当前 B 的前面。

在这里插入图片描述

# Copyright (c) strongnine

def insertionSort(a):
    n = len(a)

    if n <= 1:
        return

    for i in range(1, n):
        value = a[i]
        j = i - 1
        ## 查找插入的位置
        for j in range(j, -2, -1):
            if a[j] > value:
                ## 数据移动
                a[j + 1] = a[j]
            else:
                break
        ## 插入数据
        a[j + 1] = value


if __name__ == '__main__':
    a = [4, 5, 6, 1, 3, 2]
    insertionSort(a)
    print('a = ', a)

插入排序是原地排序的稳定的算法,时间复杂度为 O(n2)

3. 选择排序(Selection Sort)

选择排序也会把数组分为已排序区和未排序区。但是与插入排序不同的是,它每次找到未排序区的最小值,与未排序区的首个元素交换,这样就变成了已排序区的末尾元素了。

在这里插入图片描述

# Copyright (c) strongnine

def selectionSort(a):
    n = len(a)

    if n <= 1:
        return

    for i in range(n):
        ## 用于比较
        index =  i
        ## 得到最小值下标
        for j in range(i + 1, n):
            if a[index] > a[j]:
                index = j
        ## 交换
        a[i], a[index] = a[index], a[i]


if __name__ == '__main__':
    a = [4, 5, 6, 3, 2, 1]
    selectionSort(a)
    print('a = ', a)

需要注意的是,由于选择排序可能会把一个元素交换到与其数值相同的元素的后面,所以它不是一个稳定的算法。例如对于 [3, 4, 5, 6, 3, 1],在第一次交换中,第一个 3 就会和 1 交换,它会变到最后面,这样两个 3 的顺序就变化了。

冒泡排序与插入排序

为何在实际中倾向于使用插入排序而不是冒泡排序,尽管它们的时间复杂度都是O(n2),而且也都是稳定的。看一下两个算法在交换元素数值的处理上就知道了。

对于冒泡排序,交换两个元素时需要引入中间变量,也就是如果需要交换 A 和 B,我们需要让 A 赋值给 C,然后让 A 等于 B,再让 B 等于 C。而插入排序在每次比较时会把大的元素往后移,要插入的时候直接插入,所以更加的直接,在实际应用时更常用。

在 Python 上测试一下也可以知道,冒泡排序比插入排序的时间花费更多。我分别使用两种算法对一个 numpy 生成的长度为 2000 随机数组进行排序并计算时间,发现冒泡排序花费了 1 秒钟的时间,而插入排序只需使用 0.5 秒。

a = np.random.randint(10000, size=2000)

tick = time.time()
bubbleSort(a)
tock = time.time()
print('bubbleSort takes', tock - tick, 's.')

tick = time.time()
selectionSort(a)
tock = time.time()
print('selectionSort takes', tock - tick, 's.')

>>> bubbleSort takes 1.032163381576538 s.
>>> selectionSort takes 0.5756876468658447 s.
  • 8
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值