实现快排有三种方式:分别是单路快排、双路快排、三路快排
下面分别来讲解这三种排序的基本思路及其实现
单路快排
基本思路:设置一个标记值pivot,然后遍历数组,找到pivot的正确位置,然后递归左右子数组,执行相同操作,直到子数组长度为1时,该子数组为有序数组。
说明:设置random变量的主要原因:如果原先排序的数组整体上有序,那么快排的时间复杂度会退化为O(n^2),选择随机的标记值而不是每次都选择数组的第一个元素作为pivot值,可以有效解决该问题。
public class QuickSort {
private static Random random = new Random(System.currentTimeMillis());
@Test
public void test() {
int[] nums = {6, 1, 2, 7, 9, 3, 4, 5, 8};
quickSort(nums, 0, nums.length-1);
for (int num : nums) {
System.out.print(num+", ");
}
}
public void quickSort(int[] nums, int left, int right) {
//递归终止条件
if (left >= right) {
return;
}
int partiton = partition(nums, left, right);
quickSort(nums, left, partiton - 1);
quickSort(nums, partiton + 1, right);
}
public int partition(int[] nums, int left, int right) {
int randIndex = left + random.nextInt(right - left + 1);
swap(nums, left, randIndex);
int pivot = nums[left];
int j = left + 1;
// [left,j) < pivot
// [j,right]>= pivot
for (int i = left + 1; i <= right; i++) {
if (nums[i] < pivot) {
swap(nums, i, j);
j++;
}
}
swap(nums, left, j-1); // 跳出循环的时候,nums[j]处的索引值>pivot,所以这里交换的应该是j-1
return j-1;
}
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
测试结果:
双路快排
由上面可知,当数组中重复的元素太多的时候,那么快速排序的退化是特别快的;而双路快排就是要解决重复元素在递归划分时两个子数组分布不均衡(分布均匀,每个分组重复元素也就变少了,这样随机选取的元素和left进行交换发生元素值相同的概率变小了)导致时间复杂度退化的问题。
//双路快排
private int partiton_2(int[] nums, int left, int right) {
int randomIndex = left + random.nextInt(right - left + 1); // 产生[left,right]的某个索引
swap(nums, left, randomIndex);
int pivot = nums[left];
int le = left + 1;
int ge = right;
// [left+1,le)<=pivot
// (ge,right]>=pivot
while (true) {
while (le <= ge && nums[le] < pivot) { //必须严格小于;之所以要严格小于,是为了不让等于pivot的值都发布在左边
le++;
}
while (le <= ge && nums[ge] > pivot) { //必须严格大于; 之所以要严格大于,是为了不让等于pivot的值都发布在右边
ge--;
}
// le来到了一个大于等于pivot的位置(索引)
// ge来到了一个小于等于pivot的位置(索引),
//如果le > ge,那么le指向的元素值大于pivot,ge指向的元素值小于pivot
if (le >= ge) { //如果le==ge,那么对应的值就是pivot,此时就可以退出循环了
break;
}
swap(nums, le, ge);
le++;
ge--;
}
swap(nums, left, ge); //因为最后ge指向的元素值是小于等于pivot的,所以应该交换的是left和ge;如果ge的元素值等于pivot,那交换到左半区也没问题,左边即xiao'yu
return ge;
}
三路快排
优点:对于有很多值相等的排序,partition这个过程把数值相同的元素挤到了中间,这样被划分的两个子数组的重复元素就减少了很多,使得每一次确定位置的元素变多了;相比于双路快排提升了很多!
完整代码:
public class QuickSort {
private final static Random random = new Random(System.currentTimeMillis()); //随机数
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
//这里没有写在partition()里,是因为它要返回的分解区的索引有两个,直接写在quickSort()里调用就行,不然返回值就得写数组类型了
private void quickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int randomIndex = left + random.nextInt(right - left + 1); // 产生[left,right]的某个索引
swap(nums, left, randomIndex);
int pivot = nums[left];
int lt = left + 1;
int gt = right;
int i = left + 1;
// [left+1,lt) < pivot
// [lt,i) == pivot
// (gt,right] > pivot
// 按照该定义去编写代码就不会乱,避免出现死循环的问题
while (i <= gt) { //当i==gt的时,第二个数组和第三个数组还没连起来,所以循环还应该继续
if (nums[i] < pivot) {
swap(nums, i, lt);
lt++;
i++;
} else if (nums[i] == pivot) {
i++;
}else {
swap(nums, i, gt);
gt--;
}
}
swap(nums, left, lt - 1);
quickSort(nums, left, lt - 2); //左子组递归
quickSort(nums, gt+1, right); //右子组递归
}
}