1. 概述
在本教程中,我们将详细探讨 QuickSort 算法,重点介绍其 Java 实现。
我们还将讨论它的优缺点,然后分析它的时间复杂度。
2. 快速排序算法
快速排序是一种排序算法,它利用了分而治之的原则。 它具有平均 O(n log n) 复杂度,是最常用的排序算法之一,尤其是对于大数据量。
重要的是要记住,快速排序不是一个稳定的算法。 稳定排序算法是一种算法,其中具有相同值的元素在排序输出中的显示顺序与它们在输入列表中的显示顺序相同。
输入列表由称为 pivot 的元素分为两个子列表;一个子列表包含小于数据透视的元素,另一个子列表包含大于数据透视的元素。对每个子列表重复此过程。
最后,所有排序的子列表合并以形成最终输出。
2.1. 算法步骤
我们从列表中选择一个元素,称为透视。我们将使用它来将列表划分为两个子列表。
我们对枢轴周围的所有元素进行重新排序——值较小的元素放在它之前,所有大于枢轴的元素放在它之后。完成此步骤后,枢轴处于其最终位置。这是重要的分区步骤。
我们将上述步骤递归应用于枢轴左侧和右侧的子列表。
正如我们所看到的,快速排序自然是一种递归算法,就像每一种分而治之的方法一样。
让我们举一个简单的例子,以便更好地理解这个算法。
Arr[] = {5, 9, 4, 6, 5, 3}
假设我们选择 5 作为简单起见的枢轴
我们首先将所有小于 5 的元素放在数组的第一个位置:{3, 4, 5, 6, 5, 9}
然后,我们将对左侧子数组 {3,4} 重复此操作,以 3 为枢轴
没有少于 3 的元素
我们对枢轴右侧的子数组应用快速排序,即{4}
此子数组仅由一个排序元素组成
我们继续处理原始数组的右侧部分 {6, 5, 9},直到我们得到最终的有序数组
2.2. 选择最佳枢轴
QuickSort 的关键点是选择最佳透视。当然,中间元素是最好的,因为它会将列表分成两个相等的子列表。
但是从无序列表中找到中间元素既困难又耗时,这就是为什么我们将第一个元素、最后一个元素、中位数或任何其他随机元素作为枢轴。
3. 在 Java 中实现
第一种方法是 quickSort(),它将要排序的数组、第一个和最后一个索引作为参数。首先,我们检查索引,只有在仍有元素需要排序时才继续。
我们获取排序透视的索引,并使用它以递归方式调用 partition() 方法,其参数与 quickSort() 方法相同,但索引不同:
public void quickSort(int arr[], int begin, int end) {
if (begin < end) {
int partitionIndex = partition(arr, begin, end);
quickSort(arr, begin, partitionIndex-1);
quickSort(arr, partitionIndex+1, end);
}
}
让我们继续使用 partition() 方法。为简单起见,此函数将最后一个元素作为枢轴。然后,检查每个元素,如果其值较小,则在枢轴之前交换它。
在分区结束时,所有小于枢轴的元素都位于其左侧,所有大于枢轴的元素都位于其右侧。枢轴处于其最终排序位置,函数返回以下位置:
private int partition(int arr[], int begin, int end) {
int pivot = arr[end];
int i = (begin-1);
for (int j = begin; j < end; j++) {
if (arr[j] <= pivot) {
i++;
int swapTemp = arr[i];
arr[i] = arr[j];
arr[j] = swapTemp;
}
}
int swapTemp = arr[i+1];
arr[i+1] = arr[end];
arr[end] = swapTemp;
return i+1;
}
4. 算法分析
4.1. 时间复杂度
在最好的情况下,算法会将列表分成两个大小相等的子列表。因此,完整 n 大小列表的第一次迭代需要 O(n)。对剩余的两个包含 n/2 个元素的子列表进行排序,每个子列表需要 2*O(n/2)。 因此,QuickSort 算法的复杂度为 O(n log n)。
在最坏的情况下,算法在每次迭代中只选择一个元素,所以 O(n) + O(n-1) + … + O(1),等于O(n2).
平均而言,快速排序具有 O(n log n) 的复杂度,因此适用于大数据量。
4.2. 快速排序与 归并排序
让我们讨论在哪些情况下我们应该选择 快速排序而不是 归并排序。
尽管 快速排序和 归并排序的平均时间复杂度均为 O(n log n),但 快速排序是首选算法,因为它具有 O(log(n)) 空间复杂度。另一方面,归并排序需要 O(n) 额外的存储空间,这使得数组非常昂贵。
快速排序需要访问不同的索引才能进行操作,但这种访问在链表中是无法直接实现的,因为没有连续的块;因此,要访问一个元素,我们必须从链表的开头遍历每个节点。此外,归并排序的实现没有为 LinkedList 提供额外的空间。
在这种情况下,通常首选增加Quicksort和Mergesort的开销。
5. 结论
快速排序是一种优雅的排序算法,在大多数情况下非常有用。
它通常是一种“就地”算法,平均时间复杂度为 O(n log n)。