前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >快速排序和高阶函数

快速排序和高阶函数

作者头像
Sheepy
发布2018-09-10 12:14:44
5850
发布2018-09-10 12:14:44
举报

快速排序(以下简称快排)是一种经典的排序算法,名字乍一看非常实在,细思之下却又带着点不可一世的狂傲。别的排序算法像什么插入排序、选择排序、归并排序等等,它们的名字其实都是在自解释,无非是在告诉别人我到底是怎么排的。然而快排却说,我很快,所以我叫快速排序。

你只要记住,我很快.jpg

好,在下认输。

当然,快排很快,这是真的,在实践中可以做到比归并排序快3倍以上(需要一定的优化)。快排的基本思想其实很简单,就是交换 + 分治,可以看作是对冒泡排序的一种改进。具体的我就不啰嗦了,相信大家对这个也非常熟悉了,实在不了解的同学可以先Google一下。我直接上Swift的代码好了(对我就是喜欢Swift),注释也写得很清楚:

//最坏情况(初始数组顺序或逆序): 
//T(n) = T(0) + T(n-1) + θ(n) = θ(1) + T(n-1) + θ(n) = T(n-1) + θ(n) = θ(n²)(等差级数)
//一般情况: T(n) = θ(nlgn)
func quickSort(inout list: [Int], startIndex: Int, endIndex: Int) {
    //若startIndex<endIndex则序列至少有2个元素,其余情况(只有1个或0个)不需要排序直接返回
    guard startIndex < endIndex else {
        return
    }
    //排序,并返回参考点(参考点左侧的数都小于参考点,右侧的都大于参考点)
    let referenceIndex = divide(&list, startIndex: startIndex, endIndex: EndIndex)
    //递归,对参考点左边部分排序
    quickSort(&list, startIndex: startIndex, endIndex: referenceIndex - 1)
    //递归,对参考点右边部分排序
    quickSort(&list, startIndex: referenceIndex + 1, endIndex: endIndex)
}

上面的代码已经实现了快排的整体过程,但是divide这个函数还没有定义,下面就来实现它:

func divide(inout list: [Int], startIndex: Int, EndIndex: Int) -> Int {
    //用来记录参考点位置(遍历完成之后用来放置序列的第一个数)
    var referenceIndex = startIndex
    //参考点的值(序列中第一个元素)
    let referencePoint = list[startIndex]
    //遍历序列,与参考点比较
    for compareIndex in startIndex+1 ... EndIndex {
        //若小于参考点,就跟referenceIndex后的元素交换,referenceIndex前进一位
        if list[compareIndex] < referencePoint {
            (list[referenceIndex+1], list[compareIndex]) = (list[compareIndex], list[referenceIndex+1])
            referenceIndex++
        }
    }
    //将序列第一个元素放到参考点位置,它左边的值都比它小,右边的都比他大
    (list[startIndex], list[referenceIndex]) = (list[referenceIndex], list[startIndex])
    //返回参考点位置
    return referenceIndex
}

这样整个过程就完成了。其实上面的

if list[compareIndex] < referencePoint { 
    (list[referenceIndex+1], list[compareIndex]) = (list[compareIndex], list[referenceIndex+1]) 
    referenceIndex++ 
}

可以改为:

if list[compareIndex] < referencePoint {
    (list[referenceIndex], list[compareIndex]) = (list[compareIndex], list[++referenceIndex])
}

少了一行代码,但是不太好理解,有点得不偿失,所以还是算了。现在测试一下:

//测试数组
var testList = [3, 8, 9, 10, 2, 1]
quickSort(&testList, startIndex: 0, EndIndex: testList.count - 1)

var testList2 = [28, 3, 76, 99, 42, 111, 88, 99, 75]
quickSort(&testList2, startIndex: 0, EndIndex: testList2.count - 1)

嗯,亲测有效。

开头的代码注释上我写了快排的时间复杂度分析,在最坏情况下其实效率很低,跟冒泡选择那些『慢速』排序差不多,都是θ(n²)。造成这种情况的原因是,如果每次选择的参考点都是最小或者最大的那个,那么所谓的分治就失去了意义,因为每次遍历完序列都是把参考点单独拎出,然后剩下的数据归为一组(都比参考点大或者小),在过程中会出现n组序列,每组都要遍历一遍,效率自然低下。

基于上述思路,有一种很直接的优化方法,就是选取参考点的时候不再使用第一个元素,而是随机选取。这么做了之后,在最坏的情况下时间复杂度其实还是θ(n²),但最坏情况的出现跟待排序的序列顺序已经无关,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到θ(nlgn)的期望时间复杂度。

要实现随机化快排,只需要在原先的divide函数开头加上这两句就行:

//获得一个在startIndex和EndIndex之间的随机数
let random = getRandomNumIn(startIndex ... EndIndex)
//将序列的第一个元素与随机参考点进行交换
(list[startIndex], list[random]) = (list[random], list[startIndex])

获取随机数的函数:

func getRandomNumIn(range: Range<Int>) -> Int {
    guard let min = range.first, let max = range.last else {
        return 0
    }
    let delta = max - min + 1
    //不能直接arc4random % delta,否则在x86、x64不同平台运行时由于字长不同会出现不可测错误
    let randomDelta = Int(arc4random() % UInt32(delta))
    let randomNum = min + randomDelta
    return randomNum
}

对外提供一个randomQuickSort函数:

func randomQuickSort(inout list: [Int], startIndex: Int, EndIndex: Int) {
    guard startIndex < EndIndex else {
        return
    }
    //排序,并返回参考点(参考点左侧的数都小于参考点,右侧的都大于参考点)
    let referenceIndex = randomDivide(&list, startIndex: startIndex, EndIndex: EndIndex)
    //递归对参考点左边部分排序
    randomQuickSort(&list, startIndex: startIndex, EndIndex: referenceIndex - 1)
    //递归对参考点右边部分排序
    randomQuickSort(&list, startIndex: referenceIndex + 1, EndIndex: EndIndex)
}

func randomDivide(inout list: [Int], startIndex: Int, EndIndex: Int) -> Int {
    let random = getRandomNumIn(startIndex ... EndIndex)
    (list[startIndex], list[random]) = (list[random], list[startIndex])
    
    return divide(&list, startIndex: startIndex, EndIndex: EndIndex)
}

好了,快排讲完了。接下来讲讲高阶函数。高阶函数简单来说呢,就是函数可以作为变量、参数、返回值等等,总之函数是一等公民。Swift是一个多范式语言,具有一些函数式语言的特性,函数自然便是一等公民。下面我还是以快排代码为例来解释一下。之前我们的quickSortdivide是两个独立的函数,quickSort在内部调用divide函数的时候需要传一堆参数。而且 divide这个函数可能被别的函数调用,或者被直接使用,如果传入的序列跟 quickSort使用的是同一个的话,序列就有可能被意外地多次改变,不能被正确排序。这种情况下,我们可以把divide定义在quickSort内部:

func customQuickSort(inout list: [Int], startIndex: Int, EndIndex: Int) {
    let divide: () -> Int = {
        //用来记录参考点位置(遍历完成之后用来放置序列的第一个数)
        var referenceIndex = startIndex
        //参考点的值(序列中第一个数)
        let referencePoint = list[startIndex]
        //遍历序列,与参考点比较
        for compareIndex in startIndex+1 ... EndIndex {
            //若小于参考点,就跟referenceIndex后的数交换,referenceIndex前进一位
            if list[compareIndex] < referencePoint {
                (list[referenceIndex], list[compareIndex]) = (list[compareIndex], list[++referenceIndex])
            }
        }
        //将序列第一个数放到参考点位置,它左边的值都比它小,右边的都比他大
        (list[startIndex], list[referenceIndex]) = (list[referenceIndex], list[startIndex])
        //返回参考点位置
        return referenceIndex
    }
    
    //startIndex==EndIndex表明这一部分已排序完成
    guard startIndex < EndIndex else {
        return
    }
    //排序,并返回参考点(参考点左侧的数都小于参考点,右侧的都大于参考点)
    let referenceIndex = divide()
    //递归对参考点左边部分排序
    customQuickSort(&list, startIndex: startIndex, EndIndex: referenceIndex - 1)
    //递归对参考点右边部分排序
    customQuickSort(&list, startIndex: referenceIndex + 1, EndIndex: EndIndex)
}

divide内部的代码跟之前完全一样,但是它现在是被声明为一个局部变量,只能在quickSort内部被调用,而且不需要接受参数。这个时候已经不能叫它函数了,而应该叫闭包。闭包简单来讲就是一个带有上下文环境的函数,在这个例子中,divide可以捕获外部函数customQuickSort中的变量。其实换个说法就是在调用它的时候,如果在它自己内部找不到某个变量,它就会到它外部函数中去寻找。闭包是一个引用类型,它持有上下文环境的方式也是通过引用,搞清楚这个可以避免很多错误。

好了,快排有了,但如果有人还想使用随机化快排呢,而且他不想用我提供的获取随机数据的函数,而是想要用自己的,那该怎么办呢?这种情况下,我们稍微改一下customQuickSort,让它额外接收一个可空闭包作为参数,这个闭包用来获取一个随机索引,如果闭包不为空,就在divide中调用闭包,并将获取的随机索引所在的元素与序列的第一个元素交换,这样这个随机元素在接下来的过程中将作为参考点使用。稍微修改一下上面的代码:

func customQuickSort(inout list: [Int], startIndex: Int, EndIndex: Int, randomHandler: ((range: Range<Int>) -> Int)?) {
    let divide: () -> Int = {
        if let getRandom = randomHandler {
            let randomIndex = getRandom(range: startIndex ... EndIndex)
            (list[startIndex], list[randomIndex]) = (list[randomIndex], list[startIndex])
        }
    //剩余代码不变

调用:

//基本快排
customQuickSort(&testList2, startIndex: 0, EndIndex: testList2.count - 1, randomHandler: nil)
//随机化快排,自己传入一个获取随机数的闭包,我这边使用了原先定义好的那个
customQuickSort(&testList2, startIndex: 0, EndIndex: testList2.count - 1) { (range) -> Int in
    return getRandomNumIn(range)
}

这样一来,使用起来就灵活了很多。完整的代码可以看这里


注:文中的EndIndex为笔误,函数参数首字母不应该大写,改为endIndex。github上的代码已更新。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015.10.06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档