【第四天】算法图解 之 快速排序

简介: 【第四天】算法图解 之 快速排序

分而治之(D&C)——一种著名的递归式问题解决方法


D&C算法——快速排序。快速排序是一种排序算法,速度比第二章介绍的选择排序快得多。


1. 分而治之


假如你是一个农场主,有一小块土地,你要将这块土地均匀地分成方块,且分出的方块要尽可能大。



用D&C方法1>找出基线条件,这种条件必须尽可能简单;2>不断将问题分解(或缩小规模),直到符合基线条件


找出基线条件,最容易处理的情况是,一条边的长度是另一条边的整数倍


根据D&C的定义,每次递归调用都必须缩小问题的规模。我们首先找出这块地可容纳的最大方块。



那么,何不对余下的那一小块地使用相同的算法呢?



余下的土地满足基线条件,因为160是80的整数倍


因此,对于最初的土地,适用的最大方块为80m*80m


D&C并非可用于解决问题的算法,而是一种解决问题的思路


再看一个例子:


给定一个数组:【2 | 4 | 6】   要将这些数字相加,并返回结果


使用循环很容易:


def sum(arr):
    total = 0
    for x in arr:
        total = total + x
    return total
print(sum([1,2,3,4]))


运行:10


使用递归函数


1> 找出基线条件:如果数组不包含任何元素或只包含一个元素,计算总和将非常容易


基线条件:[ ]          不包含任何元素 ==> 总和为0


                 [7]         只包含一个元素 ==> 总和为7


因此这就是基线条件


2> 每次递归调用都必须离空数组更进一步




 


递归记录了状态


既然使用循环可以轻松完成任务,为何还要使用递归呢?


Haskell等函数式编程语言没有循环!


练习:


·请编写前述sum函数的代码


def sum(list):
    if list == []:
        return 0
    else:
        return list[0] + sum(list[1:])
print(sum([2,4,6]))


运行:12


·编写一个递归函数来计算列表包含的元素数


def count(list):
    if list == []:
        return 0
    else:
        return 1+count(list[1:])
print(count([2,4,6]))


运行:3


2.快速排序


快速排序是一种常用的排序算法,比选择排序快得多。


使用快速排序对数组进行排序。对排序算法来说,最简单的数组就是不需要排序的数组。因此,基线条件为数组为空或只包含一个元素。在这种情况下,只需原样返回数组——根本不用排序,如下


def quicksort(array):
    if len(array) < 2:
        return array


更长点:


两个:[1 | 7]  检查第一个元素是否比第二个元素小,如果不比第二个小,就交换他们的位置。


三个:[33 | 15 | 10]  将数组分解,直到满足基线条件。


其解释如下


快速排序工作原理:


从数组中选择一个元素,这个元素被称为基准值。暂时将数组的第一个元素用作基准值,找出比基准值小的元素以及比基准值大的元素



这被称为分区,现在你有:


1> 一个由所有小于基准值的数字组成的子数组


2> 基准值


3> 一个由所有大于基准值的数字组成的数组


这里只进行了分区,得到的两个子数组是无序的。但如果这两个数组是有序的,对整个数组进行排序将非常容易。


如果子数组是有序的,就像下面这样合并得到一个有序数组:左边的数组+基准值+右边的数组


在这里就是[10,15]+[33]+[ ],结果为[10,15,33]


如何对子数组进行排序?


对于包含两个元素的数组(左边的空数组)以及空数组(右边的数组),快速排序知道如何将他们排序,因此只要对这两个子数组进行快速排序,再合并结果,就能得到一个有序数组!


quicksort([15,10]) + [33] + quicksort([ ])


-->[10, 15, 33]


不管将哪个元素用作基准值,这都管用



这个子数组都只有一个元素,而你知道如何对这些数组进行排序。


三个元素(15作基准值):1> 选择基准值; 2> 将数组分成两个子数组:小于和大于基准值的元素; 3> 对这两个子数组进行快速排序



四个元素(33作基准值):左边包含三个元素,对三个元素的数组进行排序,对其递归地调用快速排序


因此你能够对包含四个元素的数组进行排序,接着就能对包含五个元素的数组进行排序。


为什么呢?假设有一个包含五个元素的数组。



这些子数组包含的元素数都在0-4内,而你已经知道如何用快速排序对包含0-4个元素的数组进行排序!


将任何元素用作基准值都可行,因此可以对包含五个元素的数组进行排序,同理,六个、七个……以此类推


* 归纳证明


这是一种证明算法行之有效的方式,他分两步:基线条件和归纳条件


递归条件:如果我站在一个横档上,就能将脚放到下一个横档上


归纳条件:如果我站在第二个横档上,就能爬到第三个横档


基线条件:我已经站在第一个横档上,通过每次爬一个横档,我就能爬到梯子最顶端


在基线条件中,我证明这种算法对空数组或包含一个元素的数组管用


在归纳条件中,我证明如果快速排序对包含一个元素管用,对包含两个元素的数组也管用;如果……两个……三个……以此类推。快速排序对任何长度的数组都管用


快速排序示例代码


def quicksort(array):
    if len(array) < 2:
        return array
    else:
        pivot = array[0]
        less = [i for i in array[1:] if i <= pivot]
        greater = [i for i in array[1:] if i > pivot]
        return quicksort(less) + [pivot] + quicksort(greater)
print(quicksort([10, 5, 2, 3]))


运行:


[2, 3, 5, 10]


3. 再谈大O表示法


快速排序的独特之处在于,其速度取决于选择的基准值



上述图表中的时间是基于每秒执行十次操作计算得到的。实际上,计算机每秒执行的操作数远不止十次


还有一种名为合并排序的排序算法,其运行时间为O(nlogn),此选择排序快得多!快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n^2)


1> 比较合并排序和快速排序


有一个打印列表中每个元素的简单函数


1. def print_items(list):
2. for item in list:
3. print(item)


它迭代整个列表一次,因此运行时间为O(n)


现在对这个函数进行修改,使其在打印每个元素前都休眠1秒钟


from time import sleep
def print_item2(list):
    for item in list:
        sleep(1)
        print(item)


它在打印每个元素前都暂停1秒钟,假设你使用这两个函数来打印一个包含5个元素的列表:


[2 | 4 | 6 | 8 | 10]


print_item : 2 4 6 8 10


print_item2 : <休眠>   2<休眠>  4<休眠>  6<休眠>  8<休眠>  10

这两个函数都迭代整个列表一次,因此运行时间都为O(n)


但是print_item要快得多,因为它没有在每次打印前都暂停1秒钟


因此,虽然使用大O表示法表示时,这两个函数的速度相同,但实际上print_item的速度更快。在大O表示法中,n实际上指的是这样的:c是算法所需的固定时间量,被称为常量。例如print_item所需时间可能是10毫秒*n,而print_item2所需的时间为1秒*n

通常不考虑这个常量。因为如果两种算法的大O运行时间不同,这种常量无关紧要。举例:


简单查找:10毫秒*n            二分查找:1秒*logn


你可能认为,简单查找的常量为10毫秒,而二分查找的常量为1秒,因此简单查找的速度要快得多。现在假设你要在包含40亿个元素的列表中查找,所需时间将如下:

简单查找: 10毫秒*40亿 = 463天            二分查找: 1秒*32 = 32秒


二分查找的速度还是快得多,常量根本没有什么影响


但有时候,常量的影响可能很大,对快速查找和合并查找来说就是如此


快速查找的常量比合并查找小,因此如果他们的运行时间都为O(nlogn),快速查找的速度更快

实际上,快速查找的速度确实更快,因为相对于遇上最糟情况,它遇上平均情况的可能性要大很多。


2> 平均情况和最糟情况


快速排序的性能高度依赖于你选择的基准值


假设你总是将第一个元素用作基准值,且要处理的数组是有序的,由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序



数组并没有被分成两半,相反,其中一个子数组始终为空,这导致调用栈非常长


现在假设你总是将中间元素用作基准值



第一个示例展示的是最糟情况,而第二个展示的是最佳情况


在最糟情况下,栈长为O(n),而在最佳情况下,栈长为O(logn)


* 现在来看看栈的第一层,你将一个元素用作基准值,并将其他的元素划分到两个子数组中。这涉及数组中的全部8个元素,因此该操作的时间为O(n)。在调用栈的第一层,涉及全部8个元素,但实际上,在调用栈的每层都涉及O(n)个元素。即使以不同的方式划分数组,每次也将涉及O(n)个元素



在这个示例中层数为O(logn)(调用栈的高度为O(logn)),而每层需要的时间为O(n)。因此整个算法的运行时间为O(n)*O(logn)=O(nlogn),这是最佳情况。最糟情况下为O(n)*O(n)=O(n^2)


最佳情况也是平均情况


只要每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(nlogn)。快速排序是最快的排序算法之一,也是D&C典范。


4. 小结


①D&C将问题逐步分解,使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组


②实现快速排序时,请随机的用作基准值的元素。快速排序的平均运行时间为O(nlogn)


③大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在


④比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(logn)的速度比O(n)快得多

相关文章
|
7天前
|
搜索推荐 C语言
【排序算法】快速排序升级版--三路快排详解 + 实现(c语言)
本文介绍了快速排序的升级版——三路快排。传统快速排序在处理大量相同元素时效率较低,而三路快排通过将数组分为三部分(小于、等于、大于基准值)来优化这一问题。文章详细讲解了三路快排的实现步骤,并提供了完整的代码示例。
28 4
|
1月前
|
算法 搜索推荐 Shell
数据结构与算法学习十二:希尔排序、快速排序(递归、好理解)、归并排序(递归、难理解)
这篇文章介绍了希尔排序、快速排序和归并排序三种排序算法的基本概念、实现思路、代码实现及其测试结果。
20 1
|
1月前
|
搜索推荐 Java Go
深入了解快速排序算法
深入了解快速排序算法
30 2
|
1月前
|
存储 搜索推荐 算法
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
|
1月前
|
算法 Python
Python算法编程:冒泡排序、选择排序、快速排序
Python算法编程:冒泡排序、选择排序、快速排序
|
1月前
|
搜索推荐 C语言 C++
【C语言】指针篇-精通库中的快速排序算法:巧妙掌握技巧(4/5)
【C语言】指针篇-精通库中的快速排序算法:巧妙掌握技巧(4/5)
|
3月前
|
搜索推荐 算法 Java
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
该博客文章通过UML类图和Java源码示例,展示了如何使用适配器模式将QuickSort类和BinarySearch类的排序和查找功能适配到DataOperation接口中,实现算法的解耦和复用。
39 1
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
|
3月前
|
算法 搜索推荐
算法设计 (分治法应用实验报告)基于分治法的合并排序、快速排序、最近对问题
这篇文章是关于分治法应用的实验报告,详细介绍了如何利用分治法实现合并排序和快速排序算法,并探讨了使用分治法解决二维平面上的最近对问题的方法,包括伪代码、源代码实现及时间效率分析,并附有运行结果和小结。
|
4月前
|
算法 搜索推荐 编译器
算法高手养成记:Python快速排序的深度优化与实战案例分析
【7月更文挑战第11天】快速排序是编程基础,以O(n log n)时间复杂度和原址排序著称。其核心是“分而治之”,通过选择基准元素分割数组并递归排序两部分。优化包括:选择中位数作基准、尾递归优化、小数组用简单排序。以下是一个考虑优化的Python实现片段,展示了随机基准选择。通过实践和优化,能提升算法技能。**
56 3
|
5月前
|
搜索推荐 算法 Java
Java中的快速排序、归并排序和堆排序是常见的排序算法。
【6月更文挑战第21天】Java中的快速排序、归并排序和堆排序是常见的排序算法。快速排序采用分治,以基准元素划分数组并递归排序;归并排序同样分治,先分割再合并有序子数组;堆排序通过构建堆来排序,保持堆性质并交换堆顶元素。每种算法各有优劣:快排平均高效,最坏O(n²);归并稳定O(n log n)但需额外空间;堆排序O(n log n)且原地排序,但不稳定。
46 3