文章目录
快排(不稳定)
1.递归
原理:
- 从待排序区间选择一个数,作为基准值(pivot);
(基准值在最左侧,先从右向左找;基准值在最右侧,先从左向右找) - Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
- 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1, 代表已经有序,或者小区间的长度 == 0,代表没有数据。
public static void quickSort(int[] array) { //当前形参不适合递归
quickSortInter(array, 0, array.length - 1);
//用quickSortInter实现递归 (数组,左下标,右下标)
}
// [left, right]
private static void quickSortInter(int[] a, int left, int right) {
if (left >= right) { //递归出口
// 直到 长度 <= 1
return;
}
// 1. 选择基准值 array[left]
// 2. 做 partition
int pivotIndex = partition(a, left, right);
// 左边小区间 [left, pivotIndex - 1]
// 右边小区间 [pivotIndex + 1, right]
// 3. 分别对左右小区间按同样方式处理
quickSortInter(a, left, pivotIndex - 1);
quickSortInter(a, pivotIndex + 1, right);
}
1.1 hoare法—partition(常用)
private static int partition1(int[] a, int left, int right) {
int begin = left;
int end = right;
int pivot = a[left];
// [left, begin) <= pivot
// (end, right] >= pivot
while (begin < end) {
while (begin < end && a[end] >= pivot) {
end--;
}
while (begin < end && a[begin] <= pivot) {
begin++;
}
swap(a, begin, end);
}
swap(a, left, begin);
return begin;
}
1.2 挖坑法—partition
基本思路和Hoare 法一致,只是不再进行交换,而是进行赋值(填坑+挖坑)
private static int partition2(int[] a, int left, int right) {
int begin = left;
int end = right;
int pivot = a[left];
// [left, begin) <= pivot
// (end, right] >= pivot
while (begin < end) {
while (begin < end && a[end] >= pivot) {
end--;
}
a[begin] = a[end];
while (begin < end && a[begin] <= pivot) {
begin++;
}
a[end] = a[begin];
}
a[begin] = pivot;
return begin;
}
1.3 前后遍历法—partition
private static int partition3(int[] a, int left, int right) {
// pivot = array[left]
// 比较区间 [left + 1, right]
int pivot = a[left];
int d = left + 1;
//[left,d) <=pivot
//[d,i) >=pivot
for (int i = left + 1; i <= right; i++) {
if (a[i] < pivot) {
swap(a, i, d++);
}
}
swap(a, d - 1, left);
return d - 1;
}
1.4 快排注意事项
1)partition不是快排,只是快排的一个步骤。
2)partition一定要所有的数据都和基准值作比较。
eg:基准值在左,先走右边;基准值在右,先走左边
3)partition时间复杂度O(n),空间复杂度O(1)。
4)快排时间复杂度 (遍历*递归深度): O(n)*O(log(n))~O(n^2)
快排完整代码:递归版
选取最左侧元素为基准值(三种partion):
public class QuickSort {
public static void quickSort(int[] array) {
// 当前形参不适合递归
// 用quickSortInter实现递归 (数组,左下标,右下标)
quickSortInter(array, 0, array.length - 1);
}
// [left, right]
private static void quickSortInter(int[] a, int left, int right) {
if (left >= right) {
// 直到 长度 <= 1
return;
}
// 1. 选择基准值 array[left]
// 2. 做 partition
int pivotIndex = partition3(a, left, right);
// 左边小区间 [left, pivotIndex - 1]
// 右边小区间 [pivotIndex + 1, right]
// 3. 分别对左右小区间按同样方式处理
quickSortInter(a, left, pivotIndex - 1);
quickSortInter(a, pivotIndex + 1, right);
}
private static int partition1(int[] a, int left, int right) {
int begin = left;
int end = right;
int pivot = a[left];
// [left, begin) <= pivot
// (end, right] >= pivot
while (begin < end) {
while (begin < end && a[end] >= pivot) {
end--;
}
while (begin < end && a[begin] <= pivot) {
begin++;
}
swap(a, begin, end);
}
swap(a, left, begin);
return begin;
}
private static int partition2(int[] a, int left, int right) {
int begin = left;
int end = right;
int pivot = a[left];
// [left, begin) <= pivot
// (end, right] >= pivot
while (begin < end) {
while (begin < end && a[end] >= pivot) {
end--;
}
a[begin] = a[end];
while (begin < end && a[begin] <= pivot) {
begin++;
}
a[end] = a[begin];
}
a[begin] = pivot;
return begin;
}
private static int partition3(int[] a, int left, int right) {
// array[left]
// [left + 1, right]
int pivot = a[left];
int d = left + 1;
for (int i = left + 1; i <= right; i++) {
if (a[i] < pivot) {
swap(a, i, d++);
}
}
swap(a, d - 1, left);
return d - 1;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
选取最右侧元素为基准值,采取hoare法做partion示例:
public class TestSort {
public static void quickSort(int[] array){
// []
quickSortHelp(array,0,array.length-1);
}
private static void quickSortHelp(int[] array, int left, int right) {
if(left >= right){
// 区间中有 0 个元素或者 1 个元素. 此时不需要排序
return;
}
// 区间中有 0 个元素或者 1 个元素. 此时不需要排序
int pivotIndex = partition(array,left,right);
quickSortHelp(array,left,pivotIndex-1);
quickSortHelp(array,pivotIndex+1,right);
}
private static int partition(int[] array, int left, int right) {
int baseIndex = right;
int base = array[right]; //最右侧为基准值
while (left<right){
// 从左向右找比基准值大的元素
while (left<right && array[left] <= base){
left++;
}// 当上面循环结束时,
// left 要么和right重合,要么left指向一个大于base的值
// 从左向右找比基准值小的元素
while (left<right && array[right] >= base){ // 此处等号不能省略 不然基准值可能被交换到其他位置
right--;
}// 当上面循环结束时,
// right 要么和left重合,要么right指向一个小于base的值
swap(array,left,right);
}
// 此时 left = right 相遇了,将相遇位置元素和基准值进行交换
swap(array,left,baseIndex);
// 返回此时基准值的下标,即重合位置下标
return left;
}
private static void swap(int[] array, int left, int right) {
int temp = array[left];
array[left] = array[right];
array[right] = temp;
}
public static void main(String[] args) {
int[] array = {6,1,2,7,9,3,4,5,10,8};
quickSort(array);
System.out.println(Arrays.toString(array));
}
}
为什么最后一步将相遇位置元素和基准值进行交换swap(array,left,baseIndex);
,仍然能满足快排的要求呢?
答:baseIndex 是这个序列中最后一个元素的下标,要求 left 和 right 重合位置元素必需是 >= 基准值的元素才可以交换。
①left++;
导致和 right 重合。此时最终的值取决于上次循环中 right 指向的值。上次循环中,right应该是找到了一个小于基准值的值,然后和一个大于基准值的值交换了,此处最终的 g 一定是大于基准值的。
②right--;
导致和 left 重合。此时最终的值取决于上次循环中 left 指向的值。上面的 left++ 退出一定是 left 找到了比基准值大的元素,此时 left 和 right 重合元素则一定也是大于基准值的。
测试结果:
性能分析:
当数组逆序时,取第一个或最后一个元素为基准值,时间复杂度O(n2),空间复杂度O(n)。
快排的空间复杂度,主要与递归深度有关。
2.非递归
原理:利用栈实现区间的管理。
import java.util.*;
// 升序
public static void quickSort(int[] array) {
// 借助栈, 模拟实现递归的过程
// stack 用来存放数组下标. 通过下标来表示接下来要处理的区间是什么
Stack<Integer> stack = new Stack<>();
// 初始情况下, 先把右侧边界下标入栈, 再把左侧边界下标入栈, 左右边界仍然构成前闭后闭区间
stack.push(array.length - 1);
stack.push(0);
while (!stack.isEmpty()) {
// 这个取元素的顺序要和push的顺序正好相反
int left = stack.pop();
int right = stack.pop();
if (left >= right) {
// 区间中只有 1 个或 0 个元素, 不需要整理
continue;
}
// 通过 partition 把区间整理成以基准值为中心, 左侧小于等于基准值, 右侧大于等于基准值的形式
int pivotIndex = partition(array, left, right);
// 准备处理下个区间.
// [index + 1, right] 基准值右侧区间
stack.push(right);
stack.push(pivotIndex + 1);
// [left, index - 1] 基准值左侧区间
stack.push(pivotIndex - 1);
stack.push(left);
}
}
3.快排优化
3.1优化基准值
快速排序的效率和基准值先取的好坏密切相关。如果基准值是一个接近数组中位数的元素,比较平衡;如果基准值刚好取到最大值(最小值),比较差。
1)选择第一个数作为pivot——只有顺序/逆序才是单支树。
2)随机选择任意数作为pivot——不能消除单支树,但减少了退化成单支树的概率。
3)几数取中(一般三数取中)——消除单支树。
3.2小区间使用插排
当区间比较小时,再去递归效率已经不高了。此时不进行递归,直接进行插入排序。
3.3大区间使用堆排
如果区间比较大时,递归深度也会非常深。当递归深度达到一定程度时,把当前区间的排序使用堆排序进行优化。