**算法描述**
1. 每一轮排序选择一个基准点(pivot)进行分区
1)让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
2)当分区完成时,基准点元素的位置就是其最终位置
2. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 ([divide-and-conquer])
3. 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案
**单边循环快排(lomuto 洛穆托分区方案)**
1. 选择最右元素作为基准点元素
2. j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换
3. i 指针维护小于基准点元素的边界,也是每次交换的目标索引
4. 最后基准点与 i 交换,i 即为分区位置
首先看第一轮分区的代码
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
partition(a, 0, a.length-1);
}
private static int partition(int[] a, int l, int h) {
int pv = a[h]; // 基准点元素
int i = l;
for (int j = l; j < h; j++) {
System.out.println("i=" + i + ";j=" + j);
System.out.println(Arrays.toString(a));
if (a[j] < pv) {
swap(a, i, j);
i++;
}
System.out.println("i=" + i + ";j=" + j);
System.out.println(Arrays.toString(a));
System.out.println("------------------------");
}
swap(a, h, i);
System.out.println(Arrays.toString(a));
// 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
return i;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
控制台输出:
i=0;j=0
[5, 3, 7, 2, 9, 8, 1, 4]
i=0;j=0
[5, 3, 7, 2, 9, 8, 1, 4]
------------------------
i=0;j=1
[5, 3, 7, 2, 9, 8, 1, 4]
i=1;j=1
[3, 5, 7, 2, 9, 8, 1, 4]
------------------------
i=1;j=2
[3, 5, 7, 2, 9, 8, 1, 4]
i=1;j=2
[3, 5, 7, 2, 9, 8, 1, 4]
------------------------
i=1;j=3
[3, 5, 7, 2, 9, 8, 1, 4]
i=2;j=3
[3, 2, 7, 5, 9, 8, 1, 4]
------------------------
i=2;j=4
[3, 2, 7, 5, 9, 8, 1, 4]
i=2;j=4
[3, 2, 7, 5, 9, 8, 1, 4]
------------------------
i=2;j=5
[3, 2, 7, 5, 9, 8, 1, 4]
i=2;j=5
[3, 2, 7, 5, 9, 8, 1, 4]
------------------------
i=2;j=6
[3, 2, 7, 5, 9, 8, 1, 4]
i=3;j=6
[3, 2, 1, 5, 9, 8, 7, 4]
------------------------
[3, 2, 1, 4, 9, 8, 7, 5]
注意 i、j 的值,以及数组的变化
一轮分区以后,数组变成了[3, 2, 1, 4, 9, 8, 7, 5],发现基准点左侧,都是比它小的,右侧都是比它大的
对基准点左侧子分区及右侧子分区,改变左右边界,继续进行分区操作,以此类推,直到区间内元素个数<=1时,就表示这个区间是有序的了
添加递归
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
quick(a, 0, a.length-1);
System.out.println(Arrays.toString(a));
}
public static void quick(int[] a, int l, int h) {
if (l >= h) {
return;
}
int p = partition(a, l, h); // p 索引值
quick(a, l, p - 1); // 左边分区的范围确定
quick(a, p + 1, h); // 左边分区的范围确定
}
private static int partition(int[] a, int l, int h) {
int pv = a[h]; // 基准点元素
int i = l;
for (int j = l; j < h; j++) {
if (a[j] < pv) {
swap(a, i, j);
i++;
}
}
swap(a, h, i);
// 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
return i;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
控制台输出:
[1, 2, 3, 4, 5, 7, 8, 9]
**双边循环快排(不完全等价于 hoare 霍尔分区方案)**
1. 选择最左元素作为基准点元素
2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交
3. 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置
首先看第一轮分区操作
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
partition(a, 0, a.length-1);
}
private static int partition(int[] a, int l, int h) {
int pv = a[l];
int i = l;
int j = h;
while (i < j) {
System.out.println("i=" + i + ";j=" + j);
System.out.println(Arrays.toString(a));
// j 从右找小的
while (i < j && a[j] > pv) {
j--;
}
// i 从左找大的
while (i < j && a[i] <= pv) {
i++;
}
System.out.println("i=" + i + ";j=" + j);
swap(a, i, j);
System.out.println(Arrays.toString(a));
System.out.println("-------------------------");
}
swap(a, l, j);
System.out.println(Arrays.toString(a));
return j;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
控制台输出:
i=0;j=7
[5, 3, 7, 2, 9, 8, 1, 4]
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
-------------------------
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
i=4;j=4
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
[1, 3, 4, 2, 5, 8, 9, 7]
分区操作的结果是所有的比基准点小元素的在左边,所有比基准点大元素的在右边
添加上递归
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
quick(a, 0, a.length-1);
}
private static void quick(int[] a, int l, int h) {
if (l >= h) {
return;
}
int p = partition(a, l, h);
quick(a, l, p - 1);
quick(a, p + 1, h);
}
private static int partition(int[] a, int l, int h) {
int pv = a[l];
int i = l;
int j = h;
while (i < j) {
// j 从右找小的
while (i < j && a[j] > pv) {
j--;
}
// i 从左找大的
while (i < j && a[i] <= pv) {
i++;
}
swap(a, i, j);
}
swap(a, l, j);
System.out.println(Arrays.toString(a) + " j=" + j);
return j;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
控制台输出:
[1, 3, 4, 2, 5, 8, 9, 7] j=4
[1, 3, 4, 2, 5, 8, 9, 7] j=0
[1, 2, 3, 4, 5, 8, 9, 7] j=2
[1, 2, 3, 4, 5, 7, 8, 9] j=6
要点
1. 基准点在左边,并且要先 j 后 i
2. while( **i** **< j** && a[j] > pv ) j--
3. while ( **i** **< j** && a[i] **<=** pv ) i++
疑问一,为什么 i 在从左往右,找大的的时候,需要 a[i] <= pv ,而 j 从右往左找小的的时候,不需要
a[j] > pv?
假如是这样 a[i] < pv,没有 =,执行代码,就会出现这个结果
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
partition(a, 0, a.length-1);
}
private static int partition(int[] a, int l, int h) {
int pv = a[l];
int i = l;
int j = h;
while (i < j) {
System.out.println("i=" + i + ";j=" + j);
System.out.println(Arrays.toString(a));
// j 从右找小的
while (i < j && a[j] > pv) {
j--;
}
// i 从左找大的
while (i < j && a[i] < pv) {
i++;
}
System.out.println("i=" + i + ";j=" + j);
swap(a, i, j);
System.out.println(Arrays.toString(a));
System.out.println("-------------------------");
}
swap(a, l, j);
System.out.println(Arrays.toString(a));
return j;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
控制台输出:
i=0;j=7
[5, 3, 7, 2, 9, 8, 1, 4]
i=0;j=7
[4, 3, 7, 2, 9, 8, 1, 5]
-------------------------
i=0;j=7
[4, 3, 7, 2, 9, 8, 1, 5]
i=2;j=7
[4, 3, 5, 2, 9, 8, 1, 7]
-------------------------
i=2;j=7
[4, 3, 5, 2, 9, 8, 1, 7]
i=2;j=6
[4, 3, 1, 2, 9, 8, 5, 7]
-------------------------
i=2;j=6
[4, 3, 1, 2, 9, 8, 5, 7]
i=4;j=6
[4, 3, 1, 2, 5, 8, 9, 7]
-------------------------
i=4;j=6
[4, 3, 1, 2, 5, 8, 9, 7]
i=4;j=4
[4, 3, 1, 2, 5, 8, 9, 7]
-------------------------
[5, 3, 1, 2, 4, 8, 9, 7]
在第一轮分区,第一次遍历时,错误的把基准点元素交换掉了
疑问二,为什么有了外层的 while (i < j) ,内层还需要判断 i < j ?
假设没有i < j 判断,有数组[5, 1, 2, 3, 7, 8, 9] 对它就行分区操作,j 从右向左找小的,到了 2 停下,i 从左向右找大的,到7停下,交换,数组就变成了[5, 1, 7, 3, 2, 8]
i < j 判断,确保了所有的比基准点小元素的在左边,所有比基准点大元素的在右边
疑问三,目前是先运行 j ,后运行 i ,二者的顺序是够可以调换?
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
partition(a, 0, a.length-1);
}
private static int partition(int[] a, int l, int h) {
int pv = a[l];
int i = l;
int j = h;
while (i < j) {
System.out.println("i=" + i + ";j=" + j);
System.out.println(Arrays.toString(a));
// i 从左找大的
while (i < j && a[i] <= pv) {
i++;
}
// j 从右找小的
while (i < j && a[j] > pv) {
j--;
}
System.out.println("i=" + i + ";j=" + j);
swap(a, i, j);
System.out.println(Arrays.toString(a));
System.out.println("-------------------------");
}
swap(a, l, j);
System.out.println(Arrays.toString(a));
return j;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
控制台输出:
i=0;j=7
[5, 3, 7, 2, 9, 8, 1, 4]
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
-------------------------
i=2;j=7
[5, 3, 4, 2, 9, 8, 1, 7]
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
i=4;j=6
[5, 3, 4, 2, 1, 8, 9, 7]
i=5;j=5
[5, 3, 4, 2, 1, 8, 9, 7]
-------------------------
[8, 3, 4, 2, 1, 5, 9, 7]
发现分区有误
**快排特点**
1. 平均时间复杂度是 $O(nlog_2n )$,最坏时间复杂度 $O(n^2)$
2. 数据量较大时,优势非常明显
3. 属于不稳定排序