算法原理

快速排序是一个具有较高性能的排序算法,其主要思想如下:

对数组中的某个元素,确定其在数组中的排序位置,即在其之前的所有元素均小于该元素,在其之后的均大于该元素。对小元素组和大元素组同样执行该过程,直到全部执行完毕。

每次递归确定一个元素的排序位置,核心在于确定大元素组和小元素组。设待处理元素为left,本次循环元素i,大元素组的第一个元素为j:

数组元素可看作该分组 :

left

less group

(j)—>greater group

i

other

right

初始时刻大小元素组均为空,随着i不断后移,逐渐填充两个元素组。按照如下思路进行处理:

如果array[i] >= array[left], 将i归入greater_group;如果array[i] < array[left],将i和j交换。

分组完成后交换array[left]和小元素组最后元素,即可确定array[left]元素在排序数组中的位置。

算法实现

快速排序使用分治思想对划分的大元素组和小元素组进行分别处理:

def quick_sort(unsorted_array, left = -1, right = -1):

if left == -1 and right == -1:

left = 0

right = len(unsorted_array) - 1

if left >= right:

return

p = quick_sort_partition(unsorted_array, left, right)

quick_sort(unsorted_array, left, p - 1)

quick_sort(unsorted_array, p + 1, right)

注意递归返回的条件left >= right,当两者相等时,待排序数组只有一个元素,因此无需排序;当left > right时,表示待排序数组为空,等价于使用:

if left != p:

quick_sort(unsorted_array, left, p - 1)

if right != p:

quick_sort(unsorted_array, p + 1, right)

快速排序的辅助函数实现如下:

def quick_sort_partition(unsorted_array, left, right):

random_p = random.randint(left, right)

unsorted_array[left], unsorted_array[random_p] = unsorted_array[random_p], unsorted_array[left]

v = unsorted_array[left]

last_of_less_group = left

for i in range(left, right + 1):

if(v > unsorted_array[i]):

last_of_less_group += 1

unsorted_array[last_of_less_group], unsorted_array[i] = unsorted_array[i], unsorted_array[last_of_less_group]

unsorted_array[left], unsorted_array[last_of_less_group] = unsorted_array[last_of_less_group], unsorted_array[left]

return last_of_less_group

其中包含了一个典型优化,随机选择待处理元素。这是由于对于近乎有序数组,快速排序划分的两个子数组一长一短,这种不平衡的划分会增加时间复杂度,最坏情况会达到$ O(N^2) $。

之后按照前述方式确定元素在排序数组中的位置。除此之外,还可以使用挖坑填数的方式从两端向中间寻找位置,详见下文两路快排。

算法优化

快排有诸多优化方式:

随机选择待处理元素。

在元素较少的时候,使用插入排序作为代替,较少递归开销。

双路快排,通过打乱相等元素,解决重复元素造成划分不平衡的问题。

三路快排,通过明确划分出相等元素,进一步解决上述问题。

双路快排

对于前文介绍的单路快排,如果数组中存在大量重复元素,在某轮递归处理中,将会出现大量array[left]==array[i]的情况,这些相等的数值会被放到大元素组,从而造成划分的不平衡。为了解决这个问题,采用如下思路:

从数组左端开始构造小元素组,从右端构造大元素组,直到两边都遇到不属于该元素组的元素,将这两个元素交换。其中等于array[left](待确定排序位置的元素)的情况也进行交换。

通过这种方式,等于array[left]的元素被尽可能地分散到两个分组中。

例如,对于数组[1, 2, 2, 3, 1, 1, 1, 1, 0, 0, 0, 1, 1, 2, 2]。

双路交换后变为[1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 3, 2, 2, 2, 2],返回的划分点位置为6(0为起始)。

如果为单路交换,结果为[0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 3, 1, 1, 2, 2], 返回的划分点位置为3。

如果将下述双路快排中unsorted_array[i] < v和unsorted_array[j] > v改为unsorted_array[i] <= v和unsorted_array[j] >= v,此时对于相等的情况不交换。结果为[0, 0, 0, 1, 1, 1, 1, 1, 3, 2, 2, 1, 1, 2, 2], 返回的划分点位置为3。

显然,后两种情况都产生了不平衡的划分。

def quick_sort_partition_two_way(unsorted_array, left, right):

random_p = random.randint(left, right)

unsorted_array[left], unsorted_array[random_p] = unsorted_array[random_p], unsorted_array[left]

v = unsorted_array[left]

i = left + 1

j = right

while True:

while i <= right and unsorted_array[i] < v:

i += 1

while j >= left + 1 and unsorted_array[j] > v:

j -= 1

if i >= j:

break

unsorted_array[i], unsorted_array[j] = unsorted_array[j], unsorted_array[i]

i += 1

j -= 1

unsorted_array[left], unsorted_array[j] = unsorted_array[j], unsorted_array[left]

return j

注意,while i <= right and unsorted_array[i] < v:这一行不可以先判断元素大小再判断数组越界,这样会出现数组访问越界错误,当前写法利用了“与”运算如果前面为False则不再考虑后面的特性。

三路快排

上述双路快排只是尽可能的解决了重复元素的问题,三路快排通过显式的将数组划分为三组,有效解决了重复元素的问题。划分的三组为大于array[left],小于array[left],等于array[left]。其主要思路与前述两个快排类似:

每轮递归中,对于当前元素i,如果小于目标,放到左边的less_group,如果大于目标,放到右边的greater_group,如果等于目标,放到中间。之后对两边的大小分组继续递归,直到排序完成。

使用python的partition函数实现如下:

def quick_sort_partition_three_way(unsorted_array, left, right):

random_p = random.randint(left, right)

unsorted_array[left], unsorted_array[random_p] = unsorted_array[random_p], unsorted_array[left]

last_of_less_group = left

first_of_greater_group = right + 1

v = unsorted_array[left]

i = left + 1

while i < first_of_greater_group:

if unsorted_array[i] < v:

unsorted_array[last_of_less_group + 1], unsorted_array[i] = unsorted_array[i], unsorted_array[last_of_less_group + 1]

i += 1

last_of_less_group += 1

elif unsorted_array[i] > v:

unsorted_array[i], unsorted_array[first_of_greater_group - 1] = unsorted_array[first_of_greater_group - 1], unsorted_array[i]

first_of_greater_group -= 1

else:

i += 1

unsorted_array[left], unsorted_array[last_of_less_group] = unsorted_array[last_of_less_group], unsorted_array[left]

return last_of_less_group, first_of_greater_group

上述实现较为简单,其中需要注意的地方在于unsorted_array[i] > v的情况时,不需要进行i += 1,这是由于换过来的元素大小不定,需要在当前位置继续比较。而对于unsorted_array[i] < v的情况,交换过来的array[last_of_less_group + 1]一定等于v,因此可以前往下一个元素。

算法测试

本文涉及代码均通过正确性测试。以下是性能测试结果:

创建测试数组

对于快速排序:

100000大小数组耗时:0.484760s

1000000大小数组耗时:7.085851s

100000大小随机范围较小数组耗时: **stack overflow**

100000大小近乎有序数组耗时:0.386866s

.

对于双路快速排序:

100000大小数组耗时:0.327692s

1000000大小数组耗时:4.167595s

100000大小随机范围较小数组耗时:0.328271s

100000大小近乎有序数组耗时:0.257662s

.

对于三路快速排序:

100000大小数组耗时:0.467874s

1000000大小数组耗时:6.011800s

100000大小随机范围较小数组耗时:0.032722s

100000大小近乎有序数组耗时:0.472250s

.

对于归并排序:

100000大小数组耗时:0.684923s

1000000大小数组耗时:7.055477s

100000大小随机范围较小数组耗时:0.613237s

100000大小近乎有序数组耗时:0.473848s

.

----------------------------------------------------------------------

Ran 4 tests in 37.741s

OK

首先值得注意的是,如果对10000大小随机范围[0-3]的数组进行单路快排,会出现栈溢出。此时重复元素过多,划分过于不平衡,递归次数过多。

整体而言,快速排序具有很好的性能,尤其双路快排拥有最快的性能,但三路快排对存在大量重复元素有着更好的性能,因此被许多编程语言作为默认排序方法。另外,Python的默认排序方法为TimeSort,其性能更为优秀。

算法复杂度

时间复杂度为$ O(Nlog(N)) $,空间复杂度为$ O(1) $。

有趣的实现

来源于网络的一则单行快排python实现,可以看作上文单路快排,且不含优化的版本。

quicksort = lambda l: quicksort([i for i in l[1:] if i < l[0]]) + [l[0]] + quicksort([j for j in l[1:] if j >= l[0]]) if l else []

python实现快速排序算法_基础算法:快速排序(python实现)相关推荐

  1. python二分法查找程序_基础算法——查找(二分法)(python)

    基础算法--查找(二分法)(python).二分查找法(BinarySearchST)应用十分广泛,是必须要掌握的查找方法.适用对象是有序数组.具有最优的查找效率和空间需求,能够进行有序相关的操作.但 ...

  2. 递归_三要素_基础算法必备

    递归_三要素_基础算法必备 目录 第一要素:明确函数作用 第二要素:递归结束条件 第三要素:函数等价关系 第一要素:明确函数作用 对于递归,我觉得很重要的一个事就是,这个函数的功能是什么,他要完成什么 ...

  3. cb32a_c++_STL_算法_查找算法_(5)adjacent_find

    cb32a_c++_STL_算法_查找算法_(5)adjacent_find adjacent_find(b,e),b,begin(),e,end() adjacent_find(b,e,p),p-p ...

  4. 常用十大算法_回溯算法

    回溯算法 回溯算法已经在前面详细的分析过了,详见猛击此处. 简单的讲: 回溯算法是一种局部暴力的枚举算法 循环中,若条件满足,进入递归,开启下一次流程,若条件不满足,就不进行递归,转而进行上一次流程. ...

  5. 视频教程-跟着王进老师学开发之Python篇第一季:基础入门篇-Python

    跟着王进老师学开发之Python篇第一季:基础入门篇 教学风格独特,以学员视角出发设计课程,难易适度,重点突出,架构清晰,将实战经验融合到教学中.讲授技术同时传递方法.得到广大学员的高度认可. 王进 ...

  6. python语言基本排序算法_排序算法(Python)

    参考: <数据结构(Python 语言描述)> - 3.4 基本排序算法.3.5 更快的排序 Tips:为了保持简洁,每个函数都只处理整数列表,并且假设列表不为空. 目录.jpg 术语 1 ...

  7. python编程的50种基础算法_Python算法新手入门大全

    干货:GitHub标星2.6万!Python算法新手入门大全 Python已经成为最受欢迎的程序设计语言之一.自从2004年以后,python的使用率呈线性增长.2011年1月,它被TIOBE编程语言 ...

  8. python写数据结构书_有哪些用 Python 语言讲算法和数据结构的书?

    python数据结构基础工具书籍下载-持续更新​www.jianshu.com 以上网址有大量python数据结构的书籍下载,内容比较长,我这边拷贝了一部分. 本书示例丰富,图文并茂,以让人容易理解的 ...

  9. python分治算法_分治法及其python实现例子

    在前面的排序算法学习中,归并排序和快速排序就是用的分治法,分治法作为三大算法之一的,有非常多的应用例子. 分治法概念 将一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题-- ...

最新文章

  1. python关联规则挖掘_Python3:文本关联规则挖掘实现案例分析
  2. HTTP状态码(HTTP Status Code),常见的error 404, error 504等的意思
  3. Spring之Bean的配置(一)
  4. hdu 3577(线段树区间更新)
  5. Highcharts 环境配置介绍
  6. C# WPF MVVM开发框架Caliburn.Micro IResult和协同程序⑥
  7. python读取oracle数据库性能_用python对oracle进行简单性能测试
  8. iOS执行时工具-cycript
  9. 12 哈希表相关类——Live555源码阅读(一)基本组件类
  10. 服务器修改地址,服务器修改管理地址
  11. Machine Learning学习计划
  12. mongodb的架构 副本集搭建
  13. 我的世界java占用太多内存_Java 进程占用内存过多,幕后元凶原来是线程太多
  14. 震撼上市!北朝鲜语对话语音识别数据库
  15. JVM启动参数手册——JVM之八
  16. Android 车载应用开发与分析(12) - SystemUI (一)
  17. 先有鸡还是先有鸡蛋?C语言发展史给出的答案
  18. ps快捷图标在哪个文件夹_在PS中制作一个下载文件夹的图标
  19. 海贼王---追了好久的动漫了闲来无事发几张图嘿嘿
  20. 如何把视频嵌入PPT合为一个文件

热门文章

  1. 6-1 数组元素的区间删除
  2. 每日程序C语言42-带头结点的尾插法创建链表
  3. 图像坐标球面投影_坐标系统及投影概述
  4. 《架构之美》阅读笔记一
  5. 在GitHub中创建目录
  6. 2018年第九届蓝桥杯 第五题:快速排序 (满分9分)
  7. 10th blog:Object
  8. 解题:POI 2013 Triumphal arch
  9. ZOJ 3879(大模拟)
  10. 从CMDB动态获取服务器列表,按照Ansible的约定