天天看点

优化快速排序的几种方法

快速排序

1.基本思想

快速排序采用分治思想。经过一趟排序将待排序序列分割为独立的两部分,其中一部分的所有排序关键字均小于另一部分的所有排序关键字,再按上述方法对序列的两部分分别进行快速排序,整个排序过程可以递归执行,最终达到整个序列有序的目的。

2.算法概述

分解:在待排序数列按照某种特定选取一个元素作为主元。利用主元对序列进行重排,使得序列分成独立的两个部分,其中一部分的所有排序关键字均小于另一部分的所有排序关键字,即序列所有排序关键字小于主元的排序关键字的元素置于主元的左侧,其他元素置于右侧。

解决:通过递归对主元左侧的序列已经右侧的序列进行快速排序来去求解子问题。

合并:因为快速排序是原址操作,所以不需要执行合并操作,同时也节省了部分空间。

3.算法实现(Python)

# coding = utf-8

"""
快速排序的一些优化方式
"""
import random
import time


def quick_sort(lst, first=, last=):
    # 快速排序
    if first >= last:
        return lst
    low = first
    high = last
    pivot = lst[first]  # 选取第一个为主元
    while first < last:
        while first < last and lst[last] >= pivot:
            last -= 
        lst[first] = lst[last]
        while first < last and lst[first] <= pivot:
            first += 
        lst[last] = lst[first]
    lst[last] = pivot
    quick_sort(lst, low, first - )
    quick_sort(lst, first + , high)
           

4. 算法分析

运行时间

快速排序的最坏时间复杂度是Θ(n2),平均情况下的时间复杂度为Θ(nlgn)。快速排序的渐近时间复杂度介于最坏情况和平均情况。

这里分别采用确定的快速排序(即选取序列的第一个元素或最后一个序列为主元)分别针对输入规模>500万的序列进行排序,获得确定的快速排序的运行时间。

输入规模500万的序列排序运行时间

算法 随机序列 重复序列 升序序列 降序序列
确定的快速排序 36084ms 递归深度过大,堆栈溢出 同左 同左
python内置sort方法 5372ms 93ms 184ms 99ms

测试数据分析

在针对输入序列的数据是随机的时,我们设计的快速排序的效率是可以接受的。针对输入序列是有序的时候,虽然因为递归深度过大,导致堆栈溢出(Python的解释器没有针对做优化),无法成功执行完程序,但是我们也可以预料到,每次排序划分长度都是1,也就是说我们需要将序列划分500万次,才能使序列重新有序,其时间复杂度和空间复杂度可想而知。此时的情况为最坏的情况即时间复杂度为Θ(n2),即沦为普通的冒泡排序。在实际情况中,输入序列很有可能是有序的或者是部分有序的,这时我们可以通过在算法中引入随机性,使得算法对所有的输入都能获得较好的期望性能,同时将随机化版本的快速排序被更多的人运用在大数据输入情况下的排序算法。

优化方式1:引入随机性

随机选取主元元素

# coding = utf-8

"""
优化方法1:引入随机性
"""

import random
import time


def quick_sort(lst, first=, last=):
    # 快速排序
    if first >= last:
        return lst
    low = first
    high = last
    pivot_index = random.randint(first, last)  # 随机选取主元位置
    lst[first], lst[pivot_index] = lst[pivot_index], lst[first]  # 将主元置于首位
    pivot = lst[first]
    while first < last:
        while first < last and lst[last] >= pivot:
            last -= 
        lst[first] = lst[last]
        while first < last and lst[first] <= pivot:
            first += 
        lst[last] = lst[first]
    lst[last] = pivot
    quick_sort(lst, low, first - )
    quick_sort(lst, first + , high)
           
输入规模500万的序列排序的运行时间
算法 随机序列 重复序列 升序序列 降序序列
随机化快速排序 45830ms 递归深度过大,堆栈溢出 29147ms 29363ms
python内置sort方法 6027ms 139ms 113ms 122ms

测试数据分析

通过引入随机性,我们较好的解决了序列有序的时候出现的问题,但是仍然存在一个问题那就是如果序列为重复的数据或者数据大量重复的情况,只引入随机化并不能很好的解决问题。这时候我们可以通过将排序关键字与主元相等的元素聚集在一起,下次不再对这些元素进行分割,这不仅可以解决目前的设计的算法在重复序列上存在的问题,也可以一定程度上优化对其他序列排序的效率。

优化方式2:聚集

# coding = utf-8

"""
优化方法1:引入随机性
优化方法2:聚集
"""

import random
import time


def quick_sort(lst, first=, last=):
    # 快速排序
    if first >= last:
        return lst
    low = first
    high = last
    left_lst_len =   # 分割后第一个序列和第二个序列与主元相等元素的长度
    right_lst_len = 
    # 选取随机的主元,并将主元元素与第一个元素置换
    pivot_index = random.randint(first, last)
    lst[first], lst[pivot_index] = lst[pivot_index], lst[first]
    pivot = lst[first]
    while first < last:
        while first < last and lst[last] >= pivot:
            if lst[last] == pivot:
                right_lst_len += 
            last -= 
        lst[first] = lst[last]
        while first < last and lst[first] <= pivot:
            if lst[first] == pivot:
                left_lst_len += 
            first += 
        lst[last] = lst[first]
    lst[last] = pivot
    lst1_last = first -   # 保存分割后第一个序列的最后位置和第二个序列的最开始的位置
    lst2_first = first + 
    flag = low
    while lst1_last - low >  and flag < lst1_last and left_lst_len > :  # 将第一个序列中与主元相等的元素置于主元左侧
        if pivot == lst[flag]:
            while lst[lst1_last] == pivot and left_lst_len >  and lst1_last - low > :
                left_lst_len -= 
                lst1_last -= 
            if lst1_last >  and left_lst_len > :
                lst[flag], lst[lst1_last] = lst[lst1_last], lst[flag]
                left_lst_len -= 
                lst1_last -= 
            else:
                break
        flag += 
    flag = high
    while high - lst2_first >  and flag > lst2_first and right_lst_len > :  # 将第二个序列中与主元相等的元素置于主元右侧
        if pivot == lst[flag]:
            while lst[lst2_first] == pivot and right_lst_len >  and high - lst2_first > :
                right_lst_len -= 
                lst2_first += 
            if lst2_first < high and right_lst_len > :
                lst[flag], lst[lst2_first] = lst[lst2_first], lst[flag]
                right_lst_len -= 
                lst2_first += 
            else:
                break
        flag -= 
    quick_sort(lst, low, lst1_last)
    quick_sort(lst, lst2_first, high)
           
输入规模500万的序列排序的运行时间
算法 随机序列 重复序列 升序序列 降序序列
随机化快速排序+聚集 63856ms 2272ms 38797ms 42183ms
python内置sort方法 7682ms 88ms 95ms 103ms

测试数据分析

通过将排序关键字与主元相等的元素聚集在一起,下次不再对这些元素进行分割,这不仅可以解决目前的设计的算法在重复序列上存在的问题,也可以一定程度上优化对其他序列排序的效率,如果序列的重复度(即重复出现的数据所占比例)越大,聚集所提高的效率越大。

优化方式3:三位数取中

取序列中lst[first]、lst[mid]、lst[last]中的中位数作为主元

# coding = utf-8

"""
优化方法2:聚集
优化方法3:三位数取中
"""

import random
import time


def quick_sort(lst, first=, last=):
    # 快速排序
    if first >= last:
        return lst
    low = first
    high = last
    left_lst_len =   # 分割后第一个序列和第二个序列与主元相等元素的长度
    right_lst_len = 
    select_pivot_median(lst, first, last)  # 选择主元并将置于首位
    pivot = lst[first]
    while first < last:
        while first < last and lst[last] >= pivot:
            if lst[last] == pivot:
                right_lst_len += 
            last -= 
        lst[first] = lst[last]
        while first < last and lst[first] <= pivot:
            if lst[first] == pivot:
                left_lst_len += 
            first += 
        lst[last] = lst[first]
    lst[last] = pivot
    lst1_last = first -   # 保存分割后第一个序列的最后位置和第二个序列的最开始的位置
    lst2_first = first + 
    flag = low
    while lst1_last - low >  and flag < lst1_last and left_lst_len > :  # 将第一个序列中与主元相等的元素置于主元左侧
        if pivot == lst[flag]:
            while lst[lst1_last] == pivot and left_lst_len >  and lst1_last - low > :
                left_lst_len -= 
                lst1_last -= 
            if lst1_last >  and left_lst_len > :
                lst[flag], lst[lst1_last] = lst[lst1_last], lst[flag]
                left_lst_len -= 
                lst1_last -= 
            else:
                break
        flag += 
    flag = high
    while high - lst2_first >  and flag > lst2_first and right_lst_len > :  # 将第二个序列中与主元相等的元素置于主元右侧
        if pivot == lst[flag]:
            while lst[lst2_first] == pivot and right_lst_len >  and high - lst2_first > :
                right_lst_len -= 
                lst2_first += 
            if lst2_first < high and right_lst_len > :
                lst[flag], lst[lst2_first] = lst[lst2_first], lst[flag]
                right_lst_len -= 
                lst2_first += 
            else:
                break
        flag -= 
    quick_sort(lst, low, lst1_last)
    quick_sort(lst, lst2_first, high)


def select_pivot_median(lst, first, last):
    # 将选取的主元即lst[first],lst[mid],lst[last]的中位数置于first坐标位置
    mid = first + ((last - first) >> )  # 计算序列中间元素的下标
    if lst[mid] > lst[last]:
        lst[mid], lst[last] = lst[last], lst[mid]
    if lst[first] > lst[last]:
        lst[first], lst[last] = lst[last], lst[first]
    if lst[mid] > lst[first]:
        lst[mid], lst[first] = lst[first], lst[mid]
           
输入规模500万的序列排序的运行时间
算法 随机序列 重复序列 升序序列 降序序列
三数取中的快速排序+聚集 53375ms 2550ms 30123ms 35552ms
python内置sort方法 5767ms 88ms 126ms 103ms

测试数据分析

通过引入随机性提高序列分割的效率,但是其最坏情况下的时间复杂度仍然是Θ(n2)。最佳情况下的划分是可以将待排序的序列划分为等长的序列,也就是说我们选取的主元是整个序列的中位数,但是如果要求整个序列的中位数那必定会花费更多的时间,有点得不偿失的感觉。我们可以选取折中的办法,选择最左端、中心位置、最右端的元素中的中位数作为组元,能一定程度上快速排序的比较次数。

优化方式4:引入插入排序

插入排序在待排序序列长度一定时效率高于插入排序,我们可以在快速排序分割的子序列长度达到一定时采用插入排序进行排序

# coding = utf-8

"""
优化方法2:聚集
优化方法3:三位取中
优化方法4:插入排序
"""

import random
import time


def quick_sort(lst, first=, last=):
    if last - first +  > :
        insert_sort(lst, first, last)
    else:
        # 快速排序
        if first >= last:
            return lst
        low = first
        high = last
        left_lst_len =   # 分割后第一个序列和第二个序列与主元相等元素的长度
        right_lst_len = 
        select_pivot_median(lst, first, last)  # 选择主元并将置于首位
        pivot = lst[first]
        while first < last:
            while first < last and lst[last] >= pivot:
                if lst[last] == pivot:
                    right_lst_len += 
                last -= 
            lst[first] = lst[last]
            while first < last and lst[first] <= pivot:
                if lst[first] == pivot:
                    left_lst_len += 
                first += 
            lst[last] = lst[first]
        lst[last] = pivot
        lst1_last = first -   # 保存分割后第一个序列的最后位置和第二个序列的最开始的位置
        lst2_first = first + 
        flag = low
        while lst1_last - low >  and flag < lst1_last and left_lst_len > :  # 将第一个序列中与主元相等的元素置于主元左侧
            if pivot == lst[flag]:
                while lst[lst1_last] == pivot and left_lst_len >  and lst1_last - low > :
                    left_lst_len -= 
                    lst1_last -= 
                if lst1_last >  and left_lst_len > :
                    lst[flag], lst[lst1_last] = lst[lst1_last], lst[flag]
                    left_lst_len -= 
                    lst1_last -= 
                else:
                    break
            flag += 
        flag = high
        while high - lst2_first >  and flag > lst2_first and right_lst_len > :  # 将第二个序列中与主元相等的元素置于主元右侧
            if pivot == lst[flag]:
                while lst[lst2_first] == pivot and right_lst_len >  and high - lst2_first > :
                    right_lst_len -= 
                    lst2_first += 
                if lst2_first < high and right_lst_len > :
                    lst[flag], lst[lst2_first] = lst[lst2_first], lst[flag]
                    right_lst_len -= 
                    lst2_first += 
                else:
                    break
            flag -= 
        quick_sort(lst, low, lst1_last)
        quick_sort(lst, lst2_first, high)


def select_pivot_median(lst, first, last):
    # 将选取的主元即lst[first],lst[mid],lst[last]的中位数置于first坐标位置
    mid = first + ((last - first) >> )  # 计算序列中间元素的下标
    if lst[mid] > lst[last]:
        lst[mid], lst[last] = lst[last], lst[mid]
    if lst[first] > lst[last]:
        lst[first], lst[last] = lst[last], lst[first]
    if lst[mid] > lst[first]:
        lst[mid], lst[first] = lst[first], lst[mid]


def insert_sort(lst, first, last):
    # 插入排序
    for index in range(first + , last + ):
        key = lst[index]
        j = index - 
        while j >= first:
            if lst[j] > key:
                lst[j + ] = lst[j]
                lst[j] = key
            j -= 
           
输入规模500万的序列排序的运行时间
算法 随机序列 重复序列 升序序列 降序序列
三数取中的快速排序+聚集+插入排序 40934ms 2549ms 21389ms 34769ms
python内置sort方法 5690ms 88ms 119ms 124ms

测试数据分析

针对长度较小或者部分有序数组时,插入排序的效率优于快速排序,引入插入排序将会使算法整体效率略有提高,而且这个方法适用于大部分序列。

其他优化方式

优化递归操作

我们可以使用尾递归的方式对快速排序的递归操作进行处理,来减少递归层次,避免堆栈溢出的发生,可以解决确定的快速排序无法处理已排序序列的问题

【待完成】。

使用并行算法或者多线程算法处理子序列

【待完成】

总结

快速排序的优化细节没有处理好,导致算法的整体效率没有很大的提升,以后有时间对算法细节进行处理,可以达到更佳的效果。另外输入数据都是随机生成的,导致有的优化甚至比没优化之前排序所花费的时间更多,这除了细节问题,还有数据特征不符合现实生活。现实生活中的数据经常是部分有序的序列,所以如果采用优化后的算法,实际运行情况会比当前测试的情况更佳。

参考资料

http://blog.csdn.net/hacker00011000/article/details/52176100

继续阅读