快速排序
原理
# 在列表之中,选择一个元素作为”基准”(pivot),或者叫比较值。
# 使这个基准数归位;
# 归位:列表中所有元素都和这个基准数进行比较,如果比基准数小就移到基准数的左边,如果比基准数大就移到基准数的右边;最终,基准数把列表分割成两部分。左部分的数都比基准数小,右部分的数都比基准数大;
# 以基准值左右两边的子列作为新列表,递归。
# 基准数的选择方式有很多方式,比如可以选择列表的第一个元素。
# 总结:快排就是两边往中间找,分块,在局部两边往中间找,递归...
快排算法框架
def quick_sort(li, left, right): # left、right分别是列表第一个和最后一个元素的索引值(左右边界)
if left < right: # 表示列表中至少有两个元素
mid = partition(li, left, right) # 归并找到基准数应该的位置
quick_sort(li, left, mid-1) # 左部分递归
quick_sort(li, mid+1, right) # 有部分递归
# 剩下的唯一任务就是实现 partition()函数的归并功能
代码实现
快排算法的特点:快
快排的时间复杂度:O(nlogn)
递归实现(C语言版)
# -*- coding: utf-8 -*-
# created by X. Liu on 2020/3/11
def quick_sort(li, left, right):
if left < right:
mid = partition(li, left, right)
quick_sort(li, left, mid-1)
quick_sort(li, mid+1, right)
def partition(li, left, right):
tmp = li[left] # 第一个值当基准数先保存起来
while left < right: # 开始归并
while left < right and li[right] >= tmp: # 找到右边的值比tmp小的那个位置
right -= 1 # right 往左移位
li[left] = li[right] # 右边小于tmp的数换到左边空位上
while left < right and li[left] <= tmp: # 找到左边大于tmp的数的位置
left += 1 # left往右移位
li[right] = li[left] # 左边大于tmp的数换到右边空位上
li[left] = tmp # 找到合适的归并位置,此时left==right,将tmp放在这两个位置上的任意一个。
return left # 返回改位置,作为递归下一次的左右边界值
# partition 中内层的第一个while循环的循环条件(left < right and li[right] >= tmp)解释:
#1 left < right 指的是从右边开始往左找小于tmp的数时,右边的数都比tmp大,此时就会找到列表的第一个元素,right==left,此时应该退出循环。
#2 li[right] >= tmp, 指的是在列表右边找到比tmp小的数
# # partition 中内层的第二个while循环的循环条件原理类似。
伪快排
def mySort(l):
if len(l) < 2: # 如果列表中只有一个元素就会返回
return l
num=l[0] # 拿一个元素作为参考元素
startl=[x for x in l[1:] if x <= num ] # 使用列表推导式把小于等于参考元素的放入新的列表
endl=[x for x in l[1:] if x > num ] # 使用列表推导式把大于参考元素的放入新的列表
# 使用递归的方式将排序好的元素拼接为新的列表返回
return mySort(startl)+[num]+mySort(endl)
# 上面代码虽然短小且便于理解,但是它并不是真正的快排。
# 其效率很低,主要体现在以下方面:
#1 分组基准的选取过于随便,不一定可以取到列表的中间值
#2 空间复杂度大O(nlogn)
#3 若序列长度过于小(比如只有几个元素),快排效率就不如插入排序了。
动画演示
待补充......
补充
上述实现的快排存在一个最坏情况
# 比如这个列表
l =[9,8,7,6,5,4,3,2,1]
对于这种列表,我们使用上述快排算法会出现一个最坏的情况,
这种情况下时间复杂度将不再是O(nlogn),而是O(n^2)
导致这个问题出现的原因是我们每次归并是使用的是同一个位置的数,如上述代码中的left,列表的第一个元素。
解决这个问题的办法:随机选取一个数做归并,这样就可以极大概率上避免出现快排的最坏情况。