分治法:快速排序,3种划分方式,随机化快排,快排快,还是归并排序快?

快速排序不同于之前了解的分治,他是通过一系列操作划分得到子问题,不同之前的划分子问题很简单,划分子问题的过程也是解决问题的过程

我们通常划分子问题尽量保持均衡,而快排缺无法保持均衡

快排第一种划分子问题实现方式,左右填空的双指针方式

def partition_1(arr,low,high):
    # 把基准元素取出来,留出一个空位,这里是在首位,这种留出空位的方式,比较容易理解
    pivot = arr[low]
    
    # 循环体终止条件,因为是先走右边再走左边,终止的时候一定是两个指针重合在一起
    # 也可以交叉,但是可以控制循环他们重合在一起跳出循环
    # 这里解释以下low,high这两个指针代表什么,low,high代表从其实到low都是小于基准的元素
    # 从high到end都是大于基准的元素,当low和high重合时,那左边都是小的,右边都是大的
    # 重合的位置是空的(实际上有值),因为每个时刻都有一个位置都是空的,重合剩下最后一个位置,
    # 这个位置也必然是空的,也可以用一个小的实例分析一下
    while low < high:
        # 首先右边一直往左走,直到遇到小于基准的元素,这里控制一下,不让他们交叉
        # 不添加的low <high,往左走不会越界,但是可能小于low
        while arr[high] > pivot and low <high:
            high -=1
        # 避免他们两交叉,只要相等就退出,右边遇到小于基准的元素,把左边的那个空位填上,左边的指针更新一下       
        if low <high:    
            arr[low] = arr[high]
            low +=1
         # 左指针往左就是小于基准的元素,这时右边空出来一个位置,左指针往右扫描
        while arr[low] < pivot and low <high:
            low +=1
        # 找到大于基准的元素,放到右边空出来的位置,那右指针往右全部都是大于基准元素的
        if low <high: 
            arr[high] = arr[low]
            high -=1
    # 当只剩下唯一的空位置时,把基准元素放待空的位置上    
    arr[low] = pivot
    
    return low

快排第二种划分子问题方式,单指针方式

def partition_2(arr,low,high):
    # 这时另外一种考虑方式,而且他是不需要额外空间的,他只使用一个指针来区分小于基准和大于基准的
    # pointer_less_than代表这个指针的左边全部都是小于基准的(包括自己,不包括首元素)
    # 然后从左往右扫描,遇到小于基准的元素,就把小于基准元素区域的后面紧接着的一个元素和他交换
    # 那么小于基准元素区域就多了一个元素,。。。就这样小于基准的元素就连在了一起
    # 首元素是基准元素,小于基准元素区域块,大于基准元素区域块,现在分成了三个部分
    # 把首元素和小于基准元素区域块最后一个元素交换,那三部分就变成,小于的,基准,大于的
    
    # 刚开始小于基准的元素为0,暂且指向首位值
    pointer_less_than = low
    # 然后一次扫描后面所有元素
    for i in range(pointer_less_than +1,high+1):
        # 遇到小于基准的,就把小于基准元素区域的后面紧接着的一个元素和他交换,小于的块相当于也更新了
        if arr[i] < arr[low] :
            pointer_less_than +=1
            arr[pointer_less_than],arr[i]=arr[i],arr[pointer_less_than]
    #  把首元素和小于基准元素区域块最后一个元素交换,那三部分就变成,小于的,基准,大于的       
    arr[low],arr[pointer_less_than] = arr[pointer_less_than],arr[low]
    
    return pointer_less_than

第三种划分子问题实现方式,左右同时交换,这种方式注意结束的情况

def partition_3(arr,start,end):
    # 这个方式也是不需要额外的辅助空间的
    # 他的思想是:从左(或者右也可以)扫描到第一个大于基准的元素,然后从右往左扫描到第一个小于基准的
    # 元素,将他们两交换,然后再重复上述操作,直到两个指针重合位置
    # 这两个指针分别代表:前面(除了首元素)到low为小于基准,high到end为大于基准元素
    # 他们是可能会交叉的,也有可能重合,这时数组分成三个部分:首元素基准,小于的,大于的
    # 这个地方可能会交叉的,也有可能重合,分3种情况:第一种情况[大于,小于],然后他们两个交换
    # [小于,大于],low-->大于,high-->小于,这时首元素需要和high互换
    # [小于],high-->小于,没有大于的元素和他互换,low一直加直到等于high,这时这时首元素需要和high互换
    # [大于],low-->大于,没有小于的元素和他互换,high会一直减,直到比low小1,这时这时这时首元素需要和high互换
    # 不管是那种情况下,high指向肯定是最后一个小于基准的元素 
    # 这里不能利用两者指针重合,因为两个指针重合指向的元素,可能大于基准也可能小于基准,
    # 要使用high指向的元素
    
    # 初始化左指针和右指针
    low = start
    high = end +1
    
    # 循环体退出条件为两指针重合或者交叉
    while True:
        # 需要先-1,因为交换之后,指针需要更新一下,不更新的话,循环体会多运算一步
        high -=1
        # 这里就是需要两指针交叉,这样high才能指向小于区域里面的最后一个元素
        while arr[high] > arr[start] :
            high -=1
            
        low +=1    
        while arr[low] < arr[start] and  low < end:
            low +=1
        
        # 在这个时候,数组分成三个部分:首元素是基准元素,小于基准元素区域块,大于基准元素区域块
        if low >= high:
            break
        # 把这两个元素交换,小的跑到左边,大的跑到右边
        arr[low],arr[high] = arr[high],arr[low]
    # 把首元素和小于基准元素区域块最后一个元素交换,那三部分就变成,小于的,基准,大于的
    arr[start],arr[high] = arr[high],arr[start]
    
    return high

运行结果

#%%    
def quickSort(arr,low,high):    
    if low < high:
        index = partition_1(arr,low,high)
        quickSort(arr,low,index-1)
        quickSort(arr,index+1,high)
        
        
def quickSort1(arr,low,high):    
    if low < high:
        index = partition_2(arr,low,high)
        quickSort1(arr,low,index-1)
        quickSort1(arr,index+1,high)
        
        
def quickSort2(arr,low,high):    
    if low < high:
        index = partition_3(arr,low,high)
        quickSort2(arr,low,index-1)
        quickSort2(arr,index+1,high)

arr = [7,3,66,33,22,66,99,0,1]
print(arr)
quickSort(arr,0,len(arr)-1)
print(arr)  

arr1 = [7,3,66,33,22,66,99,0,1]
#print(arr1)
quickSort1(arr1,0,len(arr1)-1)
print(arr1)  


arr2 = [7,3,66,33,22,66,99,0,1]
#print(arr2)
quickSort2(arr2,0,len(arr2)-1)
print(arr2)  

[7, 3, 66, 33, 22, 66, 99, 0, 1]
[0, 1, 3, 7, 22, 33, 66, 66, 99]
[0, 1, 3, 7, 22, 33, 66, 66, 99]
[0, 1, 3, 7, 22, 33, 66, 66, 99]

前面提到快排的划分不一定是均衡划分,而快排的效率取决于划分的对称性,稍微改进一下为随机化算法,这类把某一步修改成随机步骤的算法,叫做拉斯维加斯算法

import random        
def randomizedPartition(arr,low,high):
    def partition(arr,low,high):
        # 这时另外一种考虑方式,而且他是不需要额外空间的,他只使用一个指针来区分小于基准和大于基准的
        # pointer_less_than代表这个指针的左边全部都是小于基准的(包括自己,不包括首元素)
        # 然后从左往右扫描,遇到小于基准的元素,就把小于基准元素区域的后面紧接着的一个元素和他交换
        # 那么小于基准元素区域就多了一个元素,。。。就这样小于基准的元素就连在了一起
        # 首元素是基准元素,小于基准元素区域块,大于基准元素区域块,现在分成了三个部分
        # 把首元素和小于基准元素区域块最后一个元素交换,那三部分就变成,小于的,基准,大于的
        
        # 刚开始小于基准的元素为0,暂且指向首位值
        pointer_less_than = low
        # 然后一次扫描后面所有元素
        for i in range(pointer_less_than +1,high+1):
            # 遇到小于基准的,就把小于基准元素区域的后面紧接着的一个元素和他交换,小于的块相当于也更新了
            if arr[i] < arr[low] :
                pointer_less_than +=1
                arr[pointer_less_than],arr[i]=arr[i],arr[pointer_less_than]
        #  把首元素和小于基准元素区域块最后一个元素交换,那三部分就变成,小于的,基准,大于的       
        arr[low],arr[pointer_less_than] = arr[pointer_less_than],arr[low]
        
        return pointer_less_than
    
    index = random.randint(low,high)
    arr[low],arr[index]=arr[index],arr[low]
    return partition(arr,low,high)

def randomizedQuicksort(arr,low,high):
    if low < high:
        index = randomizedPartition(arr,low,high)
        randomizedQuicksort(arr,low,index-1)
        randomizedQuicksort(arr,index+1,high)


arr3 = [7,3,66,33,22,66,99,0,1]
print(arr3)
randomizedQuicksort(arr3,0,len(arr3)-1)
print(arr3) 

[7, 3, 66, 33, 22, 66, 99, 0, 1]
[0, 1, 3, 7, 22, 33, 66, 66, 99]

快排快,还是归并排序快?

按照理论上分析,快排平均时间复杂度O(nlogn),最坏为n^2,归并平均和最坏均为nlogn。
理论上应该是归并快才对,但是有好多人测试很大规模数据的情况下快排比归并快。
可能的原因,归并有空间开销,有数组复制合并的操作,快排属于原地排序,在数据规模大的情况下,差距就出来了。

猜你喜欢

转载自blog.csdn.net/weixin_40759186/article/details/84975541