《算法图解》学习笔记—第4章 快速排序

4.1 分而治之

分而治之(divide and conquer,D&C)——一种著名的递归式问题解决方法。D&C并非可用于解决问题的算法,而是一种解决问题的思路。D&C的工作原理:
(1) 找出简单的基线条件;
(2) 确定如何缩小问题的规模,使其符合基线条件。

示例:给定一个数字数组,需要将这些数字相加,并返回结果。
在这里插入图片描述

使用循环很容易完成这种任务。

def sum(arr):
    total = 0
    for x in arr:
        total += x
    return total

如何使用递归函数来完成这种任务呢?
**第一步:找出基线条件。**如果数组不包含任何元素或只包含一个元素,计算总和将非常容易。
在这里插入图片描述
第二步:每次递归调用都必须离空数组更近一步。
在这里插入图片描述
给函数sum传递的数组变的更短。换言之,这缩小了问题的规模!
在这里插入图片描述
编写涉及数组的递归函数时,基线条件通常是数组为空或只包含一个元素。陷入困境时,请检查基线条件是不是这样的。

s u m sum sum函数代码:

def sum(list):
    if list == []:
       return 0
    return list[0] + sum(list[1:])

练习

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

def count(list):
    if list == []:
       return 0
    return 1 + count(list[1:])

找出列表中最大的数字。

def max(list):
    if len(list) == 2:
       return list[0] if list[0] > list[1] else list[1]
    sub_max = max(list[1:])
       return list[0] if list[0] > sub_max else sub_max

4.2 快速排序

快速排序是一种常用的排序算法,比选择排序快得多。
使用快速排序对数组进行排序。对排序算法来说,最简单的数组什么样呢?基线条件为数组为空或只包含一个元素。在这种情况下,只需原样返回数组——根本就不用排序。
在这里插入图片描述
使用D&C,需要将数组分解,直到满足基线条件。下面介绍快速排序的工作原理。假设一个包含四个元素的数组如下:
在这里插入图片描述
首先,从数组中选择一个元素,这个元素被称为基准值(pivot)。接下来,找出比基准值小的元素以及比基准值大的元素。
在这里插入图片描述

这被称为分区(partitioning)。现在你有:

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

只要对这两个子数组进行快速排序,再合并结果,就能得到一个有序数组!
在这里插入图片描述
快速排序步骤如下:

  1. 选择基准值。
  2. 将数组分成两个子数组:小于基准值的元素和大于基准值的元素。
  3. 对这两个子数组进行快速排序。

快速排序的代码如下:

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)

4.3 再谈大O表示法

快速排序在最糟情况下,其运行时间为O( n 2 n^2 n2),与选择排序一样慢!但这是最糟情况。在平均情况下,快速排序的运行时间为O( n l o g n n log n nlogn)。还有一种名为合并排序(merge sort)的排序算法,其运行时间为O( n l o g n n log n nlogn)。
在这里插入图片描述

比较合并排序和快速排序

假设有下面这样打印列表中每个元素的简单函数。

def print_items(list):
    for item in list:
       print item

这个函数遍历列表中的每个元素并将其打印出来。它迭代整个列表一次,因此运行时间为O( n n n)。现在假设你对这个函数进行修改,使其在打印每个元素前都休眠1秒钟。

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

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

在这里插入图片描述
虽然使用大O表示法表示时,这两个函数的速度相同,但实际上print_items的速度更快。在大O表示法O( n n n)中, n n n实际上指的是这样的。

在这里插入图片描述
c c c是算法所需的固定时间量,被称为常量。大O表示通常不考虑这个常量,因为有时常量根本没有什么影响。参考简单查找和二分查找的大O表示:
在这里插入图片描述

但有时候,常量的影响可能很大,对快速查找和合并查找来说就是如此。实际上,快速查找的速度确实更快,因为相对于遇上最糟情况,它遇上平均情况的可能性要大得多。

平均情况和最糟情况

快速排序的性能高度依赖于你选择的基准值。假设你总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。
在这里插入图片描述
现在假设你总是将中间的元素用作基准值,在这种情况下,调用栈如下。
在这里插入图片描述

调用栈短得多!因为你每次都将数组分成两半,所以不需要那么多递归调用。你很快就到达了基线条件,因此调用栈短得多。
第一个示例展示的是最糟情况,而第二个示例展示的是最佳情况。在最糟情况下,栈长为
O( n n n),有O( n n n)层,因此该算法的运行时间为O( n n n) * O( n n n) = O( n 2 n^2 n2)。而在最佳情况下,栈长为O( l o g n log n logn),层数为O( l o g n log n logn),因此整个算法需要的时间为O( n n n) * O( l o g n log n logn) = O( n l o g n n log n nlogn)。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值