当待排序元素序列中有大量的重复排序码时,简单的快速排序算法的效率将会降到非常之低。一种直接的想法就是将待排序列分成三个子序列:一部分是排序码比基准元素排序码小的;一部分是与基准元素排序码等值的;一部分是比基准元素排序码大的,如下图所示:
但是,如果我们直接据此思想去编写实现算法的话,会让我们面临很大的困难。与基准元素等值的元素到底有多少?以及如何最快速有效地确定划分的边界?所以,完成这样的三路划分是非常困难的,甚至比两路划分过程更加复杂。
我们可以基于以下的思想实现三路划分:在划分的过程中,扫描时将遇到的左子序列中与基准元素排序码等值的元素放到序列的最左边,将遇到的右子序列中与基准元素排序码等值的元素放到序列的最右边。这样,我们会得到如下所示的序列划分图:
这里我们用到了四个指针,这里四个指针的含义:
leftLabelPtr(左子序列下一个与基准元素相等元素的下标),
rightLabelPtr(右子序列下一个与基准元素相等元素的下标),
leftScanPtr(左子序列的扫描指针),
rightScanPtr(右子序列的扫描指针)
当我们在左子序列中找到比基准元素大,且在右子序列中找到比基准元素小的元素的时候(此时左右扫描指针记录着这两和 元素),将两个元素进行交换,然后扫描指针接着扫描(左指针往右扫描,右指针往左扫描)。
直到左扫描指针大于等于右扫描指针的时候,我们跳出循环,然后将左边与基准元素相等的元素交换到中间,将右边与基准元素相等的元素交换到中间。然后递归地在左右子序列中调用快排。
下面是{25, 15, 30, 10, 50, 3, 5, 30}
数组基于三路划分的快速排序的示意图:
代码实现:
/**
* 三路划分的快速排序
*/
public class QuickSort_Three_way_division {
public static void main(String[] args) {
int[] arra = {25, 15, 30, 10, 50, 3, 5, 30};
QuickSort_Three_way_division t = new QuickSort_Three_way_division();
t.quickSort(arra, 0, arra.length - 1);
for (int i = 0; i < arra.length; i++) {
System.out.print(arra[i] + " ");
}
}
private void quickSort(int[] nums, int left, int right) {
if (right <= left)
return;
int leftLabelPtr, rightLabelPtr, leftScanPtr, rightScanPtr;
int pivot;// 锚点
leftScanPtr = leftLabelPtr = left;
rightScanPtr = rightLabelPtr = right - 1;
pivot = nums[right];
while (true) {
while (leftScanPtr < right && nums[leftScanPtr] <= pivot) {
if (nums[leftScanPtr] == pivot) {
swap(nums, leftScanPtr, leftLabelPtr);
leftLabelPtr++;
}
leftScanPtr++;
}
while (left <= rightScanPtr && nums[rightScanPtr] >= pivot) {
if (nums[rightScanPtr] == pivot) {
swap(nums, rightScanPtr, rightLabelPtr);
rightLabelPtr--;
}
rightScanPtr--;
}
if (leftScanPtr >= rightScanPtr)
break;
/*
* 将左边大于pivot的元素与右边小于pivot元素进行交换
*/
swap(nums, leftScanPtr, rightScanPtr);
leftScanPtr++;
rightScanPtr--;
}
/*
* 因为工作指针i指向的是当前需要处理元素的下一个元素
* 故而需要退回到当前元素的实际位置,然后将等于pivot元素交换到序列中间
*/
leftScanPtr--;
leftLabelPtr--;
while (leftLabelPtr >= left) {
swap(nums, leftScanPtr, leftLabelPtr);
leftScanPtr--;
leftLabelPtr--;
}
/*
* 因为工作指针j指向的是当前需要处理元素的上一个元素
* 故而需要退回到当前元素的实际位置,然后将等于pivot元素交换到序列中间
*/
rightScanPtr++;
rightLabelPtr++;
while (rightLabelPtr <= right) {
swap(nums, rightScanPtr, rightLabelPtr);
rightScanPtr++;
rightLabelPtr++;
}
/*
* 递归遍历左右子序列
*/
quickSort(nums, left, leftScanPtr);
quickSort(nums, rightScanPtr, right);
}
public void swap(int[] nums, int i, int j) {
if (i != j) {
nums[i] = nums[i] ^ nums[j];
nums[j] = nums[i] ^ nums[j];
nums[i] = nums[i] ^ nums[j];
}
}
}