gogoend / blog

blogs, ideas, etc.
MIT License
9 stars 2 forks source link

快速排序 #36

Open gogoend opened 4 years ago

gogoend commented 4 years ago

背景

今天在写对象深拷贝的时候,脑袋一片混乱 —— 因为它用到了递归。 当然,对象实质上是一种树状结构,每个分叉节点对象结构都十分类似。 emmmmm,于是我突然想起了之前困扰我多年的,传说中的快速排序,貌似也是一种用递归写比较方便的算法(虽然我也很懵)。。。So,这种排序的姿势到底是什么呢?

为什么快?

因为它用到了二分法。二分法嘛,我感觉应该就是把一个大问题拆分为两个小问题,两个小问题再拆分成四个更小的问题,直到不能再拆,问题应该也就解决了(个人愚见)。

试着解析原理

emmmm,比如随便来一个数组。

[ 17, 7, 12, 32, 27, 6, 14, 22, 9, 3, 21 ]

我们选择任意一个数字,作为排序基准pivot。就选第一个吧,17。

我们看看比17小的数和大的数都有哪些

得到 [ 7, 12, 6, 14, 9, 3 ] 17 [ 32, 27, 22, 21 ]

接下来我们对两边集合中的内容,继续按照这种方式来排序。同样基准找第一个数。

得到 [ [ 6, 3 ] 7 [ 12, 14, 9 ] ] 17 [ [ 27, 22, 21 ] 32 [ ] ]

再继续看各个基准数两边集合中的数

得到 [[[ 3 ] 6 [ ] ] 7 [[ 9 ] 12 [ 14 ]]] 17 [[ [22,21] 27 [] ] 32 [ ]]

再继续

得到 [[[ 3 ] 6 [ ] ] 7 [[ 9 ] 12 [ 14 ]]] 17 [[ [ [ 21 ] 22 [] ] 27 [] ] 32 [ ]]

终于排完了。。。

我们把最后这一步的排序结果集合格式化一下,你可以发现,它看起来像一棵树:

[
    [
        [
            3
        ]
        6
        []
    ]
    7
    [
        [
            9
        ]
        12
        [
            14
        ]
    ]
]
17
[
    [
        [
            [
                21
            ]
            22
            []
        ]
        27
        []
    ]
    32
    []
]

树的深度表示排序次数。

因此我认为,快速排序应该就是: 不断地依据基准值,拆分数组中的内容,比基准值小数放一堆,比基准值大的数值放另一堆,继续在这两堆中选一个基准值执行相关操作,直到最后每一堆里只剩下一个数值,此时数组排序完成。

一个实例代码

还不懂递归?读完这篇文章保证你会懂_javascript技巧_脚本之家

参考资料

快速排序 - 如果天空不死 - 博客园

gogoend commented 4 years ago

尝试手写快排代码

(想不到都有一个月没动过这篇文章了,真是罪过。不过正好可以考一下自己还能不能记清快排的原理)

let arrToSort = [ 17, 7, 12, 32, 27, 6, 14, 22, 9, 3, 21 ];
function qSort(arr){
    if(!arr.length) return arr
    let [pivot,...rest] = arr
    let smaller = [],bigger = [];
    rest.forEach(item =>{
        item>pivot ?bigger.push(item):smaller.push(item)        
    })
    return [...qSort(smaller),pivot,...qSort(bigger)]
}
qSort(arrToSort)

果然我还是忘了。。。又看了一遍上一节中提及的参考链接。

这个函数中巧妙使用了数组解构以及展开,简化了数组分割与合并的操作。 在使用快排时: 首先在传入数组中找到一个基准值pivot,用于和数组里其他元素做对比;此处用的pivot取的是数组里第一个元素,剩下的元素将放入到rest数组中。接下来将rest里的各个元素和基准值pivot进行对比,分别将较大值和较小值放入两数组,在基准值pivot两侧。 然后对基准值两侧的数组按照此处方法进行重复操作,进行递归。在递归过程中如果遇到空数组,则表示无元素可排序,直接返回空数组即可。当递归结束后,最终返回的、展开的数组将变得有序。

gogoend commented 4 years ago

快排的时间复杂度与稳定性

稳定性

首先看一下算法稳定性的定义:

假设在数列中存在a[i]=a[j]。 若在排序之前,a[i]在a[j]前面, 并且排序之后,a[i]仍然在a[j]前面, 则这个排序算法是稳定的。

(个人猜测)就冒泡、插入这两种简单排序算法来说,它们之间的比较仅限于相邻元素,大的都往后,小的都往前,相等的不交换,所以这两种算法是稳定的。而对于快速排序,假设排序过程中选择的某个基准值与其他某个值相等,那么是可能发生顺序改变的,因此不稳定。

时间复杂度

(笔者能力有限,遂直接引入了参考资料中的解释)

快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。 这句话很好理解:假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),需要遍历多少次呢?至少lg(N+1)次,最多N次。 (01) 为什么最少是lg(N+1)次?快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。因此,快速排序的遍历次数最少是lg(N+1)次。 (02) 为什么最多是N次?这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是N。因此,快读排序的遍历次数最多是N次。