算法基础--快速排序

本文只是自己的笔记,并不具备过多的指导意义。

为了理解很多都使用了递归,而不是自己通过while进行压栈处理。
代码的初衷是便于理解,网上大神优化过的代码很多,也不建议在项目中copy本文代码。


目录

  • 快速排序的基本思想
  • 单次遍历,确定一个值在数组中的最终位置
    • 将小于等于给定值num的数放在数 组的左边,大于num的数放在数组的右
    • 将数组中某个元素的值作为基准值num,确定其在最终排序数组中的位置
  • 经典快排
    • 在每段小数组上确定最后元素的最终位置
    • 将大数组拆分成小数组
    • 经典快排的动图
  • 经典快排的改进
    • 双路快排
    • 三路快排
  • 基准值对快排时间复杂度的影响
    • 随机快排
    • 将中位数作为基准值

快速排序的基本思想

每次排序,确定一个任意值在数组中的最终位置
具体操作上:
  1. 以数组中某个值作为基准值
  2. 遍历数组,将小于基准值的放在左侧,大于他的放在右侧
  3. 最终,确定该元素在数组中的位置。
对于经典快排
  1. 继续在该元素左侧与右侧重复1,2,3步骤。每次确定一个元素的位置最终确定整个数组的所有元素。

单次遍历,确定一个值在数组中的最终位置。

遍历数组,某个元素作为基准值,将小于基准值的放在左侧,大于他的放在右侧。最终,确定该元素在数组中的位置。

  • 将小于等于给定值num的数放在数 组的左边,大于num的数放在数组的右边
/// 给定一个数组arr,把小于等于给定值num的数放在数 组的左边,大于num的数放在数组的右边。并返回其位置
///
/// - Parameters:
///   - arr: 数组
///   - num: 划分值
/// - Returns: 最终位置
func partition0(arr: inout [Int] ,num:Int) -> Int {
    if arr.count<2 {
        return 0
    }
    var p = 0-1  //小于等于区域结束位置。
    
    for i in 0..<arr.count { //遍历整个数组
        if arr[i]<=num { //如果小于给定的num,则扩大小于等于区域,并将其交换进该区域末尾
            p=p+1
            arr.swapAt(p, i)
        }
    }
    
    //最终,p左侧为小于等于区域,右侧为大于区域
    return p
}

需要注意小于等于区域的初始值为-1,因为最初并没有任何元素被确定小于num。

  • 将数组中某个元素的值作为基准值num,确定其在最终排序数组中的位置

既然其左侧必然小于等于他,右侧必然大于他。那么他的位置一定不变。

那么,我们只需要对上述方法进行一些小改动。比如将数组中最后一位的值作为基准,这样每次就能确定最后一位的最终位置。

/// 给定一个数组arr,把小于等于末尾值num的数放在数组的左边,大于num的数放在数组的右边。并返回其位置
///
/// - Parameters:
///   - arr: 数组
/// - Returns: 最终位置
func partition(arr: inout [Int]) -> Int {
    if arr.count<2 {
        return 0
    }
    let num = arr[arr.count-1]
    var p = 0-1  //小于等于区域结束位置。
    for i in 0..<arr.count { //遍历整个数组
        if arr[i]<=num { //如果小于给定的num,则扩大小于等于区域,并将其交换进该区域末尾
            p=p+1
            arr.swapAt(p, i)
        }
    }
    
    //最终,p左侧为小于等于区域,右侧为大于区域
    return p
}

let num = arr[arr.count-1]所做的,就是上述将数组中最后一位的值作为基准的操作。

对于单次遍历,可以完成下面的结果



经典快排

  • 在每段小数组上确定最后元素的最终位置

很简单,值需要将上面的方法添加left,right参数。在大数组中确定小数组的左右边界即可。

/// 在一个数组的left,right范围内。确定最后一个元素的最终位置
///
/// - Parameters:
///   - arr: 数组
///   - left: 左边界
///   - right: 右边界
/// - Returns: 基准元素的最终位置
func partition(arr:inout [Int] ,left:Int ,right:Int) ->Int {

    var l = left - 1  //小于等于区域末端位置
    var p = left  //遍历的起始位置,从最左端开始
    while p < right { //保证不越界,并且遍历的范围不包含右侧边界位置。
        if arr[p] <= arr[right] {
            l+=1 //满足小于等于,扩大小于等于区域
            arr.swapAt(l, p) //并将其交换进该区域末尾
        }
        p+=1
    }

    arr.swapAt(right, l+1) //最后,将右侧边界位置与大于区域首位置(p+1)交换
    return l+1; //返回最后一个值的最终位置
}
  • 将大数组拆分成小数组

partition划分时找出的最终位置作为再次划分成左侧与右侧,要求两个小数组继续进行处理。

/// 快速排序
///
/// - Parameter arr: 数组
func quickSort(arr:inout [Int]) {
    quickSortProcess(arr: &arr, left: 0, right: arr.count-1)
}


/// 快速排序递归方法
///
/// - Parameters:
///   - arr: 大数组
///   - left: 左边界
///   - right: 右边界
func quickSortProcess (arr:inout [Int] ,left:Int ,right:Int) {

    if left<right { //如果右侧小于左侧,说明数组只有一个元素
        let p = partition(arr: &arr, left: left, right: right)
        quickSortProcess(arr: &arr, left: left, right: p-1)
        quickSortProcess(arr: &arr, left: p+1, right: right)
    }
}
需要注意的时再次划分时,左侧(left~p-1)与右侧(p+1~right)已经将p位置排除在外了,因为其的位置已经确定。
  • 经典快排的动图

每次划分都只确定最后一个元素的最终位置,重复进行直到整个数组有序。


经典快排的改进

  • 双路快排

不管是当条件是大于等于还是小于等于v,当数组中重复元素非常多的时候,等于v的元素太多,那么就将数组分成了极度不平衡的两个部分,因为等于v的部分总是集中在数组的某一边,导致分割不均。
双路快排当遇到重复元素的时候,也能近乎将他们平分开来。

简而言之是前后两个指针:
指针i,表示小于等于基准的区域。
指针j,表示大于等于基准的区域。

遍历暂停的时机:
当i遇到大于等于基准的值时暂停,j遇到小于等于基准的时暂停。


此时交换arr[i]与arr[j],这是双路快排最核心的思想。
  1. 若交换位置不处于连续重复元素区间。正好,将正确的元素放到了正确的位置。
  2. 若交换一段正好处于连续重复元素区间
    交换后另一端被交换后还会继续遍历,直到下一个暂停的时机,此时原本连续的重复元素之间将会穿插很多其他的值。

举个例子:
有一个绿色的蓝色的4和一个绿色的4,与基准值相同


此时交换橙色位置的4,以及黄色位置的2。
然后橙色指针继续向右移动,被卡在7的位置。
黄色指针也继续向左移动,被卡在绿色4的位置。


继续交换...


绿色的4和蓝色的4已经被分散到数组两端。

这样便保证不会出现由于经典快排中<=的边界导致数组划分不均的情况了。
  • 三路快排

经典快排值划分的小于等于,大于区域,中间用一个基准值进行区分。
类似荷兰国旗问题,我们可以将基准值以及与其相等的值划分成一整个区域。
如此。每次将不止再确定一个值,而是几个值的位置了

具体操作上:

改进的地方在于,右侧新增了一个指针,指向大于基准值区域的首位。
最终生成一个小于区域,大于区域,剩下的差值便是等于区域。

/// 快速排序
///
/// - Parameter arr: 数组
func quickSort(arr:inout [Int]) {
    quickSortProcess(arr: &arr, left: 0, right: arr.count-1)
}

func quickSortProcess (arr:inout [Int] ,left:Int ,right:Int) {

    if left<right {
        let p = partition(arr: &arr, left: left, right: right)
        quickSortProcess(arr: &arr, left: left, right: p[0]-1) //右边界为小于区域首位再向前一个
        quickSortProcess(arr: &arr, left: p[1]+1, right: right)//左边界为大于区域首位再向后一个
    }
}

/// 三路快排
///
/// - Parameters:
///   - arr: 数组
///   - left: 左边界
///   - right: 右边界
/// - Returns: 数组类型,[等于区域左边界,等于区域右边界]
func partition(arr:inout [Int] ,left:Int ,right:Int) ->[Int] {

    var l = left - 1//小于区域,默认不在范围内(上次的基准值)
    var r = right + 1//大于区域,默认不在范围内(上次的基准值)
    var p = left//遍历指针首位置
    let num = arr[right]//目标值

    while p < r { //注意这里的r不是右边界right,p==r则已经遍历到大于区域了
        if arr[p] < num { //小于目标值,将小于区域扩大并将该值交换进小于区域。
            l+=1
            arr.swapAt(p, l)
            p+=1
        }else if arr[p] == num {//等于目标值,不动,继续向下遍历
            p+=1
        }else if arr[p] > num {//大于目标值,将大于区域扩大并将该值交换进大于区域。
            r-=1
            arr.swapAt(p, r)
            //需要注意这里遍历指针p没有继续右移,因为当前p位置已经交换成了待定区域的某个值。需要再次判定
        }
    }

    //此时l为小于区域末尾,r为大于区域首部
    //等于区域位于小于区域之后一位,到大于区域之前一位
    return [l+1,r-1]
}

基准值对快排时间复杂度的影响

快速排序法事应用最广泛的排序算法之一,最佳情况下时间复杂度是 O(nlogn)。但是最坏情况下(数组本身已经有序的情况下),每次基准值都会处于数组边界处,时间复杂度将劣化到O(n^2)。

  • 随机快排

将基准值位置通过随机数的方式获取,将复杂度的表达式转化为概率表达式。最终的表达式也会趋近于O(nlogn)。

这种方式与经典快排在随机数组的情况下相差无几,甚至由于获取随机数的成本速度略低于经典快排。但在出现有序数组的情况下,速度远优于经典快排。
《快速排序与随机化快排运行速度实验比较》

随机快排应该是目前最流行的快速排序。

  • 将中位数作为基准值

来自《快速排序 改进快排的方法》
这种能将快排的时间复杂度确定在O(n^2),但是取中位数的过程究竟有多大的影响我也不确定。(目前我只会用堆来求,不妄加评论)。


参考资料

左神牛课网算法课
双路快速排序法
经典排序之快排及其优化
快速排序与随机化快排运行速度实验比较
快速排序 改进快排的方法
十大经典排序算法(动图演示)

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