排序算法是计算机科学的基础,而快速排序(Quicksort)以其平均性能优越和原地排序特性,成为最受欢迎的排序方法之一。许多现代编程语言的标准库(如C的qsort、Java的Arrays.sort)都采用快速排序或其变体。然而,快速排序并非没有陷阱:在最坏情况下,它可能退化为O(n²)的时间复杂度,且对基准选择敏感。本文将带您从原理到优化,再到实际应用,全面掌握快速排序。
快速排序基于分治策略,其基本步骤为:
分区的结果是基准元素最终位于其正确位置,这保证了每次递归至少将一个元素排好。
最简单的分区算法是Lomuto方案,它使用一个指针遍历数组,将小于基准的元素交换到左侧。以下是用Python实现的代码:
def quicksort_lomuto(arr, low, high):
if low < high:
pivot_index = partition_lomuto(arr, low, high)
quicksort_lomuto(arr, low, pivot_index - 1)
quicksort_lomuto(arr, pivot_index + 1, high)
def partition_lomuto(arr, low, high):
pivot = arr[high] # 选择最后一个元素作为基准
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
# 测试
test_arr = [3, 7, 8, 5, 2, 1, 9, 5, 4]
quicksort_lomuto(test_arr, 0, len(test_arr)-1)
print(test_arr) # 输出 [1, 2, 3, 4, 5, 5, 7, 8, 9]Lomuto分区代码简洁,但每次交换后i指针移动,当数组包含大量重复元素时,性能会下降。更常用的分区方法是Hoare分区。
Hoare分区使用两个指针从两端向中间扫描,避免了Lomuto的许多冗余交换,平均性能更好:
def quicksort_hoare(arr, low, high):
if low < high:
pivot_index = partition_hoare(arr, low, high)
quicksort_hoare(arr, low, pivot_index)
quicksort_hoare(arr, pivot_index + 1, high)
def partition_hoare(arr, low, high):
pivot = arr[low] # 选择第一个元素作为基准
i = low - 1
j = high + 1
while True:
i += 1
while arr[i] < pivot:
i += 1
j -= 1
while arr[j] > pivot:
j -= 1
if i >= j:
return j
arr[i], arr[j] = arr[j], arr[i]
# 测试
test_arr = [3, 7, 8, 5, 2, 1, 9, 5, 4]
quicksort_hoare(test_arr, 0, len(test_arr)-1)
print(test_arr) # 输出 [1, 2, 3, 4, 5, 5, 7, 8, 9]注意,Hoare分区返回的基准位置是最终分界点,递归调用时需要包含该位置。
快速排序的最坏情况发生在每次分区极不平衡时,例如数组已经有序且基准选择第一个或最后一个元素。常见的优化包括:
随机选取基准可以概率性地避免最坏情况,使期望时间复杂度保持O(n log n):
import random
def partition_random(arr, low, high):
rand_index = random.randint(low, high)
arr[low], arr[rand_index] = arr[rand_index], arr[low] # 将随机基准移到首位
return partition_hoare(arr, low, high) # 复用Hoare分区取首、中、尾三个元素的中间值作为基准,能有效应对部分有序数据:
def median_of_three(arr, low, high):
mid = (low + high) // 2
# 简单的排序:确保 arr[low] <= arr[mid] <= arr[high]
if arr[low] > arr[mid]:
arr[low], arr[mid] = arr[mid], arr[low]
if arr[low] > arr[high]:
arr[low], arr[high] = arr[high], arr[low]
if arr[mid] > arr[high]:
arr[mid], arr[high] = arr[high], arr[mid]
# 将中值作为基准放到 low+1 位置(便于分区)
arr[low+1], arr[mid] = arr[mid], arr[low+1]
return arr[low+1] # 返回基准值当子数组规模较小时(如小于10),插入排序的常数开销更低,可以提前终止递归:
def insertion_sort(arr, low, high):
for i in range(low+1, high+1):
key = arr[i]
j = i - 1
while j >= low and arr[j] > key:
arr[j+1] = arr[j]
j -= 1
arr[j+1] = key
def quicksort_optimized(arr, low, high, cutoff=10):
if high - low < cutoff:
insertion_sort(arr, low, high)
return
pivot_index = partition_hoare(arr, low, high)
quicksort_optimized(arr, low, pivot_index, cutoff)
quicksort_optimized(arr, pivot_index+1, high, cutoff)当数组包含大量重复元素时,传统快速排序会进行许多不必要的交换。三路快排(3-way Quicksort)将数组分为小于、等于、大于基准的三部分,时间复杂度降为O(n log n)到O(n)之间(当所有元素相等时)。
def quicksort_3way(arr, low, high):
if low >= high:
return
# 选择基准并分区
pivot = arr[low]
lt = low # arr[low..lt-1] < pivot
gt = high # arr[gt+1..high] > pivot
i = low + 1
while i <= gt:
if arr[i] < pivot:
arr[lt], arr[i] = arr[i], arr[lt]
lt += 1
i += 1
elif arr[i] > pivot:
arr[i], arr[gt] = arr[gt], arr[i]
gt -= 1
else:
i += 1
# 递归排序小于和大于部分
quicksort_3way(arr, low, lt-1)
quicksort_3way(arr, gt+1, high)
# 测试重复元素
test_arr = [5, 3, 5, 2, 5, 1, 5, 4, 5]
quicksort_3way(test_arr, 0, len(test_arr)-1)
print(test_arr) # 输出 [1, 2, 3, 4, 5, 5, 5, 5, 5]在实际工程中,快速排序常被用于数据库的索引构建和大数据排序。例如,在Hadoop或Spark的排序阶段,快速排序被用作本地排序的核心算法。此外,许多语言的标准库都采用了混合排序策略:当数据规模大时使用快速排序,规模小时切换插入排序,并利用三路快排处理重复数据。
下面是一个模拟数据库索引排序的示例:对记录按ID排序,同时保持其他字段的关联性:
class Record:
def __init__(self, id, name):
self.id = id
self.name = name
def __repr__(self):
return f'Record({self.id}, {self.name})'
def partition_records(records, low, high):
pivot = records[high].id
i = low - 1
for j in range(low, high):
if records[j].id <= pivot:
i += 1
records[i], records[j] = records[j], records[i]
records[i+1], records[high] = records[high], records[i+1]
return i + 1
def quicksort_records(records, low, high):
if low < high:
pi = partition_records(records, low, high)
quicksort_records(records, low, pi-1)
quicksort_records(records, pi+1, high)
# 测试
records = [Record(3, 'Alice'), Record(1, 'Bob'), Record(2, 'Charlie')]
quicksort_records(records, 0, len(records)-1)
print(records) # 输出 [Record(1, Bob), Record(2, Charlie), Record(3, Alice)]为了直观展示优化效果,我们对10000个随机整数进行测试:
快速排序的核心在于分区,而选择合适的基准和分区策略是性能关键。通过随机化、三数取中、小数组切换插入排序以及三路分区,我们可以将快速排序打造成一个鲁棒的通用排序工具。希望本文的深入分析和代码示例能帮助您在实际项目中高效应用快速排序。
专注技术分享与实战