快速排序做为一种比较经典的排序思路,经常在面试中被提及。这一节就分解下快排的思路,最后用代码实现一下。
快速排序
假设有下面一个数组需要排序
[38,67,11,26,20,99,45,54]
思路就是从第一个元素38开始,找到一个中间位置,将比38小的元素都放在左边,比38大的元素都放在右边。然后再分别对左右两边的子序列进行同样的操作,最后达到整体有序。
为了达到这个目的,我们只要弄清楚了单次排序的实现方法,后面的子序列只需要采用递归的方式就可以依法炮制了。
下面来看看单次排序的思路。
首先需要两个游标,一个low游标指向第一个元素,也就是待定位元素,另一个high游标指向最后一个元素。如下图所示
首先让high游标向左移动,一直到找到比38小(先不考虑等于)的元素,赋值给low游标。如下图所示,就是将20赋值给第一个元素
然后让low游标向右移动,一直找到比38大的元素,赋值给high游标。如下图所示,就是将67赋值给high所在的元素
如此循环,在移动high游标,将26赋值给low游标的元素
直到low和high两个游标重合,将38放到该处
这样就实现了38左边都比38小,右边都比38大的效果。
之后采用递归地思路,将左边的[20,26,11]
对20采用该方法,对右边的[67,99,45,54]
的67也采用该方法。然后再分为4个子序列重复。
代码实现
下面的代码基于python3,首先实现单次排序。
单次排序
根据上面的理论,可以写出单次排序的代码如下
def fast_sort(list_):
mid_value = list_[0]
low = 0
high = len(list_) - 1
while low < high:
while low < high and list_[high] >= mid_value:
high -= 1
list_[low] = list_[high]
while low < high and list_[low] < mid_value:
low += 1
list_[high] = list_[low]
list_[low] = mid_value
验证下
if __name__ == '__main__':
list_ = [38, 67, 11, 26, 20, 99, 45, 54]
print(list_)
fast_sort(list_)
print(list_)
打印结果为
[38, 67, 11, 26, 20, 99, 45, 54]
[20, 26, 11, 38, 67, 99, 45, 54]
内层循环的low<high
的判断是否多余?
不多余,因为有可能在内层的某个循环中就出现low>high
的情况,例如
def fast_sort(list_):
mid_value = list_[0]
low = 0
high = len(list_) - 1
while low < high:
while list_[high] > mid_value:
high -= 1
list_[low] = list_[high]
while list_[low] < mid_value:
low += 1
list_[high] = list_[low]
print(low, high) # 这里打印下low和high
list_[low] = mid_value
if __name__ == '__main__':
list_ = [38, 67, 11, 26, 20, 99, 45, 54]
print(list_)
fast_sort(list_)
print(list_)
此时的打印结果为
[38, 67, 11, 26, 20, 99, 45, 54]
4 3
[20, 26, 11, 67, 38, 99, 45, 54]
可以看到最后low为4而high为3,并且最后的结果也不对。是因为在最后一步中,low一直从26的地方经过11,又经过了high指向的26。所以有必要在内部的两个循环也分别加上限制条件。
这也给我们提了个醒,在循环内对循环退出变量做除了简单+1操作以外的复杂操作时要格外小心,记得做判断退出循环。
等于待定位元素的情况怎么处理?
这里我分别用了list_[high] >= mid_value
和list_[low] < mid_value
做为循环条件,也就是说high游标遇到相等的值会直接跳过继续往左,而low游标则会等待直到交换。最终的结果就是和待定位元素相等的值都去了中间位置的右边。当然不加这个等号或是上下都加等号也是没问题的,因为反正最终几个值得顺序都会被打乱,肯定是不稳定的,所以没有区别,这里只是为了实现都到一边的目的才加了一个等号。
递归
下面要开始处理递归,如下,利用开头的if语句退出递归,最后的两个函数调用自身分别对左右两个子序列进行操作
def fast_sort(list_, first, last):
if first >= last:
return
mid_value = list_[first]
low = first
high = last
while low < high:
while low < high and list_[high] >= mid_value:
high -= 1
list_[low] = list_[high]
while low < high and list_[low] < mid_value:
low += 1
list_[high] = list_[low]
list_[low] = mid_value
fast_sort(list_, first, low - 1)
fast_sort(list_, low + 1, last)
验证下
if __name__ == '__main__':
list_ = [38, 67, 11, 26, 20, 99, 45, 54]
print(list_)
fast_sort(list_, 0, len(list_) - 1)
print(list_)
结果正确
[38, 67, 11, 26, 20, 99, 45, 54]
[11, 20, 26, 38, 45, 54, 67, 99]
为什么多加两个参数而不是直接传递切片?
因为这里是对原list直接进行操作,切片之后返回的是新list,和原list的地址不一致,所以不能直接操作,只能间接赋值,如下也能达到目的
def fast_sort(list_):
mid_value = list_[0]
low = 0
high = len(list_) - 1
while low < high:
while low < high and list_[high] >= mid_value:
high -= 1
list_[low] = list_[high]
while low < high and list_[low] < mid_value:
low += 1
list_[high] = list_[low]
list_[low] = mid_value
if low > 0:
list_[0:low] = fast_sort(list_[0:low])
if low < len(list_) - 1:
list_[low + 1:] = fast_sort(list_[low + 1:])
return list_
注意这里最后要进行返回。同时这里要考虑特殊情况,就是单边的子序列只有一个元素的时候,这时候最后的两个if都不满足,直接原样返回退出递归。
同样去验证下
if __name__ == '__main__':
list_ = [38, 67, 11, 26, 20, 99, 45, 54]
print(list_)
# fast_sort(list_, 0, len(list_) - 1)
fast_sort(list_)
print(list_)
返回的结果是一样的
[38, 67, 11, 26, 20, 99, 45, 54]
[11, 20, 26, 38, 45, 54, 67, 99]
退出递归的大于号有必要吗?
有必要,因为当最后low和high都在最边上的元素时,会出现类似first为0而last为-1的情况,或者是first为len(list_)
而last为len(list_)-1
的情况。要对这些情况直接退出递归不做任何操作。
时间复杂度
最好情况
最好情况就是每次从中点对半分,每次子序列是原先的一般,一直到最后长度为1。所以总的遍历次数m满足
n*(1/2)^m = 1,最后m=log2n。而每次都遍历n次,所以时间复杂度为O(nlogn)
最坏情况
最坏情况就是每次都在最边上的元素,一共遍历n遍,每次n个元素。时间复杂度为O(n^2)
稳定性
从前面的分析可以看出来,无论代码如何去设定相等值的处理,原先的顺序都会被打乱,所以快速排列一定是不稳定的。
完整代码
这里我将第二种传切片的方式注释起来,以供参考
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time: 2020-07-10
# @Author: xiaofu
# def fast_sort(list_):
# # if len(list_) == 1:
# # return list_
# mid_value = list_[0]
# low = 0
# high = len(list_) - 1
#
# while low < high:
# while low < high and list_[high] >= mid_value:
# high -= 1
# list_[low] = list_[high]
# while low < high and list_[low] < mid_value:
# low += 1
# list_[high] = list_[low]
# # print(low, high)
# list_[low] = mid_value
#
# if low > 0:
# list_[0:low] = fast_sort(list_[0:low])
# if low < len(list_) - 1:
# list_[low + 1:] = fast_sort(list_[low + 1:])
#
# return list_
def fast_sort(list_, first, last):
if first >= last:
return
mid_value = list_[first]
low = first
high = last
while low < high:
while low < high and list_[high] >= mid_value:
high -= 1
list_[low] = list_[high]
while low < high and list_[low] < mid_value:
low += 1
list_[high] = list_[low]
# print(low, high)
list_[low] = mid_value
fast_sort(list_, first, low - 1)
fast_sort(list_, low + 1, last)
if __name__ == '__main__':
list_ = [38, 67, 11, 26, 20, 99, 45, 54]
print(list_)
fast_sort(list_, 0, len(list_) - 1)
# fast_sort(list_)
print(list_)
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。