快速排序及其优化超详细解答+代码(真正理解)

原文出处 https://zhuanlan.zhihu.com/p/384708371

快速排序QuickSort采用了分治法Divide-and-ConquerMethod,通过将数组链表或其他元素集分为待排序集合和已排序集合,并在一次次迭代中将待排序集合的元素转化到已排序集合当中直到全部元素都为已排序则完成排序。

快速排序利用这一策略,节约了处理已排序元素的成本。算法只关注剩余待排序的元素,其中位置连续的未排序元素子串又分为:S1(左侧),pivot(交换枢纽元),S2(右侧)

以快速排序来实现升序排序为例:

  1. 先从数组中选取出一个数组作为枢轴元pivot。(选取枢纽元的策略很关键)

  2. 将待排序集合所有小于枢轴元pivot的元素移至s1(左侧),所有大于枢轴元pivot的元素移至s2(右侧):(双指针中的相向指针法)

  3. 先将枢轴元pivot(准有序部分)放到最左或者最右区分出待排序部分。

  4. 将i, j分别指向待排序部分索引的最左和最右。

  5. 如果i索引指向的元素小于枢纽元,则i++;否则,i停止。j索引指向的元素大于枢纽元,j--;否则,j停止。

  6. 如果i<j,则交换两个元素,继续循环3,4步骤;否则跳出循环,将i对应的元素与枢纽元pivot交换(这时候完成了分割)

  7. 这个时候定义枢轴元pivot为已排序部分,对剩余未排序部分S1(左侧)S2(右侧)继续循环步骤1,2,3直到所有元素都已排序。

最原始的快速排序

最原始的快速排序采用最简单粗暴的枢纽元选取策略,即选取第一个最后一个元素为pivot。

该方案的平均时间复杂度为O(nlogn),当遇到数组刚好有序的情况下会出现最坏时间复杂度O(n^2),因为当输入序列本身有序时,会导致S1或S2集合为空,除枢纽元外所有元素都在S1或S2,此时遍历交换的时间效率大幅下降,因为浪费了另一个区间的作用。

Python代码

nums = [23,2,4,6,2,5,1,6,13,54,8]

def quicksort(nums, left, right):   # left为最左索引,righ为最右索引
    if left >= right:
        return
    pivot = left //取第一个元素为pivot
    i, j = left, right
    while i < j:
        while nums[pivot] <= nums[j] and i < j:
            j -= 1
        while nums[pivot] >= nums[i] and i < j:
            i += 1
        if i < j:
            nums[i], nums[j] = nums[j], nums[i]
    nums[pivot], nums[i] = nums[i], nums[pivot]
    quicksort(nums, left, i-1)
    quicksort(nums, i+1, right)

def quicksort2(nums, left, right):          # 用栈代替递归
    if left >= right:
        return
    stack = []
    while stack or left < right:
        if left < right:
            pivot = left
            i, j = left, right
            while i < j:
                while nums[pivot] <= nums[j] and i < j:
                    j -= 1
                while nums[pivot] >= nums[i] and i < j:
                    i += 1
                if i < j:
                    nums[i], nums[j] = nums[j], nums[i]
            nums[pivot], nums[i] = nums[i], nums[pivot]
            stack.append((left, i, right))
            right = i - 1
        else:
            left, mid, right = stack.pop()
            left = mid + 1

quicksort2(nums, 0, len(nums)-1)
print(nums) 

由于堆栈存储(递归等价于栈代替),快速排序空间复杂度O(logn)

所以S1和S2约平衡,快速排序的效率越高,即中值(中位数)枢纽元最好的选择(因为可以将序列均分为两个子序列,归并排序告诉我们,这时候是O(NlogN)

但要计算一组数组的中位数就比较耗时,会减慢快排的效率。

小问题:为什么当轴元pivot放入左侧时, 必须先平移右指针j,而不能先平移左指针i ?

答案:为了避免平移交换过程中, pivot自己被交换掉。

快速排序的优化

三数中值快速排序(三数中值法选取枢轴元pivot

虽然计算一组数组的中位数就比较耗时,会减慢快排的效率。但可以通过计算数组的第一个left中间位置(right-left)/2(向下或向上取整),最后一个right元素的中值来代替。

Python代码

def quicksort_opt1(nums, left, right): 
    if left >= right:
        return
    stack = []
    while stack or left < right:
        if left < right:
            l, m, r = left, (right-left)//2, right # 第一个, 中间位置, 最后一个
            pivot = findmedian(l, m, r) # 三数取中值法选取枢轴元pivot
            i, j = left, right
            while i < j:
                while nums[pivot] <= nums[j] and i < j:
                    j -= 1
                while nums[pivot] >= nums[i] and i < j:
                    i += 1
                if i < j:
                    nums[i], nums[j] = nums[j], nums[i]
            nums[pivot], nums[i] = nums[i], nums[pivot]
            stack.append((left, i, right))
            right = i - 1
        else:
            left, mid, right = stack.pop()
            left = mid + 1

def findmedian(l, m, r):
    if nums[l] <= nums[m]:
        if nums[m] <= nums[r]:
            return m
        else:
            if nums[l] >= nums[r]:
                return l
            else:
                return r
    else:
        if nums[m] >= nums[r]:
            return m
        else:
            if nums[l] >= nums[r]:
                return r
            else:
                return l

三数中值法来选取枢轴元pivot一定程度上避免了最坏的情况发生。

但是即使这样,我们的快速排序算法仍然有可能出现最坏的情况:时间复杂度O(n^2)

因为重复的元素越多会给算法带来越多的无用功,会涉及以下一个问题:

简单说是遇到与枢纽元相等的元素时,左右索引指针的平移需要停止吗?

  1. 如果只有一个停止:这将导致所有等于枢纽元pivot的元素都移动到同一侧(S1或S2),在极端情况下所有元素都是重复,会产生最坏情况O(n^2)
  2. 如果都不停止:在极端情况下所有元素都是重复,整个过程枢纽元pivot相当于对整个数组进行了一次遍历,时间复杂度是(n + n-1 +...+2+1)=(1/2)(1+n)n, 即时间复杂度O(n^2),其实推演一下会发现基于快速排序的执行规则这种情况跟情况1是等价*的,都不停止相当于始终只有一个指针在平移,而且是一直平移到末尾才停止。
  3. 如果都停止:在极端情况下所有元素都是重复,虽然看似会进行很多次“无意义”的交换,但由于每次双指针相遇的地点都是数组的中点,这个时候恰好将序列分为两个均等分配的子序列,还是归并排序的原理,达到分治法效率最大化,以此类推会让枢纽元pivotlogn的速度走完整个数组。所以这种方法最坏情况的时间复杂度O(nlogn)

重复元素处理思路:三向切分快速排序

三向切分快速排序推荐简书上这个博客

快速排序(Quick Sort)www.jianshu.com

双路快速排序

图解快速排序及双路三路快速排序 - SegmentFault 思否segmentfault.com

双路快速排序把待排序区域用指针 i , j 分为三个子区域:

(<nums[pivot]),未知,(>nums[pivot])

Python代码

def quicksort_opt2(nums, left, right): 
    if left >= right:
        return
    stack = []
    while stack or left < right:
        if left < right:
            l, m, r = left, (right-left)//2, right # 第一个, 中间位置, 最后一个
            pivot = findmedian(l, m, r) # 三数取中值法选取枢轴元pivot
            nums[pivot], nums[left] = nums[left], nums[pivot]
            pivot = left
            i, j = left+1, right
            while i <= j:   # 为什么不能是 i < j?
                # 此处先平移j和先平移i对结果没有影响,因为 i=left+1避开了pivot
                while nums[i] < nums[pivot] and i <= j:
                    #不能改为nums[i] <= nums[pivot], 因为这种方式将连续出现的这些值归为其中一方, 使得两棵树不平衡
                    i += 1
                while nums[j] > nums[pivot] and i <= j: 
                    j -= 1
                if i <= j:
                    nums[i], nums[j] = nums[j], nums[i]
                    j -= 1
                    i += 1
            nums[pivot], nums[j] = nums[j], nums[pivot] 
            #因为pivot放置在左边, 所以不可与左侧指针i交换---->会造成无限循环
            stack.append((left, j, right))  
            right = j - 1
        else:
            left, mid, right = stack.pop()
            left = mid + 1

nums = [23,2,4,6,2,5,1,6,13,54,8]
quicksort_opt2(nums, 0, len(nums)-1)
print(nums) 

问题1:为什么此处不能用 i < j 代替 i <= j 当作循环条件?

问题2:为什么平移结束后, pivot要跟 j 交换 而不是 i ?为什么跟 i 交换会造成无限循环?

那么在此基础上,我们能否再次优化,将前文提到的重复元素'无意义'的交换也省了? 当然可以,下面的三路快速排序就是围绕这点进行优化。

三路快速排序

在双路快排把未排序区域分为三个子区域的基础上,三路快排多分了一个子区域用来存放重复且等于pivot的元素,来识别并跳过他们。

Python代码

def quicksort_opt3(nums, left, right): 
    if left >= right:
        return
    stack = []
    while stack or left < right:
        if left < right:
            l, m, r = left, (right-left)//2, right
            pivot = findmedian(l, m, r)
            nums[pivot], nums[left] = nums[left], nums[pivot]
            pivot = left
            lt, gt = left, right + 1
            i = left + 1
            while i < gt:
                if nums[i] < nums[pivot]:
                    nums[i], nums[lt+1] = nums[lt+1], nums[i]
                    i += 1
                    lt += 1
                elif nums[i] > nums[pivot]:
                    nums[i], nums[gt-1] = nums[gt-1], nums[i] 
                    gt -= 1
                else:# nums[i] == nums[pivot]
                    i += 1
            nums[pivot], nums[lt] = nums[lt], nums[pivot] 
            stack.append((left, lt, right))  
            right = lt - 1
        else:
            left, mid, right = stack.pop()
            left = mid + 1

nums = [23,2,4,6,2,5,1,6,13,54,8]
quicksort_opt3(nums, 0, len(nums)-1)
print(nums) 

快速排序复杂度分析

快速排序的平均时间复杂度为O(nlogn)

快速排序平均复杂度数学证明可参考此回答:

如何证明快速排序法的平均复杂度为 O(nlogn)?www.zhihu.com

快速排序的短板:

即使是三路快速排序也做不到堆排序那样保证最坏时间复杂度为O(nlogn)

**快速排序是不稳定排序 **(所谓稳定就是当待排数组中存在重复元素的时候,排序后重复元素的相对顺序不会改变)

当待排序序列元素数量很小(N<=20)的时候,快速排序不如插入排序快,并且插入排序是稳定排序。

github代码出处: https://github.com/stevezkw1998/common-algorithm-demos/blob/master/quicksort.py

欢迎关注我的github:stevezkw1998 - Overview

知乎原文:## 快速排序及其优化超详细解答+代码(真正理解)

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

推荐阅读更多精彩内容

  • 本文转载至:https://blog.csdn.net/insistGoGo/article/details/77...
    随时学丫阅读 2,186评论 0 5
  • 1、快速排序的基本思想: 快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另...
    lesline阅读 775评论 0 0
  • 稳定性:排序算法的稳定性通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相...
    生产八哥阅读 322评论 0 0
  • 版权声明:本文源自简书tianma,转载请务必注明出处:http://www.jianshu.com/p/df8a...
    tianma阅读 4,370评论 0 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,643评论 2 7