
快速排序(quicksort)是一种高效的、基于比较的排序算法,其核心思想是“分治法”(divide and conquer)。它通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,最终使整个数据序列有序。
快速排序的基本步骤如下:
在实现快速排序时,尤其是在处理递归边界和分区逻辑时,很容易引入细微的错误,导致排序结果不正确或程序崩溃。以下是一个常见的、存在问题的快速排序实现示例:
public class QuickSortOriginal {
public static void quickSort(int[] s) {
quickSortSub(s, 0, s.length - 1);
}
private static void quickSortSub(int[] s, int a, int b) {
// 原始条件:如果子数组长度大于1
if(b-a > 1) {
int point = partition(s, a, b);
quickSortSub(s, a, point - 1);
quickSortSub(s, point + 1, b);
}
}
private static int partition(int[] s, int a, int b) {
int pivot = s[b]; // 选择最右侧元素作为基准
int left = a;
int right = b-1;
while(left < right) {
while(s[left] < pivot) {
left++;
}
while(s[right] > pivot) {
right--;
}
if(left < right) {
int tmp = s[left];
s[left] = s[right];
s[right] = tmp;
}
}
// 基准元素放置
s[b] = s[left];
s[left] = pivot;
return left;
}
public static void main(String[] args) {
int[] arr = {85, 10, 24, 63, 45, 27, 100, 31, 96, 50, 40, 23, 49, 96, 120, 105, 13, 5, 42, 69, 22, 12};
quickSort(arr);
for (int i: arr) System.out.print(i + ", ");
System.out.println("");
}
}上述代码在某些情况下能够正常工作,但在面对特定输入时会产生不正确的排序结果。其主要问题点在于:
quickSortSub 中的递归终止条件:
立即学习“Java免费学习笔记(深入)”;
partition 方法中的 while 循环条件:
基准元素 pivot 的最终放置:
针对上述问题,我们可以对快速排序算法进行如下优化和修正:
调整 quickSortSub 的递归终止条件:
增强 partition 方法的健壮性:
精确放置基准元素 pivot:
以下是修正后的快速排序实现代码:
public class QuickSort {
public static void quickSort(int[] s) {
if (s == null || s.length == 0) {
return;
}
quickSortSub(s, 0, s.length - 1);
}
private static void quickSortSub(int[] s, int a, int b) {
// 修正1: 当子数组至少包含两个元素时才进行分区
if (a < b) {
int point = partition(s, a, b);
// 修正2: 递归调用前检查子数组的有效性
quickSortSub(s, a, point - 1);
quickSortSub(s, point + 1, b);
}
}
private static int partition(int[] s, int a, int b) {
int pivot = s[b]; // 选择最右侧元素作为基准
int left = a;
int right = b - 1;
while (left <= right) { // 修正3: left <= right 确保所有元素被检查
// 修正4: 确保left指针不会越界且找到大于等于pivot的元素
while (left <= right && s[left] < pivot) {
left++;
}
// 修正5: 确保right指针不会越界且找到小于等于pivot的元素
while (left <= right && s[right] > pivot) {
right--;
}
if (left <= right) { // 修正6: 只有当left和right未交叉时才交换
int tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left++;
right--;
}
}
// 修正7: 将基准元素放到正确的位置
// 此时left指针指向第一个大于或等于pivot的元素,或者已经越过right
// 我们需要将pivot(原s[b])与s[left]交换
// 实际上,更常见的partition实现是将pivot与right+1位置的元素交换,
// 或者在循环结束后将pivot放到left和right交叉点的位置。
// 下面提供一种更标准且健壮的Hoare分区或Lomuto分区变体
// 考虑原始问题中的partition逻辑,它将pivot放在s[b]
// 并在最后与s[left]交换。为了兼容原始逻辑并修正,
// 我们可以将pivot与最终left指针指向的元素交换,
// 确保left指向的元素是第一个大于或等于pivot的元素。
// 这里的left就是pivot的最终位置。
// 以下是基于Lomuto分区方案的改进,它通常更易于理解和实现:
// 我们可以将pivot先与s[b]交换,然后将s[b]作为pivot。
// 或者,将s[b]作为pivot,然后将它移动到正确的位置。
// 针对原代码的修正,pivot的最终位置是left。
// 原始代码的最后交换逻辑:
// s[b] = s[left];
// s[left] = pivot;
// return left;
// 修正后的逻辑:将基准元素(原始s[b])放置到left指针的最终位置
// 此时left指针停在第一个大于或等于pivot的元素位置
// 如果pivot是s[b],那么在循环结束后,left指向的元素就是pivot应该在的位置
// 或者,我们也可以在partition开始时就将pivot交换到a,或者随机选择。
// 为了与原代码保持一致,基准元素是s[b]。
// 循环结束后,left指向第一个大于等于pivot的元素,right指向第一个小于等于pivot的元素。
// 如果我们用s[b]作为pivot,那么left就是pivot的最终位置。
// 确保s[left] >= pivot,然后交换s[b]和s[left]。
// 考虑到原始答案的修正,它在最后做了条件判断交换。
// 我们可以直接将pivot(原s[b])与s[left]交换,因为在循环结束后,
// left指向的元素是第一个大于等于pivot的,或者left已经越过了right。
// 此时left就是pivot的最终位置。
int pivotFinalPos = left; // pivot的最终位置
int temp = s[pivotFinalPos];
s[pivotFinalPos] = s[b]; // 将pivot(原始s[b])放到pivotFinalPos
s[b] = temp; // 将pivotFinalPos的原始值放回b
return pivotFinalPos;
}
public static void main(String[] args) {
int[] arr = {85, 10, 24, 63, 45, 27, 100, 31, 96, 50, 40, 23, 49, 96, 120, 105, 13, 5, 42, 69, 22, 12};
quickSort(arr);
System.out.print("Sorted array: ");
for (int i: arr) System.out.print(i + ", ");
System.out.println("");
int[] arr2 = {5, 2, 8, 1, 9, 4};
quickSort(arr2);
System.out.print("Sorted array2: ");
for (int i: arr2) System.out.print(i + ", ");
System.out.println("");
int[] arr3 = {10, 5};
quickSort(arr3);
System.out.print("Sorted array3: ");
for (int i: arr3) System.out.print(i + ", ");
System.out.println("");
int[] arr4 = {1};
quickSort(arr4);
System.out.print("Sorted array4: ");
for (int i: arr4) System.out.print(i + ", ");
System.out.println("");
}
}对上述修正的详细解释:
quickSortSub(int[] s, int a, int b) 方法:
partition(int[] s, int a, int b) 方法:
上述修正方案在一定程度上是基于原始代码逻辑的优化。在实际开发中,Lomuto分区方案因其简洁性而广受欢迎,虽然在处理重复元素时效率可能略低于Hoare分区。以下是基于Lomuto分区思想的修正版本:
public class QuickSortLomuto {
public static void quickSort(int[] s) {
if (s == null || s.length == 0) {
return;
}
quickSortSub(s, 0, s.length - 1);
}
private static void quickSortSub(int[] s, int a, int b) {
if (a < b) {
int pivotIndex = partition(s, a, b);
quickSortSub(s, a, pivotIndex - 1);
quickSortSub(s, pivotIndex + 1, b);
}
}
private static int partition(int[] s, int a, int b) {
int pivot = s[b]; // 选择最右侧元素作为基准
int i = a - 1; // i 指向小于pivot区域的最后一个元素
for (int j = a; j < b; j++) {
if (s[j] <= pivot) { // 如果当前元素小于或等于基准
i++;
// 交换 s[i] 和 s[j]
int temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}
// 将基准元素放到正确的位置
// 此时 i+1 是第一个大于pivot的元素的位置
int temp = s[i + 1];
s[i + 1] = s[b];
s[b] = temp;
return i + 1; // 返回基准元素的最终索引
}
public static void main(String[] args) {
int[] arr = {85, 10, 24, 63, 45, 27, 100, 31, 96, 50, 40, 23, 49, 96, 120, 105, 13, 5, 42, 69, 22, 12};
quickSort(arr);
System.out.print("Sorted array (Lomuto): ");
for (int i: arr) System.out.print(i + ", ");
System.out.println("");
}
}基准元素选择策略:
处理小数组:
避免最坏情况:
重复元素处理:
快速排序算法的正确实现对边界条件和分区逻辑的精确把握有着严格要求。通过对递归终止条件、分区循环条件以及基准元素放置逻辑的细致审查和修正,我们可以构建一个健壮且高效的快速排序算法。理解并应用这些修正方案,不仅能解决排序错误,还能加深对分治算法和指针操作的理解,为处理更复杂的算法问题打下坚实基础。
以上就是Java递归快速排序算法的优化与常见错误修正的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号