快速排序和归并排序都是基于“分而治之”思想的排序方法,较传统的插入排序、冒泡排序的计算量更小,二者虽然师出同门,但是在细节上又有些许不同。下面将比较一下二者的异同与优劣:

归并排序(MergeSort)包括两部分:分组与归并,例如对下面的数列 a[n]=[3,5,4,2,8,6,7,1] 进行排序(升序)

               3,5,4,2,8,6,7,1 (下面逗号表示不同的组)  

分组:将数组依次进行二等分(n/2与n/2 或者 n/2与n/2+1)

    3 5 4 2         8 6 7 1 

 3 5 ,4 2     8 6, 7 1 

  3,5,4, 2,    8,6, 7, 1  

归并:将上一层的两个子数组按顺序合并为一个小组(比较两个子数组的首元素,取小的放入合并后的数组)

  3  5,   2   4,           6  8,   1   7 

      2 3 4 5,          1 6 7  8

    1 2 3 4 5 6 7 8 

由以上过程可见,数组进行了log2(n)次二等分,排序过程每一层要进行 (n-1)次比较,因此算法复杂度为 nlog2(n)

a=[4,1,8,3,7,2,6,10,0,12,-1]

def mergesort(a):
    n=len(a)
    if n==1:return a #注意这n=1时的情况,表示递归到底(并非到n=2)
    middle=int(len(a)/2)  #注意这个middle,可以有适合长度奇数偶数的情况
    aLeft=mergesort(a[:middle]) #注意这种写法 from start
    aRight=mergesort(a[middle:]) #注意这种写法  to end
    aAlign=merge_gai(aLeft,aRight)
    return aAlign

def merge_gai(aLeft,aRight):
    n=len(aLeft)+len(aRight) #总共的长度
    aAligh=[]
    posLeft=0;pRight=0
    for k in range(n):
        if aLeft[posLeft]<aRight[pRight]:
            aAligh.append(aLeft[posLeft])
            posLeft+=1
            if posLeft==len(aLeft):
                aAligh.extend(aRight[pRight:]);break   #用extend a=[1,2],b=[3,4], a.extend(b)=[1,2,3,4],len(a)=4;a.append(b)=[1,2,[3,4]]  ,len(a)=3    
        else:
            aAligh.append(aRight[pRight])
            pRight+=1
            if pRight==len(aRight):
                aAligh.extend(aLeft[posLeft:]);break  
    return aAligh

if __name__=='__main__':
    aSorted=mergesort(a)      
    
View Code

 

 快速排序(QuickSort)整体上也可以分为 分组 和 排序 两部分,但没有归并排序那么规则,依然以上面的数组为例:

3,5,4,2,8,6,7,1 (以a[0]为基准,将数组分为两部分:比a[0]小的元素 + a[0]+比a[0]小的元素  )

1 2, 3, 5 8 6 7 4(对 比a[0]小的元素构成的子数组  与 比a[0]小的元素构成的子数组 进行上一步操作 )

1 2, 3    4, 5, 6 7 8

由以上过程可见,统计意义上 数组平均需要进行了log2(n)次划分,每次划分后需要进行,(n-1)次比较,因此算法复杂度为 nlog2(n)

# -*- coding: utf-8 -*-
"""
Created on Tue Dec 25 21:10:40 2018

@author: DavidLee
"""

a=[3,5,2,4,6,-1,8,7,1,9]

def qucikSort(a,l,r):
    if(l>=r):return
    m=l
    for k in range(l+1,r): #[l+1,r)
        if a[k]<a[l]:
            m=m+1;swap(a,m,k)
            
    swap(a,l,m)
    qucikSort(a,l,m)   #[l+1,m)         
    qucikSort(a,m+1,r) #[m+1,r),i.e., 'm' is not included       


def swap(a,p1,p2):
    t=a[p1]
    a[p1]=a[p2]
    a[p2]=t

if __name__=='__main__':
    qucikSort(a,0,len(a))    
View Code

如上面加粗的字体所示,与归并排序固定的log2(n)次二等分相比,快速排序的分组次数与输入数组的情况是相关的,如对  a[n]=[3,5,4,2,8,6,7,1] ,归并排序进行了3次等分,而快速排序只进行了2次划分,因此对于a[n]快速排序的速度更快一些。而如果对于数组b[n]=[1,2,3,4,5,6,7,8]进行排序呢?其快速排序过程如下

1 2 3 4 5 6 7 8

1,2 3 4 5 6 7 8 

1, 2 ,3 4 5 6 7 8

1, 2 ,3, 4 5 6 7 8

1, 2 ,3, 4 ,5 6 7 8

1, 2 ,3, 4 ,5, 6 7 8

1, 2 ,3, 4 ,5, 6, 7 8 

1, 2 ,3, 4 ,5, 6, 7, 8

可见每次对于长度为 M的数组进行划分,其每次只能划分出分组元素(即c[0]+比c[0]大的元素,比c[0]小的元素缺失 )

因此,对于该有序序列的算法复杂度为O(n2)。为了消除快速排序在面对有序序列时算法复杂度的提升,可以在单层划分时做些改动,比如其中引入一些随机性(e.g., 随机选择每层划分的参考元素)以及改变比较方向(e.g.,单向变双向,参见《算法珠玑》第11章)。

 

既然快速排序的稳定度并不稳定,那么为什么快速排序却这么受欢迎,成为广受欢迎的基础算法(二十世纪十大基础算法之一)呢?首先,在实际应用中,需要面对的是及其多次的排序处理,如同可以《概率论》中可以通过对事件的发生的频率统计时间发生的概率,大量的处理次数使得我们可以在统计意义上度量一个算法的复杂度,而在这点上快速排序和归并排序的复杂度是相同的;第二,如前所述,其不稳定的算法复杂度可以通过诸如引入随机性的方法解决;第三,快速排序在对子序列进行排序时是通过在原子序列通过元素间位置的交换实现的(与冒泡排序同属于交换排序),因此每次操作只需记录子序列的起始位置(i.e.,代码中的l和r);而并归排序在并归过程中需要重新开辟一个与合并后数组相同大小的空间,因此其空间复杂度为O(n),n较大时快速排序空间复杂度小的优势就变得明显了(可参考图1)。

 

      图1.  归并排序和快速排序单层有序化过程示意图

参考:

1.  《算法珠玑》第11章

2. 我最喜欢的排序算法——快速排序和归并排序  https://my.oschina.net/mjRao/blog/50669

 

 

 

 

 

 

 

 

 

  

 

posted on 2018-12-30 11:35  DavidLee爱学习  阅读(362)  评论(0)    收藏  举报