快速排序
介绍
快速排序是一种非常高效的排序算法,核心思想是分治:通过选择一个基准元素,将数组分割成两个子数组,其中一个子数组的所有元素都小于基准元素,而另一个子数组的所有元素都大于基准元素。然后,递归地对这两个子数组进行排序,最终实现整个数组的有序排列。
原理
快速排序算法的基本步骤:
- 选择一个基准元素(通常是数组中的第一个元素)。
- 将数组中小于基准元素的元素放在基准元素的左边,大于基准元素的元素放在右边。这一步通常称为分区操作。
- 递归地对基准元素左右两侧的子数组进行快速排序。
- 不断重复以上步骤,直到整个数组变为有序。
假设要对数组 [7, 0, 9, 5, 6, 8, 7, 3, 1, 2] 进行排序:
第一步:
- 先选择一个元素做为基准元素(pivot),这里选择第一个元素。
- 再设置指针 P 标记基准元素所在位置。
- 最后设置两个指针分别指向数组最左侧和最右侧:
- 指针 L 指向数组最左侧;
- 指针 R 指向数组最右侧。
如下图:
第二步:
-
将指针 R 指向的元素与基准元素 pivot 进行比较。
-
如果指针 R 指向的元素大于 pivot:
- 指针 R 向左移动一个位置;
- 循环此步骤,直至指针 R 指向的元素不大于 pivot。
-
如果指针 R 指向的元素不大于 pivot:
- 将指针 R 指向的元素填入指针 P 指向的位置;
- 指针 L 向右移动一个位置;
- 指针 P 移动到指针 R 的位置;
- 进入 【第三步】。
当前,pivot 等于 7,指针 R 指向的元素为 2,7 > 2,因此数组情况变为:
第三步:
- 将指针 L 指向的元素与基准元素 pivot 进行比较。
- 如果指针 L 指向的元素小于 pivot:
- 指针 L 向右移动一个位置;
- 循环此步骤,直至指针 L 指向的元素不小于 pivot。
- 如果指针 L 指向的元素不小于 pivot:
- 将指针 L 指向的元素填入指针 P 指向的位置;
- 指针 R 向左移动一个位置;
- 指针 P 移动到指针 L 的位置;
- 返回【第二步】。
当前,pivot 等于 7,指针 L 指向的元素为 0,7 > 0,数组情况变为:
此时,pivot 等于 7,指针 L 指向的元素为 9,7 < 9,数组情况变为:
重返【第二步】…
第四步:
当指针 L 和指针 R 交叉后:
将 pivot 的值填入指针 P 指向的位置:
可以发现:
- 指针 P 左侧的元素都比指针 P 指向的元素小;
- 指针 P 右侧的元素都比指针 P 指向的元素大。
这是一个特殊情况,通常情况下,每次分区后的子数组都是无序的,都需要进一步排序。
做法是将 [2, 0, 1, 5, 6, 3] 看成一个新的数组,重新设置 pivot 的值(pivot=2),结构如下:
然后按照之前的四步对这个子数组进行处理,直到整个数组都变为有序的即可。动态过程如下:

代码
/**
* 快速排序算法的实现
*
* @author 孤诣
*/
public class QuickSort {
public static void main(String[] args) {
// 数据准备
int[] dataArray = {7, 0, 9, 5, 6, 8, 4, 3, 1, 2};
System.out.println("排序前: " + Arrays.toString(dataArray));
// 利用快速排序算法进行排序
quickSort(dataArray, 0, dataArray.length - 1);
System.out.println("排序后: " + Arrays.toString(dataArray));
}
/**
* 快速排序算法实现
*
* @param source - 源数组
* @param startIndex - 排序起始位置
* @param endIndex - 排序结束位置
*/
public static void quickSort(int[] source, int startIndex, int endIndex) {
if (startIndex > endIndex) {
return;
}
// 分区
int lastPivotIndex = partition(source, startIndex, endIndex);
// 上一个基准元素位置已经确定, 后续排序就不需要考虑上一个基准元素了
quickSort(source, startIndex, (lastPivotIndex - 1));
quickSort(source, (lastPivotIndex + 1), endIndex);
}
/**
* 分区, 本来操作的基准元素的最终位置将被确定。
*
* @param source - 源数组
* @param startIndex - 排序起始位置
* @param endIndex - 排序结束位置
* @return 本轮操作完成后,基准元素所在位置
*/
private static int partition(int[] source, int startIndex, int endIndex) {
// 基准元素
int pivot = source[startIndex];
// 初始化相关指针
int pivotIndex = startIndex;
int leftIndex = startIndex;
int rightIndex = endIndex;
while (leftIndex < rightIndex) {
// 右侧元素小于基准元素
while (leftIndex < rightIndex) {
if (source[rightIndex] < pivot) {
source[leftIndex] = source[rightIndex];
pivotIndex = rightIndex;
leftIndex++;
break;
}
rightIndex--;
}
// 左侧元素小于基准元素
while (leftIndex < rightIndex) {
if (source[leftIndex] > pivot) {
source[rightIndex] = source[leftIndex];
pivotIndex = leftIndex;
rightIndex--;
break;
}
leftIndex++;
}
}
source[pivotIndex] = pivot;
return pivotIndex;
}
}
运行效果:
评估
- 时间复杂度:
- 最好情况:O(nlogn)
- 最坏情况:O(n^2):
- 平均时间复杂度:O(nlogn)
- 空间复杂度:
- 平均空间复杂度:O(logn)
- 最坏空间复杂度:O(n)
- 稳定性:不稳定
有的文章认为快速排序的空间复杂度为 O(1),因为快速排序可以直接在原数组上进行操作。
出于以下原因,笔者并不认为快速排序的空间复杂度为 O(1):
- 相关文章实现该算法的方式均为递归,而递归本身需要额外占用内存;
- 笔者了解到的非递归实现快速排序方式需要借助 “栈”,仍然要占用额外内存。
快速排序是一种高效的排序算法,它的平均时间复杂度较低,适用于大规模数据的排序。
然而,在最坏的情况下(即每次划分的基准都是当前子数组中最大或最小的元素),快速排序的时间复杂度会变得很高,此时快速排序会退化为冒泡排序。此外,快速排序是不稳定的排序算法,所以在需要稳定排序的场合,我们应该选择其他算法。
总的来说,快速排序适用于大多数排序场景,但需要注意最坏情况下的性能问题。

973

被折叠的 条评论
为什么被折叠?



