算法图解之Swift实践【第四章 快速排序】

165 阅读2分钟

本人已参与「新人创作礼」活动,一起开启掘金创作之路。

分而治之

概念

分而治之(divide and conquer,D&C)—— 一种著名的递归式问题解决方法。 D&C的工作原理:

  • 找出简单的基线条件;
  • 确定如何缩小问题的规模,使其符合基线条件。 例如,对于数组类问题的基线条件,则常常是数组为空,或数组内仅有一个元素。 分解方式则通常是将其分为两个尽可能等大小的数组。

练习

4.1 - 4.3 见算法图解之Swift实践【第三章 递归】 4.4 还记得第1章介绍的二分查找吗?它也是一种分而治之算法。你能找出二分查找算法的基线条件和递归条件吗?

基线条件:数组范围缩小到只有一个元素 递归条件:将数组分为两部分,根据对比目标值和区间低位和高位间的关系,丢弃其中不符合条件的一半,并对另一半继续进行二分法查找。

快速排序

实现

快速排序用到了分治的思想。

func quickSort(_ arr: [Int]) -> [Int] {
    if arr.count < 2 { // 数组元素个数为1或者数组为空,无需排序,直接返回原数组
        return arr
    }
    
    let pivot = arr[0] // 选取数组的第一个元素作为基准点
    var leftArr: [Int] = []
    var rightArr: [Int] = []
    
    for i in 1..<arr.count { // 从第二个元素开始遍历
        if arr[i] < pivot { // 得到所有比基准值小的数组
            leftArr.append(arr[i])
        } else { // 得到所有比基准值大的数组
            rightArr.append(arr[i])
        }
    }
    
    // 将子数组进行排序后,再与基准值一同组成有序数组
    return quickSort(leftArr) + [pivot] + quickSort(rightArr) }

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

扩展与应用

LeetCode 剑指 Offer 40. 最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1: 输入:arr = [3,2,1], k = 2 输出:[1,2] 或者 [2,1]

示例 2: 输入:arr = [0,1,2,1], k = 1 输出:[0]

限制: 0 <= k <= arr.length <= 10000 0 <= arr[i] <= 10000

这题的结题思路可以很简单,求最小的k个数组成的数组,首先我们可以考虑将数组进行排序,再截取数组的前k个元素即可得到结果。

其中排序可以用到本章学习的快速排序

func getLeastNumbers(_ arr: [Int], _ k: Int) -> [Int] {
    if arr.count == 0 || k == 0 {
        return []
    }
    
    let sortedArr = qsort(arr)
    return Array(sortedArr[0...k-1])
}

// 快排
func qsort(_ arr: [Int]) -> [Int] {
    if arr.count < 2 {
        return arr
    }
    
    let pivot = arr[0]
    var leftArr: [Int] = []
    var rightArr: [Int] = []
    
    for i in 1..<arr.count {
        if arr[i] < pivot {
            leftArr.append(arr[i])
        } else {
            rightArr.append(arr[i])
        }
    }
    
    return qsort(leftArr) + [pivot] + qsort(rightArr)
}

大O表示法的平均情况和最糟情况

最糟情况指的是算法运行时可能遇到的最坏的情况。

平均情况指的是算法遇到时的最佳情况。(最佳情况也是平均情况)

  1. 快速排序的平均情况为O(n log n),最糟情况为O(n^2)。这取决于基准值选取的好坏。
  2. 当每次基准值选取的均为最差情况时,则需要对包含n个元素的数组进行n次基准值的选取。当每次基准这选取的均为最佳情况时,则仅需要log n次选取。(二分)
  3. 而当每一次选取基准值后,数组的每个元素均需要和基准值进行对比,因此,每次选取基准值之后需要进行n次比较。
  4. 因此,快速排序的最糟情况为O(n * 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)

小结

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