快速排序:从基础到优化
快速排序是计算机科学中最著名的排序算法之一,由C. A. R. Hoare在1960年提出。它是一种分治法策略的应用,广泛用于各种场景,从数据库的查询优化到大数据处理,再到我们日常使用的文件排序。
快速排序的基本原理
快速排序的核心思想是选择一个元素作为基准(pivot),然后将数组分为两部分:一部分包含小于pivot的元素,另一部分包含大于pivot的元素。这个过程称为分区(partitioning)。接着,递归地对这两部分进行快速排序,直到整个数组有序。
时间复杂度和空间复杂度
快速排序的平均时间复杂度为O(n log n),在大多数情况下表现出色。然而,在最坏的情况下,如当数组已经有序或者所有元素相等时,其时间复杂度会退化到O(n^2)。为了避免这种情况,通常会采用随机化或者其他策略来选择pivot。
在空间复杂度方面,快速排序是原地排序算法,它不需要额外的存储空间来创建数组的副本。但是,由于它是递归的,所以它的空间复杂度取决于递归调用栈的深度,最坏情况下为O(n),平均情况下为O(log n)。
稳定性
快速排序不是一个稳定的排序算法。这意味着相等的元素在排序后可能会改变它们原始的顺序。
实现
普通版:快速排序
需要额外的空间复杂度
func QuickSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
pivot := arr[0]
var left []int
var right []int
for i := 1; i < len(arr); i++ {
if arr[i] < pivot {
left = append(left, arr[i])
} else {
right = append(right, arr[i])
}
}
return append(append(QuickSort(left), pivot), QuickSort(right)...)
}
这个版本引入了额外的空间 left、right 存储排序中间数据,我们可以通过原地排序进一步优化。
进阶版:原地快速排序
不需要额外的空间复杂度
func QuickSort(arr []int) {
quickSort(arr, 0, len(arr)-1)
}
func quickSort(arr []int, low, high int) {
if low < high {
// Partition the array by setting the position of the pivot element
pivotIndex := partition(arr, low, high)
// Recursively sort elements before partition and after partition
quickSort(arr, low, pivotIndex-1)
quickSort(arr, pivotIndex+1, high)
}
}
func partition(arr []int, low, high int) int {
pivot := arr[high] // using the last element as the pivot
i := low // place for swapping
for j := low; j < high; j++ {
// If current element is smaller than or equal to pivot
if arr[j] <= pivot {
arr[i], arr[j] = arr[j], arr[i] // swap arr[i] and arr[j]
i++ // increment index of smaller element
}
}
arr[i], arr[high] = arr[high], arr[i] // swap the pivot element with the element at index i
return i // return the partitioning index
}
进阶二:三位取中法
优化 pivot 取数,避免取数不合理导致的时间复杂度上升
func QuickSort(arr []int) {
quickSort(arr, 0, len(arr)-1)
}
func quickSort(arr []int, low, high int) {
if low < high {
// Choose pivot using median-of-three method
pivotIndex := medianOfThree(arr, low, high)
arr[pivotIndex], arr[high] = arr[high], arr[pivotIndex] // Move pivot to end
// Partition the array around the pivot
pivotIndex = partition(arr, low, high)
// Recursively sort elements before partition and after partition
quickSort(arr, low, pivotIndex-1)
quickSort(arr, pivotIndex+1, high)
}
}
func partition(arr []int, low, high int) int {
pivot := arr[high]
i := low
for j := low; j < high; j++ {
if arr[j] <= pivot {
arr[i], arr[j] = arr[j], arr[i]
i++
}
}
arr[i], arr[high] = arr[high], arr[i]
return i
}
func medianOfThree(arr []int, low, high int) int {
mid := low + (high-low)/2
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]
}
return mid
}
进阶三:三项切分法
当有大量相同数据的时候,重复的部分无需排序,可以使用三项切分法进行优化
func QuickSort(arr []int) {
quickSort(arr, 0, len(arr)-1)
}
func quickSort(arr []int, low, high int) {
if low < high {
// Partition the array into three parts
lt, gt := threeWayPartition(arr, low, high)
// Recursively sort elements before lt and after gt
quickSort(arr, low, lt-1)
quickSort(arr, gt+1, high)
}
}
func threeWayPartition(arr []int, low, high int) (int, int) {
pivot := arr[low] // Choose the first element as pivot
lt := low // lt is the index for the last element less than pivot
gt := high // gt is the index for the first element greater than pivot
i := low + 1 // Start from the element next to pivot
for i <= gt {
if arr[i] < pivot {
arr[lt], arr[i] = arr[i], arr[lt]
lt++
i++
} else if arr[i] > pivot {
arr[i], arr[gt] = arr[gt], arr[i]
gt--
} else {
i++
}
}
return lt, gt
}
快速排序的优化
Pivot选择的优化
快速排序的效率在很大程度上取决于pivot的选择。一个好的pivot可以将数组均匀地分成两部分,从而减少递归的深度。常见的优化策略包括随机选择pivot、三数取中法,以及中位数的中位数法。
三向切分的快速排序
当数组中存在大量重复元素时,传统的快速排序可能会进行许多不必要的比较和交换。三向切分的快速排序可以解决这个问题,它将数组分为三部分:小于pivot的元素、等于pivot的元素和大于pivot的元素。这种方法可以显著提高含有大量重复元素的数组的排序效率。
实际应用场景
快速排序由于其高效和易于实现的特点,在许多实际应用中得到了广泛的使用。例如:
- 数据库系统:在数据库管理系统中,快速排序被用于查询优化器中的排序操作。
- 文件系统:操作系统中的文件管理器可能会使用快速排序来对文件和目录进行排序。
- 大数据处理:在处理大规模数据集时,快速排序常用于分布式系统中的排序任务。
补充技术点
除了上述讨论的内容,快速排序还有其他一些值得注意的技术点:
- 尾递归优化:在递归实现中,通过尾递归优化可以减少递归调用栈的深度,从而节省空间。
- 非递归实现:可以使用栈来模拟递归过程,从而将快速排序转换为非递归形式。
- 并行快速排序:在多核处理器上,可以并行地执行快速排序的不同部分,以进一步提高性能。
结论
快速排序是一种非常高效的排序算法,适用于大多数排序任务。通过合理选择pivot和使用三向切分等优化技术,可以进一步提高其性能,特别是在处理有大量重复元素的数组时。然而,由于其不稳定性,它可能不适用于需要保持元素相对顺序的场景。