《算法图解》——第四章 快速排序

                      第四章   快速排序

1    分而治之(divided and conquer,D&C)

第一个🌰:如何将一块地均匀地分成方块,并确保分出的方块是最大的呢?

使用D&C策略(并非解决问题的算法,而是一种解决问题的思路)!D&C解决问题的两个步骤:

①找出基线条件,尽可能的简单

②不断讲问题分界,或者说缩小规模,使其满足基线条件

首先基线条件:一个条边的长度是另一条边的两倍。50m*25m

再找递归条件,这就要用到D&C策略了,先找出可容纳的最大方块,对余下方块使用同样的算法。

你可以从这块地中划出两个640 m×640 m的方块,同时余下一小块地640m*400m,适用于这小块地的最大方块,也是适用于整块地的最大方块(涉及欧几里得算法)。对这块地实用同样的办法,变成400m*240m,然后变为240m*160m,最后变为160m*80,达到了基线条件。

 

第二个🌰:给定一个数字数组。需要加总并返回

用循环很容易完成:

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

递归如何处理?

①找出基线条件:如果数组中只有一个数或者没有数,那么加总很好算

②每次递归调用都必须离空数组更近一步。

两者之间是等效的,但是右边的数组更短,缩小了问题的规模。

sum()函数的递归运行过程:

提示:编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。陷入困境时,请检查基线条件是不是这样的。

练习

4.1 请编写前述 sum 函数的代码。

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

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

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

4.3 找出列表中最大的数字。

def max(list):
    if len(list) == 2:
        return list[0] if list[0] > list[1] else list[1]
    sub_max = max(list[1:])                               #这里如果list很大,栈的空间就会很大,因为max()函数一直在运行,只到list被切分成长度为2
    return list[0] if list[0] > sub_max else sub_max
print(max([1,2,3,4,5,6,7,8,9]))


4.4 还记得第1章介绍的二分查找吗?它也是一种分而治之算法。你能找出二分查找算法的基线条件和递归条件吗?

基线条件:数组只包含一个元素。

递归条件:把数组分成两半,将其中一半丢弃,并对另一半执行二分查找。


 

 

 

2    快速排序

基线条件:数组为空或者只有一个元素,这样就不需要排序了

两个元素的数组进行元素比较即可。三个元素呢?使用D&C,将数组分解,直到满足基线条件。

快速排序的工作原理:

①从数组中选择一个元素,这个元素被称为基准值(pivot)

②找出比基准值小的元素以及比基准值大的元素,这被称为分区(partitioning),数组变为:

一个由所有小于基准值的数字组成的数组;基准值;一个由所有大于基准值的数组组成的子数组。

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

对三个元素的数组进行排序:

①选择基准值。

②将数组分成两个子数组:小于基准值的元素和大于基准值的元素。

③对这两个子数组进行快速排序。

包含四个元素呢?同样的做法,找出一个基准值,如果一个子数组有三个元素,对三个元素递归调用快速排序。那么五个元素同样也可以。

代码表示:

def quicksort(array):
    if len(array) < 2:
        return array
    else:
        pivot = array[0]                                       #将数组的第一个元素定为基准线
        less = [i for i in array[1:] if i <= pivot]            #遍历数组剩下元素,如果小于它,放入less列表
        greater = [i for i in array[1:] if i > pivot]          #遍历数组剩下元素,如果大于它,放入greater列表
        return quicksort(less) + [pivot] + quicksort(greater)  #对less和greater递归,最后返回排序数组
print quicksort([10, 5, 2, 3])


 

 

3    再谈大O表示法

快速排序的情况比较棘手,在最糟情况下,其运行时间为O(n 2 )。

与选择排序一样慢!但这是最糟情况。在平均情况下,快速排序的运行时间为O(n log n)。

比较合并排序和快速排序:

有时候,常量的影响可能很大,对快速查找和合并查找来说就是如此。快速查找的常量比合并查找小,因此如果它们的运行时间都为O(n log n),快速查找的速度将更快。实际上,快速查找的速度确实更快,因为相对于遇上最糟情况,它遇上平均情况的可能性要大得多。


 

 

4    平均情况和最糟情况

快速排序的性能高度依赖你选择的基准值。假设你总将第一个元素用作基准值,那么栈长为O(n),如果你总将中间的元素用作基准值,那么栈长为O(log n)。

实际上,在调用栈的每层都涉及O(n)个元素。

因此,完成每层所需的时间都是O(n)

在第二张图中,层数为O(log n)(用技术术语说,调用栈的高度为O(log n)),每层需要的时间为O(n)。因此整个算法需要的时间为O(n)  * O(log n) = O(n log n)。这就是最佳情况。在最糟情况下,有O(n)层,因此该算法的运行时间为O(n)  * O(n) =   O(n **2 )。

这里要告诉你的是,最佳情况也是平均情况。只要你每次都随机地选择一个数组元素作为基准值,快速排序的平均运行时间就将为O(n log n)。

 

练习
使用大O表示法时,下面各种操作都需要多长时间?

4.5 打印数组中每个元素的值。O(n)

4.6 将数组中每个元素的值都乘以2。O(n)

4.7 只将数组中第一个元素的值乘以2。O(1)

4.8 根据数组包含的元素创建一个乘法表,即如果数组为[2, 3, 7, 8, 10],首先将每个元素都乘以2,再将每个元素都乘以3,然后将每个元素都乘以7,以此类推。O(n**2 )。


 

 

5    小结
D&C将问题逐步分解。使用D&C处理列表时,基线条件很可能是空数组或只包含一个元素的数组。
实现快速排序时,请随机地选择用作基准值的元素。快速排序的平均运行时间为O(n log n)。
大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在。
比较简单查找和二分查找时,常量几乎无关紧要,因为列表很长时,O(log n)的速度比O(n)快得多。

 

posted @ 2018-04-23 12:54  方玲是个小可爱  阅读(3349)  评论(0编辑  收藏  举报