性能
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
特点
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了
实现
单边循环(lomuto分区)要点
-
·选择最右侧元素作为基准点
- ·j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
- 。交换时机: j找到小的,且与i不相等
- 。当 i 找到>=基准点元素后,不应自增,
- ·最后基准点与i交换,i 即为基准点最终索引
public class QuikSortLomuto {
public static void sort(int[] a){
quick(a,0,a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (left >= right){
return;
}
// p 代表基准点元素最终索引
int p = partition(a,left,right);
quick(a,left,p - 1);
quick(a,p + 1,right);
}
private static int partition(int[] a, int left, int right) {
// prv 是基准点元素
int prv = a[right];
int i= left,j = left;
//快慢指针
// i 找比基准点大的,j找比基准点小的
while (j < right){
// 交换时机,j找到小的,且与i不相等(即i、j指向同一元素),
if (a[j] < prv){//j没找到比prv大的元素
if (i != j){
swap(a,i,j);
}
i++;//因为 j 找到了比基准点小的了,确定 i 的位置
}
j++; // j必须不断往前走, i 可能停下
}
swap(a,i,right);
return i;
}
private static void swap(int[] a,int i,int j){
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
// int[] a ={9,3,7,2,8,5,1,4};
int[] a = {4, 2, 1, 3, 2, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
选择最左侧侧元素作为基准点
private static int partition(int[] arr, int left, int right) {
int prev = arr[left];
int index = left+1;//每次的假设大分区的左边界
//保证最后分区的正确性
for (int i = index; i <= right; i++) {
if (arr[i] < prev){
swap(arr, i, index);
index++;
}
}
swap(arr,left,index-1);
return index-1;
}
双边循环( QuikSortHoare)
- ·选择最左侧元素作为基准点
- · j找比基准点小的,i找比基准点大的,一旦找到,二者进行交换
- 。 i从左向右
- 。j从右向左
- ·最后基准点与i交换,i即为基准点最终索引
代码
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (left >= right) {
return;
}
// p 代表基准点元素最终索引
int p = partition(a, left, right);
quick(a, left, p - 1);
quick(a, p + 1, right);
}
private static int partition(int[] a, int left, int right) {
// ThreadLocalRandom.current().nextInt(10);
int index = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(a,index,left);
// [0, 10)
//希望[4,6],right - left + 1计算排序区间内的元素个数
// right - left + 1 =3 ->[0,2]+4=[4,6]
// prv 是基准点元素,与
int pv = a[left];
int il = left;
int ir = right;
while (il < ir) {
//注意:最终 il == ir 的,且il 和 ir 保持不动
// ,ir找比基准点小的,且单边寻找不能过界
while (il < ir && a[ir] > pv) {
ir--;
}
// il 找比基准点大的
while (il < ir && a[il] <= pv) {
il++;
}
//3 可以交换了
swap(a, il, ir);
}
swap(a, left, il);
return il;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 2, 8, 5, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
双边快排注意事项
-
为啥内层循环加 i < j 条件
内层i(j)相当于单独遍历,一定要保证不能越界
-
为啥要先处理 j,再处理 i
逻辑错误,相遇时换基准点会发生错误错误,所谓错误,体现在循环结束上,在i l == rj 时,(谁先开始找就停在谁那)先找大的那么是先 il停下来,此时 a[il] 大于基准点,最后在整个分区循环结束完的交换中,必然将大的元素交到前面去。
也不是不可以,需要将基准点选择、最后 il == rj 时交换改变。即谁先找,基准点从对面选择和它交换;但基准点一般于后者进行交换
-
随机元素作为基准点元素
对于顺序反转,效果不好,极度不平衡,且性能大大降低O(nlong(n)) ----> O(n^2)
理想的基准点是:恰好是中间元素
最终折中随机
最优版实现
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (left >= right) {
return;
}
// p 代表基准点元素最终索引
int p = partition(a, left, right);
quick(a, left, p - 1);
quick(a, p + 1, right);
}
/*
循环内:
i 从 left + 1 开始,从左向右找大的或相等的
j 从 right 开始,从右向左找小的或相等的
交换, i++, j--
循环外 j 和 基准点交换, j 即为分区位置 -——>分区均衡
*/
private static int partition(int[] a, int left, int right) {
int pv = a[left];
int i = left + 1;
int j = right;
//为什么要去=号,因为while循环是找j作为小区间的有边界进行所以初始化后须进入判断
while (i <= j) {
while (i <= j && a[i] < pv) {
i++;
}
while (i <= j && a[j] > pv) {
j--;
}
//符合条件时还得是否越界
if ( i <= j){
swap(a, i, j);
i++;
j--;
}
}
swap(a, j, left);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
// int[] a = {4, 2, 1, 3, 2, 4};// 最外层循环 = 要加
// int[] a = {2,1,3,1};// 内层循环 = 要加
int[] a ={2,1,3,2};// 内层if 要加
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}