引言

排序算法在计算机科学的理论和应用中具有重要价值。本人使用 Python 语言,对常用的六种排序算法,包括冒泡排序、插入排序、选择排序、归并排序、快速排序以及 Timsort 进行了速度对比,其中 Timsort 用 Python 解释器内置的 sorted() 函数予以实现,其余算法采用本人编写的代码。实验平台为本人古董级的联想 Y470 笔记本电脑 (Intel Core i3-2310M CPU @ 2.10 GHz),操作系统为 Windows 7 64 位,Python 版本为 3.7.3,采用 Jupyter Notebook 实现代码。实验采用两种方法,其一是对 0 到 9999 这 10000 个整数随机打乱后计算每种算法对同一打乱序列排序所用的时间,其二是随机生成 10000 个 [0.0, 1.0) 均匀分布的浮点数,计算每种算法对同一生成序列排序所用的时间。两种方法的结果相一致,算法用时按照从大到小的顺序依次为:冒泡排序 > 插入排序 > 选择排序 > 归并排序 > 快速排序 > Timsort,其中最长用时与最短用时大约相差 4 个数量级。

算法

冒泡排序

这是一种简单低效的排序算法,实现步骤和 Python 代码如下。

第 1 轮:依次比较第 1 和第 2 个,第 2 和第 3 个,...,第 n-1 和第 n 个元素,大的向后移,第 1 大的移到倒数第 1 个位置。
第 2 轮:依次比较第 1 和第 2 个,第 2 和第 3 个,...,第 n-2 和第 n-1 个元素,大的向后移,第 2 大的移到倒数第 2 个位置。
▪ ▪ ▪ ▪ ▪ ▪
第 n-1 轮:比较第 1 和第 2 个,大的向后移,第 n-1 大的移到倒数第 n-1 个位置,所有元素排序完成。

def bubble_sort(nums):n = len(nums)for i in range(n-1):for j in range(n-1-i):if nums[j] > nums[j+1]:nums[j], nums[j+1] = nums[j+1], nums[j]return nums

冒泡排序低效的原因之一是它对于含有

个元素的序列,总要进行
轮共计
次比较,即使该序列已经完全有序。于是一种改进方法是跟踪每一轮的比较结果,如果在某一轮完成所有两两比较后,发现没有发生任何交换,就说明此时已经完成了排序任务,我们提前收工。下面是这种优化的冒泡算法以及实现代码。

第 1 轮:依次比较第 1 和第 2 个,第 2 和第 3 个,...,第 n-1 和第 n 个元素,大的向后移,第 1 大的移到倒数第 1 个位置。
第 2 轮:依次比较第 1 和第 2 个,第 2 和第 3 个,...,第 n-2 和第 n-1 个元素,大的向后移,第 2 大的移到倒数第 2 个位置。
▪ ▪ ▪ ▪ ▪ ▪
直到某轮比较过程中,未发生任何交换,表明排序已完成。

def optimized_bubble_sort(nums):n = len(nums)for i in range(n-1):swap = Falsefor j in range(n-1-i):if nums[j] > nums[j+1]:nums[j], nums[j+1] = nums[j+1], nums[j]# 注意此处的实现细节,每轮只有第一次交换才会赋值 swap = Trueif not swap:swap = Trueif not swap:breakreturn nums

注意代码中的一处细节,就是每轮只有第一次交换才会赋值 swap = True,否则如果每次交换都赋值,会明显增加运行时间,对随机序列排序的效率甚至不如未优化的冒泡算法。

插入排序

同冒泡排序一样,插入排序也要一轮又一轮地比较相邻两元素的大小,然而它能充分利用之前已经完成排序的子列的性质,在一轮比较过程中一旦找到合适的位置就立即中断并进入下一轮,这也是其比冒泡排序更加高效的原因。下面是其实现步骤及代码。

第 1 轮:将第 2 个元素插到第 1 个元素的左边或右边,前 2 个元素排序完成。
第 2 轮:将第 3 个元素插到前 2 个元素的合适位置上,前 3 个元素排序完成。
▪ ▪ ▪ ▪ ▪ ▪
第 n-1 轮:将第 n 个元素插到前 n-1 个元素的合适位置上 (依次将其与第 n-1 个,第 n-2 个,...,第 1 个元素相比,若小于则交换位置,大于等于则跳出循环),所有元素排序完成。

def insersion_sort(nums):n = len(nums)for i in range(n-1):for j in range(i+1, 0, -1):if nums[j] < nums[j-1]:nums[j], nums[j-1] = nums[j-1], nums[j]else:breakreturn nums

选择排序

选择排序同以上两种算法一样,对含有

个元素的序列要进行
轮遍历,但它跳出了只是比较相邻两元素的思维模式,在一轮遍历过程中通过一个指标

min_index 记录未排序子列中最小元素的位置,再将最小元素放到已排序子列的末尾,从而在一轮遍历中最多只发生一次交换,提高了算法的效率。其算法步骤及代码如下。

第 1 轮:从第 1 个元素开始,找到最小元素的指标,将该元素与第 1 个元素交换。
第 2 轮:从第 2 个元素开始,找到最小元素的指标,将该元素与第 2 个元素交换。
▪ ▪ ▪ ▪ ▪ ▪
第 n-1 轮:从第 n-1 个元素开始,找到最小元素的指标,将该元素与第 n-1 个元素交换。

def selection_sort(nums):n = len(nums)for i in range(n-1):min_index = ifor j in range(i+1, n):if nums[j] < nums[min_index]:min_index = jif min_index != i:nums[i], nums[min_index] = nums[min_index], nums[i]return nums

归并排序

归并排序于 1945 年由 John von Neumann 首次提出。不同于前面介绍的几种算法,归并排序以及下面要介绍的的快速排序,突破了完全依靠交换去移动元素的思路,而是采用“分而治之”的策略,使得在处理较大规模数据时,能够有效提高算法的效率。归并排序的整体思路,是对于一个较大的序列,不断将其细分为较小序列,并递归地调用归并排序对每一小段分别排序,具体步骤和代码如下。

平均分割:把当前序列平均分割为两个子序列。
递归排序:对以上两个子序列,分别递归地用归并排序法排序 (在递归调用过程中,若序列为空或只有一个元素,表明已经完成排序)。
保序集成:将上一步得到的两个有序子列,保序集成到一起,完成整个序列的排序。

def merge(left, right):          # 保序集成算法result = []while left and right:if left[0] < right[0]:result.append(left.pop(0))else:result.append(right.pop(0))if left:result += leftif right:result += rightreturn resultdef merge_sort(nums):n = len(nums)if n <= 1:                   # 若序列为空或只有一个元素,表明已经完成排序return numsm = n//2                     # 平均分割left = nums[:m]right = nums[m:]left = merge_sort(left)      # 递归排序right = merge_sort(right)return merge(left, right)    # 保序集成

快速排序

快速排序于 1959 年由 Tony Hoare 首次提出,其同样采用“分而治之”的策略,并且相较于归并排序,思路更加简单,也更容易实现,运用得当的话对大规模随机序列排序的速度比归并排序快 2 到 3 倍。快速排序的总体思路也是把一个较大的序列,不断细分为较小序列,并递归地调用快速排序对每一小段分别排序。由于其采用基准来划分序列,因而可以省略归并排序中对子列的集成这一步骤,不过相应的代价就是基准的选取方法会对算法性能产生较大影响。

最开始,我采用如下步骤和代码实现这一算法。

挑选基准:选取序列的最后一个元素作为基准,其值为基准值。
基准分割:对序列重新排序,所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准后面 (与基准值相等的元素可到任一边),在这个分割结束之后,完成对基准的排序。
递归排序:递归地将基准前面的子序列和基准后面的子序列用此快速排序法排序 (在递归调用过程中,若序列为空或只有一个元素,表明已经完成排序)。

def poor_quick_sort(nums):n = len(nums)if n <= 1:                # 若序列为空或只有一个元素,表明已经完成排序return numsless = []more = []pivot = nums.pop()        # 挑选基准for i in nums:            # 基准分割if i < pivot:less.append(i)else:more.append(i)nums.append(pivot)        # 这一句是为了让传入的 nums 保持不变return poor_quick_sort(less) + [pivot] + poor_quick_sort(more) # 递归排序

但是,当使用完全逆序整数序列 list(range(9999, -1, -1)) 对其进行测试时,由于每次所选基准两边元素的数量差都达到最大,也就是遇到了所谓的“最坏状况”,结果程序抛出了 RecursionError,即超过递归深度限制的异常,如下所示。

---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-2-b97014017ca2> in <module>
----> 1 poor_quick_sort(list(range(9999, -1, -1)))<ipython-input-1-f8072ae399a3> in poor_quick_sort(nums)12             more.append(i)13     nums.append(pivot)        # 这一句是为了让传入的 nums 保持不变
---> 14     return poor_quick_sort(less) + [pivot] + poor_quick_sort(more) # 递归排序... last 1 frames repeated, from the frame below ...<ipython-input-1-f8072ae399a3> in poor_quick_sort(nums)12             more.append(i)13     nums.append(pivot)        # 这一句是为了让传入的 nums 保持不变
---> 14     return poor_quick_sort(less) + [pivot] + poor_quick_sort(more) # 递归排序RecursionError: maximum recursion depth exceeded while calling a Python object

解决这个问题的一种办法就是在第一步随机挑选基准,步骤和代码改写如下。

挑选基准:从序列中随机选择一个元素作为基准,其值为基准值。
基准分割:对序列重新排序,所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准后面 (与基准值相等的元素可到任一边),在这个分割结束之后,完成对基准的排序。
递归排序:递归地将基准前面的子序列和基准后面的子序列用此快速排序法排序 (在递归调用过程中,若序列为空或只有一个元素,表明已经完成排序)。

import random
def quick_sort(nums):n = len(nums)if n <= 1:                         # 若序列为空或只有一个元素,表明已经完成排序return numsless = []more = []pivot_index = random.randrange(n)  # 挑选基准pivot = nums.pop(pivot_index)for i in nums:                     # 基准分割if i < pivot:less.append(i)else:more.append(i)nums.insert(pivot_index, pivot)    # 这一句是为了让传入的 nums 保持不变return quick_sort(less) + [pivot] + quick_sort(more)    # 递归排序

Timsort

Timsort 于 2002 年由 Tim Peters (也是 Zen of Python 的作者) 在开发 Python 语言时提出,成为 Python 2.3.0 及之后版本的标准排序算法。Timsort 是一种结合了插入排序和归并排序的混合算法,Wikipedia 词条对其做了简要介绍,如果想要知道这个算法的更多细节及其在 Python 官方解释器 CPython 中的具体实现,可以参考作者 Tim Peters 对这个算法的描述,以及 GitHub 上面的源代码。另外,在 Google 和知乎上面搜索也能得到关于此算法的更多信息。限于篇幅,这里不再对其进行详细讨论。

实验

导入所需模块

实验首先要导入所需模块,这里仅使用两个包含在 Python 标准库中的模块,即生成伪随机数和进行随机选择的 random 模块以及提供时间访问和转换的 time 模块。

import random
import time

定义所需变量和函数

然后定义两个变量,一个是排序元素的数目,太少则无法区分算法优劣,太多又会产生不必要的时间成本,考虑到实验平台的性能,取 10000 个数字。另一个是把前面定义的若干排序函数放进一个 Python 字典 当中,函数名作为“键”,对应的“值”为字符串表示的算法名称,这样既可以统一处理不同排序函数,又方便以后添加新的排序算法。接着再定义一个 is_sorted(nums) 函数,用以判断传入的 nums 序列是否完成排序。

number = 10000
methods = {bubble_sort: "Bubble", optimized_bubble_sort: "Optimized Bubble", insersion_sort: "Insersion", selection_sort: "Selection", merge_sort: "Merge", quick_sort: "Quick", sorted: "Tim"}def is_sorted(nums):n = len(nums)for i in range(n-1):if nums[i] > nums[i+1]:return Falsereturn True

随机打乱整数的排序

mylist = list(range(number))
random.shuffle(mylist)for func in methods:temp = mylist[:]tic = time.time()result = func(temp)toc = time.time()assert is_sorted(result)print(methods[func] + " Sort Time for Integer: %.2f ms" %(1000. * (toc - tic)))

以上代码测试了不同排序算法对随机打乱整数的排序用时。首先用 mylist 存储 0 到 9999 整数序列的乱序排列,然后对不同算法循环执行以下操作。

  • 复制 mylist 到临时变量 temp (为什么用 mylist[:] 而不是 mylist,老司机都懂的)
  • 记录排序之前的时间戳,单位为 s
  • temp 排序,并将结果存储在 result
  • 记录排序之后的时间戳,单位为 s
  • 验证排序结果的正确性
  • 显示该算法的排序用时,单位为 ms

实验时关闭其它无关进程,发现每次结果略有不同,但相差不大。作为粗略实验,不必采用多次实验取平均值的方法,只是展示一下某次运行的具体结果。

Bubble Sort Time for Integer: 20322.16 ms
Optimized Bubble Sort Time for Integer: 20208.16 ms
Insersion Sort Time for Integer: 15230.87 ms
Selection Sort Time for Integer: 7843.45 ms
Merge Sort Time for Integer: 108.01 ms
Quick Sort Time for Integer: 37.00 ms
Tim Sort Time for Integer: 2.00 ms

随机生成浮点数的排序

mylist = [random.random() for i in range(number)]for func in methods:temp = mylist[:]tic = time.time()result = func(temp)toc = time.time()assert is_sorted(result)print(methods[func] + " Sort Time for Float: %.2f ms" %(1000. * (toc - tic)))

此时的 mylist 存储了 10000 个随机生成的 [0.0, 1.0) 均匀分布浮点数作为排序对象,其余和上面的整数排序完全相同,不再赘述。某次运行的结果如下。

Bubble Sort Time for Float: 19414.11 ms
Optimized Bubble Sort Time for Float: 19331.11 ms
Insersion Sort Time for Float: 14290.82 ms
Selection Sort Time for Float: 7933.45 ms
Merge Sort Time for Float: 109.01 ms
Quick Sort Time for Float: 37.00 ms
Tim Sort Time for Float: 2.00 ms

讨论

首先对于同一种排序算法,处理整数和浮点数的用时较为接近,这比较容易理解。因为排序算法的基本步骤就是比较两个元素的大小,至于这两个元素是整数,浮点数,还是其他什么有序集合,对于计算机而言并没有什么区别,因为数据在计算机内部都是用二进制数字表示的。而不同排序算法的效率之所以有那么大的差异,是因为它们在设计比较方式上有明显的不同。下面我们从时间复杂度和空间复杂度的角度简单讨论一下这些排序算法的性能。

时间复杂度

从结果上看,冒泡排序、插入排序和选择排序的用时在 10000 ms 量级,归并排序和快速排序的用时在 100 ms 量级,而最优异的 Timsort 用时仅为 ms 量级,最长与最短用时足足差了 4 个量级,这表明,至少对于随机数排序而言,不同算法之间有着巨大的性能差异。

简单地说,对于长度为

的序列,冒泡排序、插入排序和选择排序这些较挫算法的平均时间复杂度为
,而归并排序、快速排序和 Timsort 的平均时间复杂度为
,随着序列长度的增加,它们之间的差异也越来越明显。另外,作为一种混合算法,Timsort 对不同长度的序列采用不同策略,并且能够充分利用序列中已有的有序片段,种种优化使其表现更加出色。

排序算法的时间复杂度,在数学上有严格的定义,许多算法书籍也有详细的讨论,比如著名的《算法导论》。另外,这篇知乎高赞回答提供了一些非常直观的视角,值得一读。

空间复杂度

仔细阅读文章的朋友应该还记得,实验中采用同一序列测试不同算法,也就是在生成一个随机序列 mylist 后,测试每种算法前都用 temp = mylist[:] 将其复制到一个临时变量,并对该临时变量进行排序,这样保证最初生成的 mylist 不参与排序,也就不会改变,使所有算法站在同一起跑线上。

其实,对于后三种算法归并排序、快速排序和 Timsort 而言,完全可以用 temp = mylist,而不影响实验结果,因为这三种算法的空间复杂度为

,它们把最终的排序结果存到新的变量并将其返回,而最初传入算法函数的变量则保持原样。但前三种算法冒泡排序、插入排序和选择排序不可以这样做,原因是这三种算法的空间复杂度为
,或者说是原地排序,即直接在传入序列上作修改,使其变为有序,而不需额外的内存空间。因此如果对前三种算法用

temp = mylist,那么第一次循环后 mylist 就成为有序序列,后面的算法其实是对这个有序序列进行排序,显然不公平。另外,前三种算法函数也不需要最后的 return 语句,不过为了实验代码的一致性,加上了这一句话。

进一步的实验

本实验只是一个用于演示的简单实验,所用序列完全随机且不含有重复元素。进一步的实验可尝试近似随机,近似有序,含有重复元素,重复元素分布集中或分散等多种类型的序列,也可以增加其他排序算法。

附录

程序源代码已上传到本人的 GitHub 上,欢迎围观并批评指正。感谢阅读!


版权所有,转载请注明出处

c++ sort 从大到小排序_常用排序算法速度比较相关推荐

  1. golang 排序_常用排序算法之冒泡排序

    周末无事,带娃之余看到娃娃在算数,想到了排序-尝试着把几种常用的排序算法跟大家聊一聊,在分析的后面我会用GoLang.PHP和JS三种语言来实现下. 常见的基于选择的排序算法有冒泡排序.插入排序.选择 ...

  2. 求出千位数上的数加百位数上的数等于十位数上的数加个位数上的数的个数cnt,再把所有满足条件的四位数依次存入数组b中,然后对数组b中的四位数按从大到小的顺序进行排序。

    已知数据文件IN13.DAT中存有300个四位数,并已调用读函数readDat()把这些数存入数组a中,请编制一个函数jsValue(),其功能是:求出千位数上的数加百位数上的数等于十位数上的数加个位 ...

  3. c++ sort 从大到小排序_算法的艺术:MySQL order by对各种排序算法的巧用

    在 [精华]洞悉MySQL底层架构:游走在缓冲与磁盘之间 这篇文章中,我们介绍了索引树的页面怎么加载到内存中,如何淘汰,等底层细节.这篇文章我们从比较宏观的角度来看MySQL中关键字的原理.本文,我们 ...

  4. c++ sort 从大到小排序_C语言必学的12个排序算法:冒泡排序(第4篇)

    基本思想 冒泡排序(Bubble Sort),是一类"交换"类排序方法,类似水中冒泡,最大的数据会沉到水底,较小的数会浮上来.很简单,以从小到大排序为例,每一趟排序将"逆 ...

  5. arraylist从大到小排序_经典排序方法的python实现和复杂度分析

    1.冒泡排序: 冒泡排序算法的运作如下: 比较相邻的元素.如果第一个比第二个大(升序),就交换他们两个. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对.这步做完后,最后的元素会是最大的数 ...

  6. c++ sort 从大到小排序_C语言必学的12个排序算法:堆排序(第7篇)

    题外话堆排序比之前的简单选择.冒泡算法.快速排序算法复杂一些,因为用到了树形数据结构,但是本文使用了数组实现完全二叉树,因此也比较简单.C语言初学者,可以简单了解其思想,具体的知识掌握可以参照数据结构 ...

  7. python选择排序从大到小_Python实现选择排序

    一.选择排序简介 选择排序(Selection sort)是一种简单直观的排序算法.选择排序首先从待排序列表中找到最小(大)的元素,存放到元素列表的起始位置(与起始位置进行交换),作为已排序序列,第一 ...

  8. python中用def实现自动排序_漫画排序算法Python实现

    冒泡排序 冒泡排序的思想,我们要把相邻的元素两两比较,当一个元素大于右侧相邻元素时, 交换它们的位置;当一个元素小于或等于右侧相邻元素时,位置不变. def bubbleSort(list): ran ...

  9. 折半查找的思想及源码_常用排序与查找算法

    1 选择排序 选择排序(Selection sort)是一种简单直观的排序算法.它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中 ...

  10. python链表排序_链表排序+末尾各种排序

    #工具人排序 def nums_sort(data): if not data: return [] min_data = min(data) max_data = max(data) nums =  ...

最新文章

  1. php 网络图片 转本地,PHP将Base64图片转换为本地图片并保存
  2. vue.js 使用axios实现下载功能
  3. eclipse 报错问题:java.lang.ClassNotFoundException:
  4. 用.net中的SqlBulkCopy类批量复制数据 (转载)
  5. yum和apt-get 软件包管理器的用法及区别
  6. 【工具相关】iOS-Reveal的使用
  7. matlab 取矩阵上三角元素,MATLAB triu():提取上三角矩阵
  8. Power BI数据网关
  9. 好用的PDF编辑软件有哪些?这几款工具建议收藏
  10. 使用BootStrap制作网页页面
  11. setheader是什么意思_HTTP 请求头 响应头信息含义
  12. kubectl rollout restart重启pod
  13. 基于Lumerical FDTD的等离子体光子晶体分析
  14. 哈希函数(散列函数)详解
  15. 2017物联网蓬勃发展,看各领域巨头如何抢先机占山头
  16. IP地址为 140.111.0.0 的B类网络,若要切割为9个子网,而且都要 连上Internet,请问子网掩码设为
  17. 泉州计算机编程培训班,泉州编程小学生培训班
  18. 什么是TCP协议的三次握手四次挥手
  19. 数字逻辑综合工具实践-DC-08——静态时序分析(STA)
  20. 宝塔Linux面板的搭建

热门文章

  1. Silverlight 3.0正式版RTW的发布日期
  2. 实现RedHat6.3全屏,解决最大分辨率只有800*600
  3. 计算机学院姚茜,武汉理工大学第三届届学位评定委员会
  4. jmeter如何看tps_jmeter性能测试疑难杂症解决思路
  5. kill mysql 进程_如何快速处理mysql连接数占满的问题?
  6. 页面常见的布局方式(图解)
  7. ASP.NET读取自定义的config文件
  8. 微服务容错限流Hystrix入门
  9. LeetCode 981.基于时间的键值存储(C++)
  10. JavaFX及Java客户端技术的未来