算法图解--学习笔记(快速排序)

116 阅读3分钟

关键词

  • 快速排序
  • 递归
  • 分而治之
  • 合并排序
  • 归纳证明

数学

归纳证明

归纳证明是一种证明算法行之有效的方式,它分两步:基线 条件和归纳条件。是不是有点似曾相识的感觉?例如,假设我要证明我能爬到梯子的最上面。 递归条件是这样的:如果我站在一个横档上,就能将脚放到下一个横档上。换言之,如果我站 在第二个横档上,就能爬到第三个横档。这就是归纳条件。而基线条件是这样的,即我已经站 在第一个横档上。因此,通过每次爬一个横档,我就能爬到梯子最顶端。

  • 归纳条件
  • 基线条件

时间复杂度

快速排序的速度高度依赖于基准值。最糟的情况时间复杂度为O(n²), 最好的情况是O(n * log n)

最糟糕的情况

如果数组是有序的,而且永远选择第一个元素作为基准值,那么这个调用栈就变得最长了,调用栈的高度更好等于数组的长度,因为基准值的一边总是空的。这个就是最坏的情况,时间复杂度为O(n * n) = O(n²)。

计算公式: 每一次调用栈需要遍历所有元素一次,时间复杂度记录为 O(n),调用栈的高度刚好等于数组长度O(n), 所以,时间复杂度为O(n * n) = O(n²)

最佳的情况(也是平均情况)

基准值选择假设刚好每次都是中间的数,那么调用栈就变得非常短,因为每一次都把数组切成两半,所以不需要那么多递归,很快就达到了基准条件。调用栈的时间复杂度为O(log n),整个算法时间复杂度为O(n) * O(log n) = O(n * log n)

注: 虽然快速排序的时间复杂度跟基准值有很大的关系,实际上,快速查找的速度确实更快,因为相对于遇上最糟情况,它遇上平均情况的可能性要大得多。所以,快速排序总的来说还是最快的排序算法之一。

练习


func QuiklyOrder(arr []int) (res []int) {
    if len(arr) < 2 {
        return arr;
    }
    left := []int{};
    right := []int{};
    pivot := arr[0];
    for index, _ := range arr {
        if index != 0 {
            if arr[index] <= pivot {
                left = append(left, arr[index])
            } else {
                right = append(right, arr[index])
            } 
        }
    }
    newLeft := QuiklyOrder(left)
    newRight := QuiklyOrder(right)
    result := append(append(newLeft, pivot), newRight...)
    return result
}

func main() {
    arr := []int{2, 1, 4, 5, 4, 7, 8, 3};
    results := QuiklyOrder(arr)
    fmt.Println("原数组", arr)
    fmt.Println("排序后", results)
}

// 输出结果

$ go run main.go
原数组 [2 1 4 5 4 7 8 3]
排序后 [1 2 3 4 4 5 7 8]

小结

  • 实现快速排序时,请随机地选择用作基准值的元素
  • 归纳证明这个数据知识很重要,涉及到公式的推导
  • 大O表示法中的常量有时候事关重大,这就是快速排序比合并排序快的原因所在?