快速排序和高阶函数

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


你只要记住,我很快.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上的代码已更新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • 本文有七千字,阅读大约需要占用你10分钟时间。 好吧。。随便写的,我也不知道会花多久看完。因为写的比较烂,而且只是...
    锅与盆阅读 8,016评论 5 36
  • 版本记录 前言 将数据结构和算法比作计算机的基石毫不为过,追求程序的高效是每一个软件工程师的梦想。下面就是我对算法...
    刀客传奇阅读 5,102评论 4 72
  • quicksort可以说是应用最广泛的排序算法之一,它的基本思想是分治法,选择一个pivot(中轴点),将小于pi...
    黎景阳阅读 434评论 0 1
  • 亲爱的老公: 今天爸妈费心的给我买了鸡做了鲜美的鸡汤吃,晚饭的时候还交代我吃完。 我说你们都没怎么吃呢, 妈说:本...
    邹小羊羊阅读 320评论 0 1
  • 很多时候,我们希望python一致都给我们的都是精确的除法结果,而不用在意给除法去进行计算的是什么类型的数字。,那...
    L小橙子阅读 470评论 0 0