算法导论——python实践(7.快速排序)

1、快速排序的描述

快速排序是原址排序,不用新增某一序列用于存储在排序过程中的临时变量。原址排序就是在原来的数组上进行操作。

主要分为两步

分解:对于一个数组A[p.....r]排序,将其划分为两个子数组A[p.....q-1]和A[q+1.....r],使得A[p.....q-1]中的每一个元素都小于等于A[q],而A[q]也小于等于A[q+1.....r]中的每一个元素。此时我们就能将A[q]在正确排序后的正确下标索引解出来。对应下面程序的partition函数。

解决:通过递归调用快速排序,因为上一步已经把A[q]的正确位置找出,并且左边小于等于A[q],右边严格大于A[q],只要分别对左右两边再调用快速排序算法即可。

def partition(a,p,r):
    x=a[r]
    i=p-1
    for j in range(p,r):
        if a[j]<=x:
            i+=1
            a[i],a[j]=a[j],a[i]
    a[i+1],a[r]=a[r],a[i+1]
    return i+1
def quicksort(a,p,r):
    if p<r:
        q=partition(a,p,r)
        quicksort(a,p,q-1)
        quicksort(a,q+1,r)
quicksort(a,0,len(a)-1)

关于partition函数:其主要作用将数组分成两部分,并找到A[r]的正确位置,并且将数组分为三个区域(1)若p<=k<=i,a[k]<=x.(2)若i+1<=k<=j-1,a[k]>x.(3)若k==r,a[k]=x.

2、快速排序的随机化版本

import random
def partition(a,p,r):
    x=a[r]
    i=p-1
    for j in range(p,r):
        if a[j]<=x:
            i+=1
            a[i],a[j]=a[j],a[i]
    a[i+1],a[r]=a[r],a[i+1]
    return i+1
def randomized_partition(a,p,r):
    i=random.randint(p,r)
    a[i],a[r]=a[r],a[i]
    return partition(a,p,r)
def randomized_quicksort(a,p,r):
    if p<r:
        q=randomized_partition(a,p,r)
        randomized_quicksort(a,p,q-1)
        randomized_quicksort(a,q+1,r)

randomized_quicksort(a,0,len(a)-1)

为什么要引入随机化:由于可能初始的序列存在最坏情况的可能性,通过在算法中引入随机性,从而使得算法对于所有的输入都能获得较好的期望性能。

3、快速排序分析

最坏情况分析:运行时间\Theta (n^{2})

最好情况分析:运行时间\Omega (nlgn)

期望运行时间:\Omega (nlgn)

4、部分习题

7.4-5 当输入数据几乎有序时,插入排序速度很快。请利用插入排序改善快速排序算法。

对一个长度小于K的子数组调用快速排序时,让他不做任何排序就返回,当上层的快速排序调用返回后,对整个数组运行插入排序完成排序过程。比较了此种算法和快速排序算法的运行时间上的区别,在输入少量数据的情况下区别不大,快速排序甚至更快。

C语言利用插入排序改善快排

扫描二维码关注公众号,回复: 3399676 查看本文章
def partition(a,p,r):
    x=a[r]
    i=p-1
    for j in range(p,r):
        if a[j]<=x:
            i+=1
            a[i],a[j]=a[j],a[i]
    a[i+1],a[r]=a[r],a[i+1]
    return i+1
def quicksort(a,p,r):
    if r-p>=3:               #长度小于k的子数组不排序,此时k=3
        q=partition(a,p,r)
        quicksort(a,p,q-1)
        quicksort(a,q+1,r)
def insert_sort(a,p,r):
    for j in range(p+1,r+1):
        key=a[j]
        i=j-1
        while(i>=0 and a[i]>key):
            a[i+1]=a[i]
            i=i-1
        a[i+1]=key
starttime=time.clock()
quicksort(a,0,len(a)-1)
insert_sort(a,0,len(a)-1)
endtime=time.clock()
print((endtime-starttime))

思考题7-1(Hoare划分的正确性)

a.假定这里是p=0,r=lan(a)-1=11,可以看到返回的j的值为8,对应数组中的11,恰是以主元13为分界将数组划分的下标值。

0 1 2 3 4 5 6 7 8 9 10 11 i j
13 19 9 5 12 8 7 4 11 2 6 21 -1 12
6 19 9 5 12 8 7 4 11 2 13 21 0 10
6 2 9 5 12 8 7 4 11 19 13 21 1 9
6 2 9 5 12 8 7 4 11 19 13 21 9 8

b.对伪代码中第2~3行,确定i,j初始值i=p-1,j=r+1;循环开始repeat语句必先执行再判断until语句,循环后的最终结果p<=i<j<=r(由于j递减,访问最多只能访问到a[r]),i为递增,最小只能访问到a[p]),由于第11行if语句的限制。一个更好理解的方式是,假设j取到p(最小值),意味着此时a[j]=x,且a[p]到a[r]中必定至少有一个元素>=x,不然j不会取到最小值p,此时第一个repeat语句执行完,进入第二个repeat语句,i必定能在p~r之间取到一个值。所以i,j下标可以使得不会访问p~r之外的数组元素。

c.b中已经解释过。在i取到值后,由于If语句,会直接return,退出partition函数。为什么不能等于r:假设j=r,i第一次循环必定取到p,p<r,所以i<j,交换元素之后继续执行while循环,j必定再递减。

d.第一个repeat的作用是找到数组右半部分小于主元的位置j.第二个repeat的作用是找到数组左半部分大于主元的位置i
然后位置j的元素A[j]和位置i的元素A[i]互换,这样循环数次后,左半部分都小于主元,右半部分都大于主元。

c.repeat until语句相当于是do...while,用python实现可以用如下语句实现。

# coding:utf-8
def hoare_partition(a,p,r):
    x=a[p]
    i=p-1
    j=r+1
    while True:
        while True:
            j-=1
            if a[j]<=x:
                break
        while True:
            i+=1
            if a[i]>=x:
                break
        if i<j:
            a[i],a[j]=a[j],a[i]
        else:
            return j
def hoare_quicksort(a,p,r):
    if p<r:
        q=hoare_partition(a,p,r)
        hoare_quicksort(a,p,q)
        hoare_quicksort(a,q+1,r)
a=[2,31,45,3,234,453,23,623,232,14]
hoare_quicksort(a,0,len(a)-1)
print (a)
#输出结果[2, 3, 14, 23, 31, 45, 232, 234, 453, 623]

思考题7-2 (针对相同元素值的快速排序)

问题描述:在随机化快速排序中假设元素值都是互异的,如果出现较多相同元素值怎么修改算法?

a.如果数值相同,每次划分的结果都是最坏情况,参照7.2最坏情况分析,运行时间为\Theta (n^{^{2}})

b.具体的思路在包含在注释中了。

def partition1(a,p,r):
    x=a[r]
    i=p-1
    for j in range(p,r):
        if a[j]<=x:
            i+=1
            a[i],a[j]=a[j],a[i]
    a[i+1],a[r]=a[r],a[i+1]#此时i+1=t,i+1的值为x。p~i:<=x,i+1:x,i+2~r:>x
    q=i+1  #一开始用的q=i,出错了
    for k in range(p,i+2):#对p~i+1(<=x)部分进行操作
        if a[k]==x and q>k and a[q]!=x:#如果a[k]等于x,且a[q]!=x,直接交换
            a[k],a[q]=a[q],a[k]
            q-=1
        elif a[k]==x and q>k and a[q]==x:#如果a[k]等于x,且a[q]=x,q往左边退一格直到!=x时再交换
            while True:
                q-=1
                if a[q]!=x and q>k:  #这几个循环都要去q>k来控制,避免重复比较。
                    a[k],a[q]=a[q],a[k]
                    break
                elif q<=k:
                    break
        elif q<=k:
            break
    return q,i+1
a=[1,2,3,4,3,1,3,4,5,6,7,5,7,543,3,2,3,4,1,3,14,5,5,6,4]
print(partition1(a,0,len(a)-1))
print (a)
#输出结果(11, 14)
#[1, 2, 3, 3, 3, 1, 3, 1, 3, 2, 3, 4, 4, 4, 4, 6, 7, 5, 7, 543, 14, 5, 5, 6, 5]

关于此题因为答案上的算法更为简单一点,详情请见下面的伪代码,我用python实现了一下伪代码,发现结果并不是期望的那样。这里暂时就不讨论这种算法了。

c.

a=[1,2,3,4,3,1024,3,14,5,6,7,5,54,543,3,2,3,8,1,1024,14,5,5,6,14]
def randomized_partition1(a,p,r):
    i=random.randint(p,r)
    a[i],a[r]=a[r],a[i]
    return partition1(a,p,r)
def randomized_quicksort1(a,p,r):
    if p<r:
        (q,t)=randomized_partition1(a,p,r)
        randomized_quicksort1(a,p,q-1)
       # randomized_quicksort1(a,q,t)
        randomized_quicksort1(a,t+1,r)
randomized_quicksort1(a,0,len(a)-1)
print(a)
#输出结果:
#[1, 1, 2, 2, 3, 3, 3, 3, 3, 4, 5, 5, 5, 5, 6, 6, 7, 8, 14, 14, 14, 54, 543, 1024, 1024]

7.4 快速排序的栈深度

7. 1节中的QUICKSORT算法包含了两个对其自身的递归调用。在调用PARTITION后,QUICKSORT分别递归调用了左边的子数组和右边的子数组。QUICKSORT中的第二个递归调用并不是必须的。我们可以用一个循环控制结构来代替它。这一技术称为尾递归,好的编译器都提供这一功能。考虑下面这个版本的快速排序,它模拟了尾递归情况。

#尾递归代替两次递归调用
def partition(a,p,r):
    x=a[r]
    i=p-1
    for j in range(p,r):
        if a[j]<=x:
            i+=1
            a[i],a[j]=a[j],a[i]
    a[i+1],a[r]=a[r],a[i+1]
    return i+1
def tail_recursive_quicksort(a,p,r):
    while p<r:
        q=partition(a,p,r)
        tail_recursive_quicksort(a,p,q-1)
        p=q+1

a证明:TAIL-RECURSIVE-QUICKSORT(a,  0, len(a)-1)能正确地对数组a进行排序。编译器通常使用栈来存储递归执行过程中的相关信息,包括每一次递归调用的参数等。  最新调用的信息存在栈的顶部,而第一次调用的信息存在栈的底部。当一个过程被调用时,其相关信息被压入栈中;当它结束时,其信息则被弹出。因为我们假设数组参数是用指针来指示的,所以每次过程调用只需要O(1)的栈空间。栈深度是在一次计算中会用到的栈空间的最大值。

第一次循环,PARTITION函数把p和r划分成两部分,小于A[q]的都放入到TAIL_RECURSIVE_QICKSORT(A,p,q-1)内,然后调用自身,继续选取主元,把小于A[q]又都放入到TAIL_RECURSIVE_QICKSORT(A,p,q-1),经过多次对自身调用后,TAIL_RECURSIVE_QICKSORT的第三个参数减少到p=r,这样TAIL_RECURSIVE_QICKSORT开始返回,然后执行p=q+1,对每次的TAIL_RECURSIVE_QICKSORT调用的进行下一层循环,下一层循环PARTITION(A,q+1,r)是对于大于A[q]的区间进行再划分,这样不断的循环划分递归直到数组有序。

参考文献:

【1】算法导论 机械工业出版社 第七章 快速排序。

【2】算法导论答案英文版(免费下载)

猜你喜欢

转载自blog.csdn.net/weixin_42206504/article/details/82693453