本题要求实现一个用选择法对整数数组进行简单排序的函数。_通俗易懂讲 Python 算法:快速排序...

1c933af292771279aea23f99c98b37a6.png

原文:https://stackabuse.com/quicksort-in-python/

作者:Marcus Sanatan

译者:老齐

欢迎在 bilibili  搜索 freeCodeCamp 官方账号或者直接访问 https://space.bilibili.com/335505768 观看我们的技术视频

介绍

快速排序是一种流行的排序算法,经常与归并排序一起使用。快速排序是一个高效的排序算法,平均复杂度为 O(nlogn)。它受欢迎的部分原因还在于易于实施。

在本文中,我们将先使用整数集合演示快速排序算法,然后会用自定义对象深入探讨这种算法的实现方式。

在分治法、原地排序和非稳定排序中,都有快速排序算法。

- 在分治法中,对大数组使用递归进行排序之前,使用快速排序算法将数组拆分为更小的数组,直到最后得到一个空数组或只有一个元素的数组。

- 在原地排序中,快速排序不创建数组的任何副本,它在内存中完成所有的操作。

- 稳定排序指相同的值在数组中的顺序也是相同的,非稳定的排序算法不能保证这一点,只能说这样的顺序当然有可能出现,但不能保证。这在针对对象排序而不是对基本类型排序时变得很重要。例如,假设有几个自定义的 Person 类实例具有相同的 age,即 21 岁的 Dave 和 21 岁的 Mike。如果要对包含 Dave 和 Mike 的集合使用快速排序法按年龄排序,则无法保证每次运行算法时 Dave 都会出现在 Mike 之前,反之亦然。

快速排序

快速排序算法的执行流程如下:

- 将集合分成两个(大致)相等的部分,取一个伪随机元素并将其用作主元(注:“主元”,原文中 pivot,在多数介绍快速排序算法的中文资料中,并不对齐单独命名,只简单说成是“分割点”,即划分集合的元素,但本文作者使用了 pivot 这个词来表示“分割点”,译者认为很便于理解和表述,并将其翻译为“主元”)。

- 小于主元的元素将移动到其左侧,大于主元的元素将移动到其的右侧。

- 分别对于主元左侧和右侧的元素,重复此上述,直到对整个数组完成排序。

当我们将某个元素描述为比另一个元素“大”或“小”时,并不一定意味着这些元素是整数,我们可以自定义对象,并根据选择的任何属性进行排序。

假设有一个自定义类 Person,每个实例都有 name 和 age 属性,我们可以按姓名(词典顺序)或按年龄(升序或降序)进行排序。

faed600c03dd3c450a856581a19ed24b.png

快速排序算法的原理

对于快速排序算法,很多时候,数组是不能等分的,这是因为整个过程取决于如何选择主元。我们需要选择一个主元,使其比一半的元素大,比另一半的元素小。尽管这个过程看起来很直观,但很难做到。

想一想:如何为数组选择合适的主元?关于这个问题的解决方法,在快速排序算法的发展史上有过好多种。如果随机选择,是行不通的,因为这不能保障主元是合适的,随机选择的代价会非常“昂贵”。此外,还曾经有人提出从中间选择一个元元素,或者选择第一个、或者后半部分中间的,设置用更复杂的递归公式选择,等等。

最直接的最简单的方法是选择第一个(或最后一个)元素作为主元,具有讽刺意味的是,这会导致快速排序法对已经排序(或几乎排序了)的数组执行效果非常糟糕。

但是,大多数人还是用这种方法来实现快速排序算法,因为它很简单,而且这种选择主元的方式是一种非常有效的操作(我们需要重复执行),这也正是我们下面要做的。

既然我们选择了一个主元,接下来该用它做些什么?同样,有几种方法可以处理各个分区。我们要设置三个指针,有一个指向主元,另外两个分别指向“较小”元素和“较大”元素。

然后移动元素,使所有小于主元的元素都在左侧,而所有较大的元素都在右侧。更小或更大的元素不一定会被排序,我们只希望它们位于主元的适当的一侧。然后,用递归方法遍历主元的左侧和右侧。

一步一步来看看我们计划要做的事情,从而理解以上所说的快速排序算法的执行过程。使用下面显示的数组,我们选择了第一个元素作为主元(29),low 表示指向较小元素的指针,最右边的 high 表示指向较大元素的指针。

- 29 是第一主元,low 指向 99,high 指向 44

29 | 99 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44 (high)

- high 从右向左移动,直到找到一个小于主元的值

29 | 99 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21 (high),44

- 现在 high 指向了 21,它是一个比主元小的元素,我们想在数组的开头附近找到一个值,使之可以用于与 21 交换。用一个比主元还小的值交换是没有意义的,所以如果 low 指向一个更小的元素,我们试着找到一个更大的元素。

- 将 low 变量向右移动,直到找到一个大于主元的元素。幸运的是, low 已经定位在 99,它就比主元大

- 交换 high 和 low 指向的元素:

29 | 21 (low),27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,99 (high),44

- 在我们这样做之后,就将 high 继续向左移动,将 low 向右边移动(21 和 99 现在所处的位置是正确的)。

- 再次,将 high 向左移动,下一个数字就是小于主元的 12。

- 现在我们通过将 low 向右移动来找一个大于主元的值,就是 41 了,它是第一个这样的值

不断重复上述过程,直到 low 指针和 high 指针最终在某个元素相遇:

29 | 21,27,12,19,28 (low/high),44,78,87,66,31,76,58,88,83,97,41,99,44

- 不再使用 29 作为主元了,所以剩下唯一要做的就是交换主元和 high 所指元素 28,然后我们就完成了这个递归步骤:

28,21,27,12,19,29,44,78,87,66,31,76,58,88,83,97,41,99,44

如你所见,我们已经让小于 29 的所有值现在都在 29 的左侧,大于 29 的所有值都在右侧。

然后,算法对 28、21、27、12、19(左侧)集合和44、78、87、66、31、76、58、88、83、97、41、99、44(右侧)集合执行相同的操作。

实现

对数组排序

快速排序是一种自然递归算法,将输入数组分成较小的数组,将元素移动到主元的适当一侧,然后重复。

让我们来看看几个递归调用:

- 当第一次调用算法时,我们考虑所有的元素——从索引 0 到 n-1,其中 n 是数组中的元素数量。

- 如果主元最终位于位置 k,那么我们将对从 0 到 k-1 和从 k+1 到 n-1 的元素重复该过程。

- 在将元素从 k+1 排到 n-1 时,当前主元将在某个位置 p 结束。然后我们将元素从 k+1 序到 p-1,从 p+1 排序到 n-1,依此类推。

也就是说,我们将使用两个函数:partition() 和 quick_sort()。quick_sort() 函数将首先用 partition() 函数对集合分组,然后在每组上递归调用自己。

下面从 partition() 函数开始:

def partition(array, start, end):
    pivot = array[start]
    low = start + 1
    high = end

    while True:
        # If the current value we're looking at is larger than the pivot
        # it's in the right place (right side of pivot) and we can move left,
        # to the next element.
        # We also need to make sure we haven't surpassed the low pointer, since that
        # indicates we have already moved all the elements to their correct side of the pivot
        while low <= high and array[high] >= pivot:
            high = high - 1

        # Opposite process of the one above
        while low <= high and array[low] <= pivot:
            low = low + 1

        # We either found a value for both high and low that is out of order
        # or low is higher than high, in which case we exit the loop
        if low <= high:
            array[low], array[high] = array[high], array[low]
            # The loop continues
        else:
            # We exit out of the loop
            break

    array[start], array[high] = array[high], array[start]

    return high

最后,让我们实现 quick_sort() 函数:

def quick_sort(array, start, end):
    if start >= end:
        return

    p = partition(array, start, end)
    quick_sort(array, start, p-1)
    quick_sort(array, p+1, end)

有了这两个函数,就可以对一个简单的数组执行 quick_sort():

array = [29,99,27,41,66,28,44,78,87,19,31,76,58,88,83,97,12,21,44]

quick_sort(array, 0, len(array) - 1)
print(array)

输出:

[12, 19, 21, 27, 28, 29, 31, 41, 44, 44, 58, 66, 76, 78, 83, 87, 88, 97, 99]

由于此算法是非稳定的,所以不能保证这两个 44 的顺序总是这样的,也许会交换 —— 虽然这在整数数组中意义不大。

对自定义对象进行排序

有几种方法可以重写此算法,以便在 Python 中对自定义对象进行排序。一种非常典型的 Python 方法是实现给定类的比较运算符,这意味着我们实际上不需要更改算法实现,因为 >、=、<= 等也可以应用于类对象。

另一个选择是以参数的方式给函数传入一个比较函数,用这个方法来执行对象的实际比较。用这种方式重写算法函数以用于自定义对象是相当简单的。但是请记住,算法并不稳定。

让我们从 Person 类开始:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return self.name

这是一个非常基本的类,只有两个属性,name 和 age。我们希望使用 age 作为排序键,这将通过为排序算法提供自定义 lambda 函数来实现。

但首先,让我们看看如何在函数中实现算法。我们没有直接使用 <= 或 >= 运算符进行比较,而是在函数中来判断哪个 Person 实例的年龄更高:

def partition(array, start, end, compare_func):
    pivot = array[start]
    low = start + 1
    high = end

    while True:
        while low <= high and compare_fun(array[high], pivot):
            high = high - 1

        while low <= high and not compare_fun(array[low], pivot):
            low = low + 1

        if low <= high:
            array[low], array[high] = array[high], array[low]
        else:
            break

    array[start], array[high] = array[high], array[start]

    return high
def quick_sort(array, start, end, compare_func):
    if start >= end:
        return

    p = partition(array, start, end, compare_func)
    quick_sort(array, start, p-1, compare_func)
    quick_sort(array, p+1, end, compare_func)

现在,让我们对这些实例对象的集合进行排序。你可以看到,在 quick_sort 函数的参数中,有 lambda 函数,它会对 age 属性的值进行实际的比较:

p1 = Person("Dave", 21)
p2 = Person("Jane", 58)
p3 = Person("Matthew", 43)
p4 = Person("Mike", 21)
p5 = Person("Tim", 10)

array = [p1,p2,p3,p4,p5]

quick_sort(array, 0, len(array) - 1, lambda x, y: x.age for person in array:
    print(person)

输出是:

Tim
Dave
Mike
Matthew
Jane

通过这种方式实现算法,只要提供适当的比较函数,它就可以用于我们选择的任何自定义对象。

优化快速排序算法

考虑到快速排序独立地对给定数组的“一半”进行排序,那么它就非常便于实现并行计算。我们可以用一个单独的线程对数组的每个“一半”进行排序,理想情况下,可以将排序所需的时间减半。

但是,如果我们在选择主元时特别不走运,快速排序法可能会递归太深,此时即使用并行计算,其效率也不如归并排序。

对小数组进行排序时,建议使用非递归算法。即使是像插入排序这样的简单操作,在小数组上也比快速排序更有效。因此,理想情况下,我们可以检查排序对象是否只有少量元素(大多数建议是 10 个或更少),如果是这样,就用插入排序代替。

快速排序的一个流行变体是多主元快速排序,它使用 n-1 个主元将原始数组分解为 n 个较小的数组。然而,大多数情况下只使用两个主元,而不是更多。

有趣的事实:Java 7 的排序实现中使用了双主元快速排序,针对较小数组的则是插入排序。

结论

正如我们前面提到的,快速排序的效率很大程度上取决于主元的选择——它可以成就或突破算法时间(和堆栈空间)的复杂度。在使用自定义对象时,算法的非稳定性也是一个潜在的问题。

然而,尽管如此,快速排序的平均时间复杂度 O(nlogn) 和相对低的内存使用、简单的实现方法,使它成为一种非常有效和流行的算法。


推荐阅读:

30 行 Python 代码实现 Twitch 主播上线实时通知

从软件工程专业思考到的大前端技术栈


edbdcd8bd378d1c009af0d2b68753778.png

非营利组织 freeCodeCamp.org 自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。线下开发者社区遍布 160 多个国家、2000 多个城市。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。

你也想成为 freeCodeCamp 社区的贡献者吗?欢迎了解 招募丨freeCodeCamp 翻译计划

点击【在看】,与朋友交流?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/411629.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[jQuery]JQuery一个对象可以同时绑定多个事件,这是如何实现的?

JQuery一个对象可以同时绑定多个事件&#xff0c;这是如何实现的&#xff1f; ①$(document).ready(function() {$("button").bind({click: function() {$("p").slideToggle()},mouseover: function() {$("body").css("background-color&q…

张娟娟(为奥运冠军名字作诗)

张娟娟&#xff08;为奥运冠军名字作诗&#xff09;——代腾飞 2008年8月18日 于成都张弓搭箭射靶心娟娟俊美胜古今娟弓即破他人梦百步穿杨改乾坤 转载于:https://www.cnblogs.com/daitengfei/archive/2008/08/25/1276023.html

IO_ADDRESS()的实现【转】

上面我们说了如何去在系统中自己实现一个设置系统寄存器的一个方法&#xff0c;上面归根到底要进行物理地址到虚拟地址的映射 现在我们就说说IO_ADDRESS()的实现 #define __REG32ALI&#xff08;addr) (*((volatile unsigned long *)((addr) - ALI_REGS_PHYS_BASE ALI_REGS_V…

vscode标记_高效扩展工具让 VS Code 如虎添翼

Codelf 变量命名神器Star&#xff1a;10688https://github.com/unbug/codelf新建项目&#xff0c;变量&#xff0c;类&#xff0c;方法&#xff0c;接口都需要命名&#xff0c;一个好的命名可以一眼看出这个地方的功能&#xff0c;CodeIf 一键起名不再难&#xff0c;输入关键词…

[html] 如何实现标题栏闪烁、滚动的效果

[html] 如何实现标题栏闪烁、滚动的效果 定时器背景设置个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题

各种数据库连接字符串

1、ACCESS数据库连接set conn Server.CreateObject("ADODB.Connection")conn.Open("driver{Microsoft Access Driver (*.mdb)};dbq" &_Server.MapPath("person.mdb"))set rs conn.Execute( "SELECT * FROM grade" )For I 0 to …

linux服务器情况

查看Linux 进程命令 ps -aux 或者ps -ef linux 进程很多 如果需要查找某一个进程可以使用 管道和grep命令 Linux下常用命令 grep 匹配字符 ps 查询Linux进程 1.查看服务器CPU飙升卡爆&#xff0c;最后发现是服务器在跑挖矿程序&#xff0c;CPU使用率奇高。在此总结…

[html] 页面导入样式时,使用link和@import有什么区别?

[html] 页面导入样式时&#xff0c;使用link和import有什么区别&#xff1f; 区别&#xff1a; 1.link是HTML标签&#xff0c;import是css提供的。 2.link引入的样式页面加载时同时加载&#xff0c;import引入的样式需等页面加载完成后再加载。 3.link没有兼容性问题&#xff…

CSS 有关Position = absolute (绝对定位 是相对于谁而言)

css中有绝对定位法&#xff0c;以前一直搞不懂绝对定位是相对于谁而言的绝对定位。 现在搞清楚了&#xff0c;不是相对于父元素&#xff0c;也不是相对于BODY。 而是相对于所属元素树中&#xff0c;相邻最近的那个显示标识了position属性的元素。 比如 Code<div id"a&q…

[html] 简述超链接target属性的取值和作用

[html] 简述超链接target属性的取值和作用 _self: 在当前窗口打开页面 _blank: 在新窗口打开页面 _top: 在整个框架打开页面不是很理解个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣…

mysql gui vim_vim(一): 小技巧

1) 如何yank字符然后再查找 ( / Ctrl - R 0 ) The most recently yanked text will be stored in the 0 and registers (if no register was explicitly specified e.g. by xy ). Then you can paste the text of any that register in the last line (eith1) 如何yank字符然后…

服务端和客户端

1.一般来说&#xff0c;客户端就是我们使用的电脑&#xff08;包括我们使用的浏览器IE,Firefox&#xff09;&#xff1b;服务器端就是存放网页与数据库数据的服务器。 2.你是客户&#xff0c;因为你在访问&#xff0c;你访问的是服务端。去吃饭&#xff0c;你到饭店&#xff0c…

mysql 判断质数_java之判断输入的数是否为素数

import java.util.Scanner;public class TestIsSushu {public static void main(String[] args) {Scanner scan new Scanner(System.in);System.out.println("输入正整数&#xff1a;");int i scan.nextInt();if(i<0) {System.out.println("输入错误&#…

LeetCode Smallest Range

数据范围是3500,3500也就是说n的平方是可以接受的。这里告诉你就是有序的,也就是在提醒你可能会是一个类似于二分的算法,所以的话其实基于这两个认识的话我们就可以利用一个枚举叫二分的算法来解决这道题。怎么做呢&#xff1f;就首先的话我们要枚举一端,一端的话我们可以把所有…

Oracle创建用户、表空间、导入导出、...命令

//创建临时表空间create temporary tablespace test_temp tempfile E:\oracle\product\10.2.0\oradata\testserver\test_temp01.dbf size 32m autoextend on next 32m maxsize 2048mextent management local;//创建数据表空间create tablespace test_dataloggingdatafile E:\or…

[html] 简述下html5的离线储存原理,同时说明如何使用?

[html] 简述下html5的离线储存原理&#xff0c;同时说明如何使用&#xff1f; 原理&#xff1a;HTML5的离线存储是基于一个新建的.appcache文件的缓存机制(不是存储技术)&#xff0c;通过这个文件上的解析清单离线存储资源&#xff0c;这些资源就会像cookie一样被存储了下来。…

DCX读书报告Bring structure to the web有感

DCX今天做的学术报告直接采用MSRA荣研究员的“Bring Structure to the web”的PPT&#xff0c;读的效果很不错&#xff0c;看样子是下功夫了&#xff0c;他理解了80%以上 当然学术报告与读书报告&#xff1a; 1、从形式上而言&#xff0c;都应该采用中文&#xff0c;源图须重画…

Web前端3.0时代,“程序猿”如何“渡劫升仙”

Web前端入行门槛低&#xff0c;很多人在成为前端工程师后很容易进入工作的舒适区&#xff0c;认为该熟悉的业务已熟悉了&#xff0c;然后就是重复用轮子&#xff0c;这样很容易让自己的成长处于原地打转以及低水平重复的状态。 想要不被行业抛弃&#xff0c;就要努力提升自己。…

[html] viewport常见设置都有哪些?

[html] viewport常见设置都有哪些&#xff1f; width: widthdevice-widthinitial-scalemaximum-scaleuser-scalable个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&#xff0c; 但坚持一定很酷。欢迎大家一起讨论 主目录 与歌谣一起通关前端面试题…

mysql用 fifo 记录日志_MySQL一丢丢知识点的了解

1. MySQL体系结构从概念上讲&#xff0c;数据库是文件的集合&#xff0c;是依照某种数据模型组织起来并存放于二级存储器中的数据集合&#xff1b;数据库实例是程序&#xff0c;是位于用户与操作系统之间的一层数据管理软件&#xff0c;用户对数据库数据的任何操作&#xff0c;…