快速排序的核心思想是分治法,通过递归地将问题分解为更小的子问题来解决。
功能模块:
分区操作(Partition):
功能:将数组分为两部分,使得左边的元素都不大于基准值 pivot,右边的元素都不小于基准值 pivot。
实现:通过双指针法(i 和 j)实现分区操作。
递归排序(Recursive Sorting):
功能:对分区后的左右子数组分别进行快速排序。
实现:递归调用快速排序函数,分别处理左区间 [l, j] 和右区间 [j + 1, r]。
基准值选择(Pivot Selection):
功能:选择一个合适的基准值 pivot,以优化排序性能。
实现:常见的选择方法包括:
- 中间值((l + r) // 2)。
- 三数取中法(取左、右、中间三个值的中位数)。
- 随机选择。
终止条件(Base Case):
功能:当数组区间内只有一个元素或没有元素时,停止递归。
实现:通过判断 l >= r 来终止递归。
关键参数:
- 数组(data):需要排序的数组。
- 左端点(l)和右端点(r):当前排序区间的起始和结束索引。
- 基准值(pivot):分区操作的基准值,通常从数组中选择。
- 指针(i 和 j):用于分区操作的左右指针。
基本结构:
(1)数组q[N], 左端点l, 右端点r
(2)确定划分边界x
(3)将 q 分为 <=x 和 >=x 的两个小数组
(4)递归处理两个小数组
def quick_sort(l, r, data): #要排序的数组data,左端点l,右端点r
if l >= r: #表示空或者排序完成
return 0
i, j = l - 1, r + 1 #设置i,j的初值
pivot = data[(l + r) // 2] #划分边界,一般取位于中点的值
while i < j:
while 1:
i += 1 #表示i从左向右移动
if data[i] >= pivot: #若大于等于pivot,则退出。因为要保持左边的数小于pivot
break
while 1:
j -= 1 #表示j从右向左移动
if data[j] <= pivot: #若小于等于pivot,则退出。因为要保持右边的数大于pivot
break
if i < j: #若i<j表示还未排序结束
data[i], data[j] = data[j], data[i] #交换保持i,j位置不变然后交换值。
#交换之后data[i]小于pivot,data[j]大于pivot
quick_sort(l, j, data) #递归处理两个小数组,注意传入的左右端点
quick_sort(j + 1, r, data)
i,j初值
其中,i和j的初值分别是l-1,r+1是为了简化循环逻辑,避免在循环中对边界条件进行额外的检查。具体来说:
如果将 i 和 j 的初始值分别设置为 l 和 r,那么在第一次循环中,需要额外检查 data[l] 和 data[r] 是否已经满足条件。例如:
如果 data[l] >= pivot,则不需要移动 i。
如果 data[r] <= pivot,则不需要移动 j。
这种情况下,代码会变得更加复杂,因为需要在每次循环中处理边界条件。
i,j移动时为什么使用 >= 和 <=
(1)确保分区的正确性
如果使用 > 和 <,分区操作可能会导致以下问题:
基准值 pivot 本身可能无法正确归位。
左侧可能会出现大于 pivot 的元素,右侧可能会出现小于 pivot 的元素。
例如,假设数组中有多个与 pivot 相等的元素,使用 > 和 < 会导致这些元素被错误地分配到两侧,从而破坏分区的正确性。
(2)处理重复元素
使用 >= 和 <= 可以确保与 pivot 相等的元素被正确处理。具体来说:
如果 data[i] == pivot,i 会继续向右移动,直到找到一个大于 pivot 的元素。
如果 data[j] == pivot,j 会继续向左移动,直到找到一个小于 pivot 的元素。
这样可以保证:
左侧所有元素都小于等于 pivot。
右侧所有元素都大于等于 pivot。
与 pivot 相等的元素可以被分配到任意一侧,但不会破坏分区的正确性。