Python数据结构与算法—快速排序(NB组)

一、基本思路

1、特点

排序速度快。

2、思路

S1: 取一个元素p(第一个元素),使元素归位;

S2:列表为p分成两部分,左边都比p小,右边都比p大;

S3:递归完成排序。

3、具体操作

1)如上图,将元素p,例如5,存入某个变量。则5所在位置为空。两个箭头表示left和right。由于列表归位后的效果为p的左侧都比p小,右侧都比p大。因此该空位应该是比p元素(5)小的数准备的。

2)且应优先将右侧的小于p的数值移动到空的位置,则从右往左找到小于p的数字2,将数字2的索引改为空位置的索引,其中原来2所在的位置是指针right的指向,空位置为指针left指向。这时2原来的位置right又变为空位。

3)上一个空位位于右侧right,则从左找到大于5的数值移动到该空位,即left指针找到7,移动7到空位,则7所在位置为right指针指向。

4)这时left指的位置又出现空位。从右往左移动right指针,找到小于5的数1,移动到空位上。

5)重复上面的操作,最后箭头left与right重合,这是左边的数字都小于5,右边的数值都大于5,将5放入这个空位中。如下图。

4、代码框架

def quick_sort(data,left,right): #data表示列表数据,left和right分别为列表指针
    if left < right: #保证列表元素大于2
        mid = partition(data,left,right) #p元素的位置选择
        #使用递归,重复找不同p元素位置
        quick_sort(data,left,mid-1) #上一个p元素的左侧范围
        quick_sort(data,mid+1,right) #上一个p元素的右侧范围

二、代码实现

1、核心函数partition()代码

#快速排序

#插入p元素的核心函数
def partition(li,left,right): #li-列表,left和right表示列表范围
    tmp = li[left] #保存列表的第一个元素,即p元素
    while left < right: #当left指针在right指针的左侧时,循环继续
        while left < right and li[right] >= tmp: #从右往左找小于p元素的值
        #由于right往左移动会-1,若无法找到小于p元素的值,也需要确保跳出while循环,所以需要加left<right
            right -= 1 #right往左移一步
        li[left] = li[right] #从右往左找到小于p元素的值后,将其移动到左边空位上
        while left < right and li[left] <= tmp: #从左往右找,大于p元素的值,若小于p元素继续循环
        #与上一个while中的left<right相同,需要确保若无法找到大于p元素的值,也需要跳出循环;
        #且上一个while循环后已找到p元素的位置则不再进入该循环
            left += 1 #left往右移一步
        li[right] = li[left] #从左往右找到大于p元素的值,移到右侧空位上
    li[left] = tmp #当left=right时,p元素归位
    return left  #返回p元素的索引(位置)

li = [5,7,4,6,9,8,3,0,1]
partition(li,0,len(li)-1)
print(li)

输出结果:

[1, 0, 4, 3, 5, 8, 9, 6, 7]

说明:通过partition()函数,将p元素归位使p元素的左侧都小于p的值,右侧都大于p的值。

2、整体代码实现

#插入排序
def partition(li,left,right): #li-列表,left和right表示列表范围
    tmp = li[left] #保存列表的第一个元素,即p元素
    while left < right: #当left指针在right指针的左侧时,循环继续
        while left < right and li[right] >= tmp: #从右往左找小于p元素的值
        #由于right往左移动会-1,若无法找到小于p元素的值,也需要确保跳出while循环,所以需要加left<right
            right -= 1 #right往左移一步
        li[left] = li[right] #从右往左找到小于p元素的值后,将其移动到左边空位上
        while left < right and li[left] <= tmp: #从左往右找,大于p元素的值,若小于p元素继续循环
        #与上一个while中的left<right相同,需要确保若无法找到大于p元素的值,也需要跳出循环;
        #且上一个while循环后已找到p元素的位置则不再进入该循环
            left += 1 #left往右移一步
        li[right] = li[left] #从左往右找到大于p元素的值,移到右侧空位上
    li[left] = tmp #当left=right时,p元素归位
    return left  #返回p元素的索引(位置)
#整体代码
def quick_sort(li,left,right):
    if left < right: #确保列表元素大于2
        mid = partition(li,left,right) 
        quick_sort(li,left,mid-1) #p元素左侧范围left到mid-1
        quick_sort(li,mid+1,right) #p元素右侧范围mid+1到right

lis = [5,7,4,2,9,3,0,8]
quick_sort(lis,0,len(lis)-1)
print(lis)

结果输出:

[0, 2, 3, 4, 5, 7, 8, 9]

三、时间复杂度

快速排序的时间复杂度为,其小于Low B三人组(冒泡排序、选择排序、插入排序)的时间复杂度为

1、原理大致分析:

1)由于快速排序整体为递归,先分析核心函数partition(),partition()函数的时间复杂度为n,虽然有两层while循环,但实际只是将p元素左边和右边都遍历一遍,加起来还是遍历一遍整个列表,即为每一趟的时间复杂O(n)。

2)分析递归,假设n=16,且每次左边和右边的元素数量相同,递归部分代码的运行逻辑如下图:

由上图可以发现,第一趟是n,第二趟是2n,第三趟是4n,第四趟8n,其中每一趟后,n的规模逐半减少,符合时间复杂度O(logn)的表现。

3)将partition()函数与递归部门结合,分析得时间复杂度为n * logn=nlogn。因此,整体代码的时间复杂度为

2、代码运行时间对比——用时间装饰器

(1)代码输入

import random
from cal_time import cal_time
import copy #复制模块 

#冒泡排序
@cal_time
def bubble_sorted1(li):
    for i in range(len(li)-1): #排序将进行n-1趟,最后一个位置不需要再遍历
        exchange = False #判断跑完一趟是否有元素交换
        for j in range(len(li)-i-1):  #每趟的无序区间,只有无序区间需要排序
            if li[j] > li[j+1]:
                li[j],li[j+1] = li[j+1],li[j] #序列内部对调位置
                exchange = True
        # print(li)
        if not exchange: #如果未发生交换,跳出循环,输出结果
             return 

#快速排序
def partition(li,left,right): #li-列表,left和right表示列表范围
    tmp = li[left] #保存列表的第一个元素,即p元素
    while left < right: #当left指针在right指针的左侧时,循环继续
        while left < right and li[right] >= tmp: #从右往左找小于p元素的值
            right -= 1 #right往左移一步
        li[left] = li[right] #从右往左找到小于p元素的值后,将其移动到左边空位上
        while left < right and li[left] <= tmp: #从左往右找,大于p元素的值,若小于p元素继续循环
            left += 1 #left往右移一步
        li[right] = li[left] #从左往右找到大于p元素的值,移到右侧空位上
    li[left] = tmp #当left=right时,p元素归位
    return left  #返回p元素的索引(位置)

def _quick_sort(li,left,right):
    if left < right: #确保列表元素大于2
        mid = partition(li,left,right) 
        _quick_sort(li,left,mid-1) #p元素左侧范围left到mid-1
        _quick_sort(li,mid+1,right) #p元素右侧范围mid+1到right

#由于quick_sort()为递归,使用时间装饰器需要在外面加一个包装,否则每次递归都会调用时间装饰器
@cal_time
def quick_sort(li):
    _quick_sort(li,0,len(li)-1)

li = list(range(10000)) #创建一个大规模列表
random.shuffle(li) #打乱列表顺序

li1 = copy.deepcopy(li) #深度复制li列表
li2 = copy.deepcopy(li) #深度复制li列表,是li1与li2相同

quick_sort(li1)
bubble_sorted1(li2)

(2)结果输出

quick_sort running time: 0.024933338165283203 secs.
bubble_sorted1 running time: 8.43781590461731 secs.

(3)解释说明

对比两种算法的运行时间,很明显,快速排序的时间比冒泡排序快上很多。

四、快速排序的劣势

1、递归

消耗系统资源,递归最大深度

2、最坏情况

时间复杂度最坏情况是O()。

(1)说明

当序列的顺序刚好与需要排序的顺序完全相反时,例如,列表[5,4,3,2,1],用快速排序需排序列表长度为5。

1)第一趟排序后[1,4,3,2,5],归位的元素5,5的右侧没有数列,左侧需排序列表长度为4。

2)第二趟排序后[1,4,3,2,5],归位的元素1,1 仍旧位置不变,1左侧无数值,右侧需排序列表长度为3。

3)第三趟排序后[1,2,3,4,5],归位的元素4,4右侧无需要排序的数值,右侧需排序列表长度为2。

4)第四趟排序后[1,2,3,4,5],归位的元素2,2左侧无需要排序的数值,右侧需排序列表长度为1,无需再排序。

总结:每一趟的运行规模只减少1,并未接近逐半减少,因此时间复杂度为

3、大规模情况示例

(1)不修改代码的递归深度

输入代码:

import random
from cal_time import cal_time

#快速排序
def partition(li,left,right): #li-列表,left和right表示列表范围
    tmp = li[left] #保存列表的第一个元素,即p元素
    while left < right: #当left指针在right指针的左侧时,循环继续
        while left < right and li[right] >= tmp: #从右往左找小于p元素的值
            right -= 1 #right往左移一步
        li[left] = li[right] #从右往左找到小于p元素的值后,将其移动到左边空位上
        while left < right and li[left] <= tmp: #从左往右找,大于p元素的值,若小于p元素继续循环
            left += 1 #left往右移一步
        li[right] = li[left] #从左往右找到大于p元素的值,移到右侧空位上
    li[left] = tmp #当left=right时,p元素归位
    return left  #返回p元素的索引(位置)

def _quick_sort(li,left,right):
    if left < right: #确保列表元素大于2
        mid = partition(li,left,right) 
        _quick_sort(li,left,mid-1) #p元素左侧范围left到mid-1
        _quick_sort(li,mid+1,right) #p元素右侧范围mid+1到right

#由于quick_sort()为递归,使用时间装饰器需要在外面加一个包装,否则每次递归都会调用时间装饰器
@cal_time
def quick_sort(li):
    _quick_sort(li,0,len(li)-1)

#大规模倒序,最坏情况示例
li = list(range(10000,0,-1)) #倒序排序
quick_sort(li)

输出代码:

........
........
RecursionError: maximum recursion depth exceeded in comparison

报错说明:python代码已经达到了最大的递归深度

(2)修改代码的递归深度

代码输入:

import random
from cal_time import cal_time
import sys
sys.setrecursionlimit(10000) #sys模块,修改最大递归深度

#快速排序
def partition(li,left,right): #li-列表,left和right表示列表范围
    tmp = li[left] #保存列表的第一个元素,即p元素
    while left < right: #当left指针在right指针的左侧时,循环继续
        while left < right and li[right] >= tmp: #从右往左找小于p元素的值
            right -= 1 #right往左移一步
        li[left] = li[right] #从右往左找到小于p元素的值后,将其移动到左边空位上
        while left < right and li[left] <= tmp: #从左往右找,大于p元素的值,若小于p元素继续循环
            left += 1 #left往右移一步
        li[right] = li[left] #从左往右找到大于p元素的值,移到右侧空位上
    li[left] = tmp #当left=right时,p元素归位
    return left  #返回p元素的索引(位置)

def _quick_sort(li,left,right):
    if left < right: #确保列表元素大于2
        mid = partition(li,left,right) 
        _quick_sort(li,left,mid-1) #p元素左侧范围left到mid-1
        _quick_sort(li,mid+1,right) #p元素右侧范围mid+1到right

#由于quick_sort()为递归,使用时间装饰器需要在外面加一个包装,否则每次递归都会调用时间装饰器
@cal_time
def quick_sort(li):
    _quick_sort(li,0,len(li)-1)

#大规模倒序,最坏情况示例
li = list(range(2500,0,-1)) #倒序排序
lis = list(range(2500)) #正序
random.shuffle(lis) #打乱排序
quick_sort(li)
quick_sort(lis)

结果输出:

quick_sort running time: 0.30812644958496094 secs.
quick_sort running time: 0.007014274597167969 secs.

说明:当数据规模相同时,同样使用快速排序,达到最坏情况的列表,排序所需时间多很多。

五、最坏情况解决

原先的p元素都是选取的序列的第一个位置,可以通过随机选择一个数字,并与序列第一个值互换位置,后续操作相同即可。但是仍然还是会出现最坏情况,只是发生的概率会比较小。

1、修改代码

输入代码

#最坏情况修正
import random
from cal_time import cal_time
import copy #复制模块 
import sys

sys.setrecursionlimit(10000) #sys模块,修改最大递归深度

#快速排序
def partition(li,left,right): #li-列表,left和right表示列表范围
    num = random.randint(left,right) #生成一个随机整数
    li[left],li[num] = li[num],li[left] #交换无序区第一个数值,更改为随机获取的数,以打乱列表现有顺序
    tmp = li[left] #保存列表的第一个元素,即p元素
    while left < right: #当left指针在right指针的左侧时,循环继续
        while left < right and li[right] >= tmp: #从右往左找小于p元素的值
            right -= 1 #right往左移一步
        li[left] = li[right] #从右往左找到小于p元素的值后,将其移动到左边空位上
        while left < right and li[left] <= tmp: #从左往右找,大于p元素的值,若小于p元素继续循环
            left += 1 #left往右移一步
        li[right] = li[left] #从左往右找到大于p元素的值,移到右侧空位上
    li[left] = tmp #当left=right时,p元素归位
    return left  #返回p元素的索引(位置)

def _quick_sort(li,left,right):
    if left < right: #确保列表元素大于2
        mid = partition(li,left,right) 
        _quick_sort(li,left,mid-1) #p元素左侧范围left到mid-1
        _quick_sort(li,mid+1,right) #p元素右侧范围mid+1到right

#由于quick_sort()为递归,使用时间装饰器需要在外面加一个包装,否则每次递归都会调用时间装饰器
@cal_time
def quick_sort(li):
    _quick_sort(li,0,len(li)-1)

li = list(range(2500,0,-1))
quick_sort(li)

输出结果:

quick_sort running time: 0.004987001419067383 secs.

2、说明

对比原先的partition()函数,添加了两行代码,交换需排序列表第一个数值,从而打乱列表顺序。可得同样规模且倒序的列表,排序所需时间相对比原代码所需时间少很多。(对比4-(3)大规模情况示例的结果)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值