单路快排
核心思想(partition)
选取一个元素作为基准值,将小于基准值的元素放在基准值左边,将大于或者等于基准值的元素放在基准值右边。经过这样一次操作后数组已经宏观上有序,且基准值已经来到它的最终位置。然后对基准值左边的数组和基准值右边的数组采取同样的方式进行处理,直至数组不能再进行划分,则说明此时数组已经有序。
主体代码:
private static void quickSort(int[] arr,int l,int r){
if(l > r){
return;
}
// 划分后基准值所在的下标
int p = partiton2(arr,l,r);
// 处理基准值左边的子数组
quickSort(arr,l,p - 1);
//处理基准值右边的子数组
quickSort(arr,p + 1,r);
}
图解:
- 选取基准值:
- 元素调整,调整完毕后将基准值与小于区的最后一个元素进行交换
- 递归重复以上步骤直至数组中仅剩一个元素或者没有元素
此时数组已经有序。
代码实现(选取第一个元素作为划分标志):
划分:L+1,j] 小于区,[j+1,i-1]大于等于区
// 单项调整
//[L+1,j] 小于区,[j+1,i-1]大于等于区
private static int partition(int[] arr,int l,int r){
int i = l+1;
int j = l;
while(i <= r){
if(arr[i] < arr[l]){
swap(arr,++j,i++);
}else{
i++;
}
}
swap(arr,l,j);
return j;
}
交换
数组是引用类型,必须通过引用传递将数组本身传过去进而进行交换。
private static void swap(int[] arr,int i,int j){
if(i != j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
但此时存在一个问题,若数组中存在大量等于基准值的元素,经过partition操作后左右两个子数组严重失衡,使得快速排序退化为O(n^2)的算法。即出现如图这样的问题:
为了改变这样的情况引入双路快排。
双路快排
核心思想
双路快排其它操作同单路快排,只是在进行数组划分时将小于等于基准值的,放在数组左边,将大于等于基准值的元素放在数组右边,也就是说等于基准值的元素大体上平摊到左右两边,这样就可以避免划分极不均衡的情况。
此时我们设置两个指针i,j,j从尾向前,i从头向尾,进行遍历,当arr[i] 大于等于基准值,arr[j]小于等于基准值时两个索引位置的元素进行交换,否咋i++,j–,直至i>=j.
图解:
- 选取基准值,设置首尾索引
- 遍历,满足条件时进行交换
- 继续遍历直至i >= j 交换基准值与小于等于区的最后一个数
n
经过这样一次操作后数组已经宏观上有序,且基准值已经来到它的最终位置。
代码实现:
private static int partiton2(int[] arr,int l,int r){
int i = l+1;
int j = r;
while(true){
while(i <= r && arr[i] < arr[l]) i++;
while(j >= l+1 && arr[j] > arr[l]) j--;
if(i > j){
break;
}
swap(arr,i++,j--);
}
swap(arr,l,j);
return j;
}
但此时问题又出现了,当整个数组大部分元素有序时会怎么样呢?不难发现这样也会导致左右两个子数组严重失衡,导致快排退化为O(n^2)的算法。
那么如何避免这种情况呢?我们引入随机快排。
随机快排
核心思想:在选取基准值的时候,任意选取一个元素,然后将该元素与数组首元素进行交换,然后将交换过来的元素作为基准值。这样第一次选到最大值或者最小值的概率为1 / n,第二次 1 / n -1,第三次 1 / n - 2,以此类推,也就是说每次选取到的基准值都为最大或者最小值的概率为1 / n * n 无限逼近于0.这样就可以避免因为数组有序而导致算法复杂度退化的问题
代码实现:
private static int partiton2(int[] arr,int l,int r){
int i = l+1;
int j = r;
while(true){
// 每次都随机选取基准值
int index = (int)(Math.random()*(r - l + 1) + l);
swap(arr,l,index);
while(i <= r && arr[i] < arr[l]) i++;
while(j >= l+1 && arr[j] > arr[l]) j--;
if(i > j){
break;
}
swap(arr,i++,j--);
}
swap(arr,l,j);
return j;
}
三路快排
核心思想
将整个数组按照划分值val,分成小于val的区域称为小于区,等于val的区域等于区和大于val的区域大于区三部分,在下一次的递归中就不必再处理等于val的等于区,因为等于区的元素已经到达了最终位置,对于存在大量等于val的数组三路快排大大提升了效率。
代码实现(partition过程):
/**
*
* @param arr 待处理数组
* @param left 数组左边界
* @param right 数组右边界
* @return 等于区的边界值通过一个数组返回
*/
public static int[] partition(int[] arr,int left,int right){
// [0,less] 小于区
int less = left - 1;
// [more, right] 大于区
int more = right + 1;
//随机生成一个下标
int index = (int) (Math.random() * (right - left + 1) + left);
// 与最后一个位置的元素进行叫换
swap(arr,index,right);
int val = arr[right];
while(left < more){
if(arr[left] < val){
swap(arr,++less,left++);
}else if(arr[left] > val){
swap(arr,--more,left);
}else{
left++;
}
}
//[less+1,more-1] 等于区
return new int[]{less+1,more-1};
}
// 元素交换
public static void swap(int[] arr,int i,int j){
if(i != j){
arr[i] ^= arr[j];
arr[j] ^= arr[i];
arr[i] ^= arr[j];
}
}
}
测试:
public class TestDemo2 {
public static void main(String[] args) {
int[] arr = new int[]{1,3,5,7,3,4,2,8,9,2,5};
System.out.println("排序前");
for(int data : arr){
System.out.print(data + " ");
}
System.out.println();
System.out.println("排序后:");
quickSort(arr,0,arr.length - 1);
for(int data : arr){
System.out.print(data + " ");
}
}
public static void quickSort(int[] arr,int l,int r){
if(l >= r){
return;
}
int[] ret = partition(arr,l,r);
quickSort(arr,l,ret[0] - 1);
quickSort(arr,ret[1] + 1,r);
}
运行结果:
算法分析:
- 时间复杂度:
最好O(nlogn),最坏O(n*n)。 - 空间复杂度
递归调用本质在不断压栈,因此快排的空间复杂度与递归的深度有关,最好的情况是 O(logn),最坏O(n)。 - 稳定性:不稳定。
,