[数据结构·排序] 快速排序
快速排序 (Quicksort)
快速排序(Quicksort)是一种非常高效的、基于比较的、就地(in-place)排序算法。它由东尼·霍尔(Tony Hoare)在1959年发明,是目前使用最广泛的排序算法之一。
核心思想:分治 (Divide and Conquer)
快速排序的核心思想是“分治法”。它将一个大数组不断地分割成越来越小的子数组,然后分别对子数组进行排序,最终使得整个数组有序。
这个过程主要分为三步:
- 选择基准 (Pivot): 从数组中挑选一个元素,这个元素被称为“基准”或“枢轴”。
- 分区 (Partition): 重新排列数组。将所有小于基准的元素移动到基准的左侧,所有大于基准的元素移动到基准的右侧(等于的可以放在任意一侧)。完成分区后,这个基准元素就处在它在排序后数组中的最终位置。
- 递归 (Recurse): 递归地对基准左侧的子数组和基准右侧的子数组重复上述过程。
当一个子数组的大小为 0 或 1 时,它自然就是有序的,递归就停止了。
算法步骤详解
假设我们要对数组 arr 从索引 low 到 high 的部分进行排序:
- 选择基准 (Pivot):
- 选择一个基准
pivot。最简单的策略是选择arr[high](最后一个元素)作为基准。其他策略还有选择第一个元素、中间元素或随机选择一个元素(随机选择可以有效避免最坏情况)。
- 选择一个基准
- 分区 (Partition):
- 这是快速排序最关键的一步。目标是找到基准
pivot在排好序后应该在的“正确位置”pi。 - 我们使用一个指针
i来追踪“小于pivot”区域的边界。 - 遍历从
low到high-1的所有元素(j)。 - 如果
arr[j]小于pivot,我们就把i向右移动一位,并交换arr[i]和arr[j]。这相当于把小的元素arr[j]扔到了“小于”区域。 - 遍历结束后,
[low...i]区域的所有元素都小于pivot。 - 最后,我们将基准
pivot(即arr[high])与arr[i+1]交换。 - 现在,
arr[i+1]就是基准pivot,它左边的都比它小,右边的都比它大。我们返回这个索引pi = i + 1。
- 这是快速排序最关键的一步。目标是找到基准
- 递归 (Recurse):
- 得到了基准的最终位置
pi后,原问题被分成了两个子问题: - 递归调用
quickSort(arr, low, pi - 1)来排序左侧的子数组。 - 递归调用
quickSort(arr, pi + 1, high)来排序右侧的子数组。
- 得到了基准的最终位置
性能分析
时间复杂度
- 平均情况 (Average Case): $O(n \log n)$
- 在平均情况下,每次分区都能把数组大致分成两个大小相近的子数组。解决一个大小为 $n$ 的问题需要 $O(n)$ 的分区时间,和解决两个大小为 $n/2$ 的子问题的时间。
- 最坏情况 (Worst Case): $O(n^2)$
- 当数组已经完全有序(或逆序),并且你每次都选择第一个或最后一个元素作为基准时,就会发生最坏情况。
- 这时,每次分区都会产生一个 $0$ 个元素的子数组和一个 $n-1$ 个元素的子数组。递归树会变得极度不平衡,深度为 $n$,导致总时间复杂度变为 $O(n^2)$。
- (解决方法:随机选择基准,或者使用“三数取中”法。)
空间复杂度
- 平均情况: $O(\log n)$
- 空间复杂度主要由递归调用栈的深度决定。平均情况下,递归树的深度为 $O(\log n)$。
- 最坏情况: $O(n)$
- 在最坏情况下(如上所述),递归树的深度为 $O(n)$。
稳定性
- 快速排序是一种 不稳定 的排序算法。
- 在分区过程中,相等元素的相对顺序可能会被改变。例如,数组
[5a, 3, 5b, 2](5a和5b值相等但为了区分做了标记),如果选择3作为基准,5a和5b的顺序不会变。但如果选择2,5a和5b的顺序也不会变。但如果选择5a作为基准,5b可能会被交换到5a的前面。
C++ 实现
下面是使用 C++ (std::vector) 实现的快速排序,采用了 Lomuto 分区方案(Lomuto partition scheme),即选择最后一个元素作为基准。
1 |
|
1 |
|
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Welcome To My-Blog!
评论
