快速排序(Quick Sort)凭借 O(n log n) 的平均时间复杂度、原地排序的空间优势,以及实际应用中极高的运行效率,成为处理大规模数据排序的 "首选方案"。
一、快速排序的核心原理
核心是 **"选基准、分区间、递归排序"**------ 通过一趟排序将数组分为 "小于基准" 和 "大于基准" 的两个子区间,再对两个子区间重复该过程,直到所有子区间长度为 1(天然有序)。
1.1 三大核心步骤(以升序排序为例)
假设待排序数组为nums
,排序区间为[left, right]
(初始为[0, len(nums)-1]
),完整流程如下:
- 选择基准(Pivot):从数组中选一个元素作为 "基准"(如区间第一个元素、最后一个元素、中间元素,或随机元素);
- 分区(Partition) :将数组重新排列,使所有小于基准 的元素移到基准左侧,所有大于基准 的元素移到基准右侧(等于基准的元素可左可右),最终基准落在 "正确的排序位置" 上(记为
pivot_idx
); - 递归排序 :对基准左侧的子区间
[left, pivot_idx-1]
和右侧的子区间[pivot_idx+1, right]
分别重复步骤 1-2,直到子区间长度为 0 或 1(无需排序)。
1.2 关键:分区(Partition)过程的实现
分区是快速排序的 "灵魂",直接决定算法的效率。这里以 " Lomuto 分区法 "(简单易懂,适合入门)为例,拆解分区过程:
- 目标:将基准(假设选区间最后一个元素
nums[right]
)放到正确位置,左侧元素≤基准,右侧元素≥基准; - 步骤:
- 初始化 "小于基准区间的右边界"
i = left - 1
(初始时 "小于基准的区间" 为空); - 遍历区间
[left, right-1]
,对每个元素nums[j]
:- 若
nums[j] ≤ 基准
:将i
加 1,交换nums[i]
和nums[j]
(扩展 "小于基准的区间");
- 若
- 遍历结束后,
i+1
即为基准的正确位置,交换nums[i+1]
和nums[right]
,返回i+1
(基准索引)。
- 初始化 "小于基准区间的右边界"
分区过程示例(数组[3, 1, 4, 1, 5]
,基准选最后一个元素5
):
- 初始:
left=0, right=4
,基准pivot=5
,i=-1
; - 遍历
j=0
(nums[j]=3
):3≤5 →i=0
,交换nums[0]
和nums[0]
(无变化); - 遍历
j=1
(nums[j]=1
):1≤5 →i=1
,交换nums[1]
和nums[1]
(无变化); - 遍历
j=2
(nums[j]=4
):4≤5 →i=2
,交换nums[2]
和nums[2]
(无变化); - 遍历
j=3
(nums[j]=1
):1≤5 →i=3
,交换nums[3]
和nums[3]
(无变化); - 遍历结束:交换
nums[4]
(i+1=4
)和nums[4]
(基准),返回4
(基准5
已在正确位置)。
1.3 时间复杂度与空间复杂度
场景 | 时间复杂度 | 空间复杂度 | 说明 |
---|---|---|---|
平均情况 | O(n log n) | O(log n) | 每次分区将数组分为两个近似等长的子区间,递归深度为 log n(递归栈空间) |
最坏情况 | O(n²) | O(n) | 数组已有序 / 逆序,每次分区选到 "最大 / 最小元素" 为基准,递归深度为 n |
最好情况 | O(n log n) | O(log n) | 每次分区选到 "中位数" 为基准,子区间完全等长 |
原地排序优化 | - | O(1) | 用迭代代替递归(手动维护栈),消除递归栈空间消耗 |
**为什么实际应用中快速排序比归并排序快?**归并排序时间复杂度稳定为 O (n log n),但需要 O (n) 的额外空间,且存在大量数组拷贝操作;快速排序是原地排序(除递归栈外无额外空间),缓存命中率更高,实际运行时常数项更小,因此处理大规模数据时更快。
二、基础实现: Lomuto 分区法与 Hoare 分区法
快速排序的分区实现有两种主流方式:Lomuto 分区法(简单但效率略低)和 Hoare 分区法(高效,Tony Hoare 原版实现)。下面分别给出代码实现,并对比两者差异。
2.1 Lomuto 分区法(选最后一个元素为基准)
适合入门,逻辑简单,但交换次数较多,且最坏情况(有序数组)下效率低。
python
def quick_sort_lomuto(nums, left, right):
"""
快速排序(Lomuto分区法):升序排序
:param nums: 待排序数组
:param left: 排序区间左边界
:param right: 排序区间右边界
"""
# 递归终止条件:区间长度≤1(无需排序)
if left >= right:
return
# 1. 分区:返回基准的正确索引
pivot_idx = partition_lomuto(nums, left, right)
# 2. 递归排序左子区间和右子区间
quick_sort_lomuto(nums, left, pivot_idx - 1)
quick_sort_lomuto(nums, pivot_idx + 1, right)
def partition_lomuto(nums, left, right):
"""Lomuto分区:选nums[right]为基准"""
pivot = nums[right] # 基准元素
i = left - 1 # 小于基准区间的右边界(初始为空)
# 遍历区间[left, right-1],将小于基准的元素移到左侧
for j in range(left, right):
if nums[j] <= pivot:
i += 1
# 交换nums[i]和nums[j],扩展小于基准的区间
nums[i], nums[j] = nums[j], nums[i]
# 将基准移到正确位置(i+1)
nums[i + 1], nums[right] = nums[right], nums[i + 1]
return i + 1 # 返回基准索引
# 测试案例
if __name__ == "__main__":
test_nums = [3, 1, 4, 1, 5, 9, 2, 6]
quick_sort_lomuto(test_nums, 0, len(test_nums) - 1)
print("Lomuto分区法排序结果:", test_nums) # 输出:[1, 1, 2, 3, 4, 5, 6, 9]
2.2 Hoare 分区法(选第一个元素为基准)
Hoare 分区法是快速排序的原版实现,通过 "双指针从两端向中间扫描" 实现分区,交换次数更少,效率比 Lomuto 高约 30%,但逻辑稍复杂。
核心逻辑:
-
左指针
i
从left
开始向右移,直到找到nums[i] > 基准
; -
右指针
j
从right
开始向左移,直到找到nums[j] < 基准
; -
交换
nums[i]
和nums[j]
,重复上述步骤,直到i >= j
; -
最后交换
nums[left]
(基准)和nums[j]
,使基准落在正确位置。pythondef quick_sort_hoare(nums, left, right): """ 快速排序(Hoare分区法):升序排序 :param nums: 待排序数组 :param left: 排序区间左边界 :param right: 排序区间右边界 """ if left >= right: return pivot_idx = partition_hoare(nums, left, right) quick_sort_hoare(nums, left, pivot_idx - 1) quick_sort_hoare(nums, pivot_idx + 1, right) def partition_hoare(nums, left, right): """Hoare分区:选nums[left]为基准""" pivot = nums[left] # 基准元素 i = left # 左指针(从左向右扫) j = right # 右指针(从右向左扫) while i < j: # 1. 右指针向左移,找到第一个小于基准的元素(注意:必须先移动右指针) while i < j and nums[j] >= pivot: j -= 1 # 2. 左指针向右移,找到第一个大于基准的元素 while i < j and nums[i] <= pivot: i += 1 # 3. 交换两个指针指向的元素 nums[i], nums[j] = nums[j], nums[i] # 4. 将基准移到正确位置(i=j处) nums[left], nums[j] = nums[j], nums[left] return j # 返回基准索引 # 测试案例 if __name__ == "__main__": test_nums = [3, 1, 4, 1, 5, 9, 2, 6] quick_sort_hoare(test_nums, 0, len(test_nums) - 1) print("Hoare分区法排序结果:", test_nums) # 输出:[1, 1, 2, 3, 4, 5, 6, 9]
关键注意点 :Hoare 分区法中,必须先移动右指针 再移动左指针,否则可能导致基准位置错误(例如数组
[2, 1, 3]
,若先移左指针会跳过1
,最终基准无法正确归位)。三、快速排序的三大优化
基础版快速排序在某些场景下存在明显缺陷(如有序数组下时间复杂度退化为 O (n²)),通过以下三大优化,可将其性能提升至 "工业级" 水平。
3.1 优化 1:随机选择基准(避免最坏情况)
问题根源 :基础版快速排序固定选 "第一个 / 最后一个元素" 为基准,若数组已有序 / 逆序,每次分区都会选到 "最大 / 最小元素",导致子区间长度为
n-1
和0
,递归深度变为n
,时间复杂度退化为 O (n²)。优化方案 :随机选择基准 ------ 从当前区间[left, right]
中随机选一个元素作为基准,再与 "第一个 / 最后一个元素" 交换,后续分区逻辑不变。效果:随机基准能极大降低 "选到极端值" 的概率,使平均时间复杂度稳定在 O (n log n),最坏情况几乎不会出现。pythonimport random def quick_sort_random_pivot(nums, left, right): """快速排序(随机基准优化)""" if left >= right: return # 优化1:随机选择基准,与nums[right]交换(后续用Lomuto分区法) random_idx = random.randint(left, right) nums[random_idx], nums[right] = nums[right], nums[random_idx] # 分区+递归排序 pivot_idx = partition_lomuto(nums, left, right) quick_sort_random_pivot(nums, left, pivot_idx - 1) quick_sort_random_pivot(nums, pivot_idx + 1, right) # 测试有序数组(基础版会退化,优化版无压力) if __name__ == "__main__": sorted_nums = [1, 2, 3, 4, 5, 6, 7, 8] quick_sort_random_pivot(sorted_nums, 0, len(sorted_nums)-1) print("随机基准优化排序结果:", sorted_nums) # 输出:[1, 2, 3, 4, 5, 6, 7, 8]
3.2 优化 2:小数组用插入排序(减少递归开销)
问题根源 :快速排序的递归调用存在函数栈开销,对于长度较小的子区间(如n ≤ 10
),递归的 "overhead"(额外开销)甚至超过排序本身的时间,而插入排序在小数组上的实际效率更高(插入排序对近乎有序的数组也有优势)。优化方案 :当子区间长度right - left + 1 ≤ 阈值
(通常取 10~20)时,改用插入排序;仅对长度大于阈值的子区间继续用快速排序。效果:减少约 50% 的递归调用次数,整体效率提升 10%~20%。
python
def insertion_sort(nums, left, right):
"""插入排序:对nums[left..right]进行升序排序"""
for i in range(left + 1, right + 1):
key = nums[i] # 当前待插入元素
j = i - 1
# 找到key的插入位置
while j >= left and nums[j] > key:
nums[j + 1] = nums[j]
j -= 1
nums[j + 1] = key
def quick_sort_hybrid(nums, left, right):
"""快速排序(混合插入排序优化)"""
# 优化2:小数组用插入排序(阈值设为10)
if right - left + 1 <= 10:
insertion_sort(nums, left, right)
return
# 优化1:随机基准
random_idx = random.randint(left, right)
nums[random_idx], nums[right] = nums[right], nums[random_idx]
pivot_idx = partition_lomuto(nums, left, right)
quick_sort_hybrid(nums, left, pivot_idx - 1)
quick_sort_hybrid(nums, pivot_idx + 1, right)
# 测试包含小数组的场景
if __name__ == "__main__":
test_nums = [5, 3, 8, 6, 2, 7, 1, 4, 0, 9, 11, 10]
quick_sort_hybrid(test_nums, 0, len(test_nums)-1)
print("混合插入排序优化结果:", test_nums) # 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
3.3 优化 3:三数取中(解决重复元素问题)
问题根源 :若数组中存在大量重复元素(如[2, 1, 2, 2, 3, 2]
),基础版快速排序会将 "等于基准的元素" 全部归到一侧,导致子区间长度失衡,效率下降;此外,随机基准在重复元素较多时,仍可能选到 "极端值"。优化方案 :三数取中 ------ 从区间的 "左端点、中点、右端点" 三个元素中选 "中位数" 作为基准,确保基准尽可能处于区间中间位置。效果:在重复元素较多的场景下,子区间长度更均衡,时间复杂度稳定在 O (n log n),同时避免了随机基准的 "不确定性"。
python
def median_of_three(nums, left, right):
"""三数取中:返回left、mid、right三个位置中值的索引"""
mid = left + (right - left) // 2 # 避免溢出
# 排序三个位置的元素,使nums[left] ≤ nums[mid] ≤ nums[right]
if nums[left] > nums[mid]:
nums[left], nums[mid] = nums[mid], nums[left]
if nums[left] > nums[right]:
nums[left], nums[right] = nums[right], nums[left]
if nums[mid] > nums[right]:
nums[mid], nums[right] = nums[right], nums[mid]
# 返回中位数索引(mid),并与nums[right-1]交换