分治法在排序算法中的应用(JAVA)--快速排序(Lomuto划分、Hoare划分、随机化快排)
分治法在排序算法中的应用
快速排序:时间复杂度O(nlogn)
如果说归并排序是按照元素在数组中的位置划分的话,那么快速排序就是按照元素的值进行划分。划分方法由两种,本节将主要介绍Huare划分,在减治法在查找算法中的应用(JAVA)--快速查找这篇文章中讲述了Lomuto划分用于快速查找算法,下面会给出基于Lomuto的快排代码。
1、基于Lomuto划分的快速排序算法
public class Main {
static int[] a= {5, 3, 1, 9, 8, 2, 4, 7};
public static void main(String[] args) {
fastsort(0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static int Lomuto(int l, int r) {
int p = a[l];
int s = l;
for (int i = l+1; i <= r; i++) {
if (a[i] < p) {
s = s+1;
int temp = a[s];
a[s] = a[i];
a[i] = temp;
}
}
int temp = a[l];
a[l] = a[s];
a[s] = temp;
return s;
}
private static void fastsort(int l, int r) {
if (l < r) {
int s = Lomuto(l, r);
fastsort(l, s-1);
fastsort(s+1, r);
}
}
}
2、基于Hoare划分的快速排序
Hoare划分是一种更为复杂的划分方式,我们假设有一个数组a[0, n-1],其子数组为a[l, r](0 <= l <= r <= n-1),假定首个元素为枢轴p,下面从数组两端进行扫描,并将扫描到的元素与枢轴比较。从左到右扫描(用指针i来表示),扫描到第一个大于等于枢轴p的,停止;从右到左扫描(用指针j表示,遇到第一个小于等于枢轴的元素)。这里注意等于枢轴的元素也要进行处理,这样可以保证数组分的更加平均。如果遇到相等元素继续扫描,对于一个具有n个相同元素的数组来说,划分后得到的两个子数组长度可能为n-1和0。
两侧的扫描都停止之后,根据扫描指针是否相交会有三种情况:若 i < j,则交换a[i]与a[j],i+1,j-1;若 i > j,交换a[p]与a[j];若i = j,则交换a[p]与a[j]。眼尖的读者估计看出来了,后两种情况其实是一种。
下图为Hoare划分的示意图:
public class Main {
static int[] a= {5, 3, 1, 9, 8, 2, 4, 7};
public static void main(String[] args) {
fastsort(0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static int Hoare(int l, int r) {
int p = a[l];
int i = l-1;
int j = r+1 ;
while (true) {
do {
j--;
} while (a[j] > p);
do {
i++;
} while (a[i] < p);
if (i < j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
} else
return j;
}
}
private static void fastsort(int l, int r) {
if (l < r) {
int s = Hoare(l, r);
fastsort(l, s);
fastsort(s+1, r);
}
}
}
一般来说,如果我们要考虑一个算法的实用性,我们需要讨论的不是最坏情况下的效率,而应该是平均情况下的效率。实际经过分析,快速排序平均情况下的操作仅比最优情况多执行39%的比较过程而已。所以在处理随机序列时,速度要强于归并排序和堆排序。
缺点:快速排序并非稳定性排序方式,空间复杂度为O(logn),不及堆排序的空间复杂度O(1).
提出问题:对于数组非常小的情况下(对于大多数计算机来说,元素个数为5-15),使用插入排序会更快
解决思路1:判断元素个数,较小时采用插入排序,较大时采用快速排序。
解决思路2:不再对划分出来的较小数组排序,而是在快速排序结束后再使用插入排序的方法对整个接近有序的数组进行细微调节。
提出问题:如果我们每次都选取第一个元素为枢轴,这自然会有诸多不便,不能应对一些特殊的情况。
解决思路:随机化快速排序、三平均划分法,下面给出代码,基本思想还是划分,有兴趣的读者可以研究一下。
随机化快排:使用随机元素作为枢轴。
public class Main {
static int[] a= {5, 3, 1, 9, 8, 2, 4, 7};
public static void main(String[] args) {
fastsort(0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
private static int random_partition(int l, int r) {
int i = (int) (l + Math.random() % (r - l + 1));
int temp = a[i];
a[i] = a[r];
a[r] = temp;
return partition(l, r);
}
private static int partition(int l, int r) {
int p = a[r];
int i = l - 1;
for (int j = l; j < r; j++) {
if (a[j] <= p) {
i++;
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
int temp = a[i+1];
a[i+1] = a[r];
a[r] = temp;
return i+1;
}
private static void fastsort(int l, int r) {
if (l < r) {
int s = random_partition(l, r);
random_partition(l, s-1);
random_partition(s+1, r);
}
}
}
三平均划分法快排:以最左元素、最右元素、最中间元素的中位数为枢轴。
public class Main {
static int[] a= {5, 3, 1, 9, 8, 2, 4, 7};
public static void main(String[] args) {
fastsort(0, a.length-1);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
public static int mid_partition(int l, int r)
{
int range = r - l + 1;
int mid1 = (int) (l + Math.random() % range);
int mid2 = (int) (l + Math.random() % range);
int mid3 = (int) (l + Math.random() % range);
int mid = (a[mid1] < a[mid2]) ?
(a[mid2] < a[mid3] ? mid2 : (a[mid1] < a[mid3] ? mid3 : mid1)):
(a[mid1] < a[mid3] ? mid1 : (a[mid2] < a[mid3] ? mid2 : mid3));
int temp = a[mid];
a[mid] = a[r];
a[r] = temp;
return partition(l, r);
}
private static int partition(int l, int r) {
int p = a[r];
int i = l - 1;
for (int j = l; j < r; j++) {
if (a[j] <= p) {
i++;
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
int temp = a[i+1];
a[i+1] = a[r];
a[r] = temp;
return i+1;
}
private static void fastsort(int l, int r) {
if (l < r) {
int s = mid_partition(l, r);
fastsort(l, s-1);
fastsort(s+1, r);
}
}
}
提出问题:在划分方式上Lomuto划分和Hoare划分也可以进行改进,使用诸如三路划分的方式,将数组划分3段,每段元素分别小于、等于、大于枢轴元素等等。当然,这些就仅供研究学习了,平时我们使用最简单版本的快速排序是没有任何问题的。