介绍
快速速排序在每一轮挑选一个基准元素, 并让其他比它大的元素移动到数列一边, 比它小的元素移动到数列的另一边, 从而把数列拆解成两个部分。然后再按此方法对这两部分数据分别进行快速排序, 整个排序过程可以递归进行, 以此达到整个数据变成有序序列
问题引入
我们先来看两个有关的问题
问题一
给定一个数组arr, 和一个数num, 请把小于等于num的数放在数 组的左边, 大于num的数放在数组的右边。 要求额外空间复杂度O(1), 时间复杂度O(N)
思路
1.使用两个变量small(表示小于num区域的范围),p1(用来记录遍历到的位置)
2.当arr[p1]小于num的时候就把p1上面的数和小于区域的下一个数做交换,小于区域向右扩充一个位置
3.p1向右移动
public void process(int[] arr, int num) {
//假设小于区域刚开始在-1的位置
int small = -1;
int p1 = 0;
while (p1 < arr.length) {
if (arr[p1] <= num) { //和<区域的下一个数做交换
swap(arr, small + 1, p1);
small++; //<区域向右扩
}
p1++;
}
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
问题二(荷兰国旗问题)
给定一个数组arr, 和一个数num, 请把小于num的数放在数组的 左边, 等于num的数放在数组的中间, 大于num的数放在数组的 右边。 要求额外空间复杂度O(1), 时间复杂度O(N)
思路
1.这道题和问题一相比多了一个条件需要把等于num的数放在中间,那么我们增加一个区域大于区域,将小于区域和大于区域划分好中间部分就是等于num的数
2.使用三个指针less,more,cur(less表示小于区域,more表示大于区域,cur表示当前位置)
3.
3.1 如果arr[cur] < num cur和小于区域的下一个数交换位置,小于区域向右扩,cur指针向右移动
3.2 如果arr[cur] > num cur和大于区域的前一个数交换位置,大于区域向左扩
3.3 如果arr[cur]==cur cur向右移动
注意: 在3.2中大于区域向左扩之后cur没有向右移动是因为不知道交换之后的cur的情况需要重新判断。
public void process(int[] arr, int num) {
int less = -1; //小于区域
int more = arr.length; //大于区域
int cur = 0; //表示指针当前位置
while (cur < more) { //如果cur碰到了大于区域说明过程结束
if (arr[cur] < num) {
swap(arr, less + 1, cur);
less++; //<区域向右扩
cur++; //指针向右移动
} else if (arr[cur] > num) {
swap(arr, more - 1, cur);
more--; //>区域向左扩
} else { //如果arr[less]==num 直接向右移动
cur++;
}
}
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
快速排序代码
public void quickSort(int[] arr) {
if (arr == null || arr.length < 2) return;
quickSort(arr, 0, arr.length - 1);
}
public void quickSort(int[] arr, int L, int R) {
if (L < R) {//只要划分的部分剩余元素数量不是1继续快排
swap(arr, (int) (L + Math.random() * (R - L + 1)), R);
int[] finish = partition(arr, L, R);
quickSort(arr, L, finish[0] - 1);//左边部分
quickSort(arr, finish[1] + 1, R);
}
}
public int[] partition(int[] arr, int L, int R) {
//选取最后一个数作为参照
int num = arr[R];
int less = L - 1;
int more = R;
int cur = L;
while (cur < more) {
if (arr[cur] < num) {
swap(arr, cur, less + 1);
less++;//小于区域向右扩
cur++; //cur向右移动
} else if (arr[cur] > num) {
swap(arr, cur, more - 1);
more--;//>区域向左扩
} else {
cur++;
}
}
swap(arr, more, R);
return new int[]{less + 1, more};
}
public void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
优化部分分析
if (L < R) {//只要划分的部分剩余元素数量不是1继续快排
swap(arr, (int) (L + Math.random() * (R - L + 1)), R);
int[] finish = partition(arr, L, R);
quickSort(arr, L, finish[0] - 1);//左边部分
quickSort(arr, finish[1] + 1, R);
}
在进行partition之前我们随机选一个数和最后一个数进行交换。这样避免了一些特殊情况使快速排序的时间复杂度为O(N^2),比如我们举个例子对于
[1,2,3,4,5,6,7]这组数我们每次进行partition的过程中如果选取最后一个数作为基准元素进行左右划分会发现每次只有小于它的部分没有大于它的部分,这样时间复杂度为O(N^2),而如果随机选取出一个数作为基准元素,这样根据数学方法计算出排序的时间复杂度为O(N*logN)
时间复杂度
没有随机选取基准元素的时间复杂度O(N^2)
随机选取基准元素的时间复杂度O(N*logN)
额外空间复杂度
递归调用的过程中要保存一些数据比如说划分的基准元素的位置
最优的情况下空间复杂度为:O(logn) ,每一次都平分数组的情况
最差的情况下空间复杂度为:O( n ) ,退化为冒泡排序的情况