快速排序是常见的冒泡排序的改进。和经典的排序算法合并排序一样,快速排序也运用了分治的思想。冒泡排序是在一个序列中只把一个元素冒泡到数列的一端,而快排是每次选中一个pivot当作基准元素,小于pivot放在pivot的左边,反之放在右边,从而将序列分为了两个部分。
1 快排的前身冒泡排序
冒泡排序的思路:就像是冒泡一样,依次比较相邻的两个数,如果第一个大于第二个那么就将这两个数交换。重复以上的步骤,直到没有任何一个数字需要比较。
如图就像冒泡一样:
图片来源:简书,侵删
冒泡排序的实现:
def bubble(arr):
n=len(arr)
for i in range(n-1):
for j in range(i,n):
if arr[i]>arr[j]:
temp=arr[i]
arr[i]=arr[j]
arr[j]=temp
return arr
时间复杂度分析: 由于在遍历数组时,每次遍历都会再遍历一遍数组,所以时间复杂度为O(N^2)
2 快速排序QuickSort
2.1 快速排序的思路:每次都在数列中选定一个数当作pivot一个基准。将数列依次与pivot进行比较,如果小于pivot,那么将该元素放在pivot左边,反之放在pivot右边。这样每一轮计算都能将数组分为两个部分,比pivot大和比pivot小。这是一个很典型的分治思想,将一个大问题分成了子问题。
往往pivot的设立是不定的,可以是序列的中间,可以是开头,结尾。
2.2选定pivot后每次该如何移动到左边或右边:知道了大概的算法思路,现在要来解决,每次计算时如何将比pivot小的移动到左边,比它大的移动到右边。
我们采取这样的办法,在序列最左边最右边设立两个哨兵,分别叫做i,j。这两个哨兵会向序列中间移动,i向右-> , j向左<-。当i指向的数字大于pivot时,i停止移动,当j指向的数字小于pivot时,j停止移动。当i和j都停止移动时,将i和j进行交换。直到最后i和j重复,此时停止比较。此时我们就可以将序列分为两部分,小于pivot和大于pivot,那么整个pivot的位置就是可以确定下来的了。
如以下例子,我们拥有一个序列[1,2,7,5,6,4,8],并且将5作为pivot:
重复以上的方法,直到pivot左右都只有一个元素后停止,不再划分。
2.3 快速排序的图解:
来源百度,侵删
2.4 快速排序的代码实现:
在有了每一次的排序方法后,我们只需要不断地将序列分割。一个典型的分治思想,我们可以运用递归的方法来实现。
假定每次pivot都选中序列的最左边位置:
def quick_sort(list, low, high):
i = low
j = high
if i >= j:
return list
pivot = list[i]
while i < j:
while i < j and list[j] >= pivot:
j = j - 1
list[i] = list[j]
while i < j and list[i] <= pivot:
i = i + 1
list[j] = list[i]
list[i] = pivot
quick_sort(list, low, i - 1)
quick_sort(list, j + 1, high)
return list
时间复杂度分析:
快排对我们选择pivot非常相关,
最坏的情况:如果pivot刚好就选中的是最大的或者最小那一个,那么我们根本就起不到分块的作用,所以时间复杂度是O(n^2)
最好的情况:每次的pivot选择刚好就是在中间的那个,每次都平分,那么就和归并排序一样,时间复杂度也是O(nlogn)
平均时间复杂度为O(nlogn)
详细证明过程可以参考快速排序时间复杂度分析 - 知乎 (zhihu.com)