1. 快速排序
介绍
快速排序是一种在最坏情况下时间复杂度为O(n2)的排序算法,但在实际应用时却是较好的选择,因为快速排序的期望时间复杂度为O(nlgn),其中隐藏的常系数非常小,快速排序也能像堆排序一样,可以就地排序。
快速排序也是利用分治策略的思想,其过程分为:
- 分解:将一个数组分为两个子数组,一个数组的所有元素都小于或等于数组的主元,另外一个数组则都大于数组的主元。主元是在数组划分之前预先设定的值,一般选择数组最后一个元素作为主元。
- 求解:递归的调用快速排序算法,对分解的两个子数组进行快速排序。当分解的子数组少于两个元素时为递归终止条件。
- 合并:由于快速排序为就地排序,直接在原数组上操作,所以不需要额外的合并操作。
代码
def QuickSort(arr: List[int], p: int, r: int):
# 采用双指针实现
def Partition(arr: List[int], p: int, r: int):
# 确定主元
x = arr[r]
# 定义慢指针
i = p - 1
for j in range(p, r):
# 若小于主元,则慢指针加一,并交换快慢指针所指的值
if arr[j] <= x:
i += 1
arr[i], arr[j] = arr[j], arr[i]
# 将主元放在中间分割位置
arr[i + 1], arr[r] = arr[r], arr[i + 1]
return i + 1
if p < r:
q = Partition(arr, p, r)
QuickSort(arr, p, q - 1)
QuickSort(arr, q + 1, r)
在代码实现中,分解数组为关键部分。这里采用双指针来实现,快指针遍历数组元素,与主元进行比较,当快指针所指元素小于或者等于主元时,慢指针加一,并与快指针所指的元素进行交换,实现了小于或者等于主元的元素都位于数组的前半部分,退出循环后,在将主元与慢指针后一位的元素交换。通过以上操作实现了主元元素将数组分为了左右两个子数组,并且满足了分解要求。
时间复杂度
下面来考虑该算法的时间复杂度,什么时候处于最坏情况呢?从分解这一部分可以看出,在确定主元元素时,我们都是预先将数组最后的元素作为主元,而主元可能将一个数组正好分为数量平均的两个子数组,也可能将一个数组分为一个空数组和一个(n-1)规模的子数组。若每次划分后的两个子数组都是后者这种极不平衡的状态时,就是快速排序处于最坏情况了。我们可以写出最坏情况下,运行时间的递归式:
![](https://i-blog.csdnimg.cn/blog_migrate/49e8b589c60e2ce8d6fd9703587312a0.png#pic_center)
当数组划分的子数组都平均时,即子数组的规模都是原数组的一般,则达到最好情况,运行时间的递归式:
其时间复杂度与归并排序一样,为O(nlgn)。
2. 随机化的快速排序
介绍
为了避免分解数组时,出现极不平衡的状态,我们在选择主元的时候,采用随机抽取数组中的一个元素作为主元,假设数组中每个元素被抽中的概率都相等。通过这种方式,可以让快速排序算法更加趋于平均情况,使得出现极不平衡的情况概率下降。
随机化的快速排序与快速排序在代码上的区别在于主元的随机化选择,除此之外,都一样。
代码
def RandomQuickSort(arr: List[int], p: int, r: int):
def RandomPartition(arr: List[int], p: int, r: int):
# 随机选择主元
index = random.randrange(p, r)
arr[index], arr[r] = arr[r], arr[index]
x = arr[r]
# 定义慢指针
i = p - 1
for j in range(p, r):
# 若小于主元,则慢指针加一,并交换快慢指针所指的值
if arr[j] <= x:
i += 1
arr[i], arr[j] = arr[j], arr[i]
# 将主元放在中间分割位置
arr[i + 1], arr[r] = arr[r], arr[i + 1]
return i + 1
if p < r:
q = RandomPartition(arr, p, r)
RandomQuickSort(arr, p, q - 1)
RandomQuickSort(arr, q + 1, r)
时间复杂度
随机化的快速排序由于随机抽样的加入,在运行时间上采用期望时间复杂度,为O(nlgn)。具体的推导过程见算法导论第二部分7.4 快速排序分析
部分内容引用于算法导论一书