快速排序
快速排序是对冒泡排序的一种改进, 它是不稳定的。由C. A. R. Hoare在1962年提出的一种划分交换排序,采用的是分治策略(一般与递归结合使用),以减少排序过程中的比较次数,它的最好情况O(nlogn),最坏情况O(n^2),平均时间复杂度为O(nlogn)。
基本思想
快速排序使用分治法策略来把一个序列分为两个子序列
1.将未排序序列看成一个元素的集合
2.在集合中选出一个主元
3.通过主元,将集合元素分为两个子集(子集:如果集合A的任意一个元素都是集合B的元素,那么集合A称为集合B的子集)
-
左边的子集元素均小于主元
-
右边的子集元素均大于主元
4.在子集中递归地执行上述操作,直到子集为空、或者仅有1个元素为止
实现过程
每一趟排序中找一个点pivot,将表分割成独立的两部分,其中一部分的所有都比pivot小,另一部分比pivot大,然后再按此方法对这两部分数据分别进行快速排序。为了方便,我们这里通常选择数组的第一个元素作为主元,再与最后一个元素交换。
-
设置两个下标变量i,j,i指向第一个元素,j指向倒数第二个元素
-
先让j从右到左扫描,如果发现比主元小的元素,则停止;然后让i从左到右扫描,如果发现比主元大的元素,则停止
-
如果i <= j,则交换i和j所在的元素
-
重复上两步,直到i > j为止,最后交换主元和i所在的元素
代码实现:
public class QuickSort {
public static void main(String[] args) {
int[] array = {6, 2, 4, 8, 9, 5, 7, 3, 1, 10};
System.out.println("排序之前的数组: " + Arrays.toString(array));
quickSort(array, 0, array.length - 1);
System.out.println("排序之后的数组: " + Arrays.toString(array));
}
public static void quickSort(int[] array,int start,int end){
if (start<end){
//选取数组第一个数做主元
int pivot = array[start];
int i = start;
int j = end;
while (i<j) {
//j从右往左扫描
while ((i<j)&&array[j]>=pivot) {
j--;
}
//i从左往右扫描
while ((i<j)&&array[i]<=pivot) {
i++;
}
if((array[i]==array[j])&&(i<j)){
i++;
}
else {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
//将主元为与i的位置交换
array[start]=array[i];
array[i]=pivot;
quickSort(array,start,j-1);
quickSort(array,j+1,end);
}else{
return;
}
}
}
输出的排序结果为:
排序之前的数组: [6, 2, 4, 8, 9, 5, 7, 3, 1, 10]
排序之后的数组: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
,这里基本实现了快速排序,还有许多优化和改进的地方。下面从几个方面来继续寻找更好的排序方式
主元的选择
如果随意选择一个主元,可能造成两个子集大小相差悬殊
我们期望的是每次选择主元都能把集合平均分成两个大致的子集合,但世界上做不到,比较好的方法是三数中值分割法(取最开始、最后、中间三个元素的中值作为主元)
-
将三个元素调整为: 最左元≤中值 ≤最右元。
-
然后让中值与Right-1交换(因为最右元必然大于中值,无需再比较)
相等的情形
如果遇到相等的元素不停止,在特殊情况下
因此我们选择遇到相等元素时,停止并交换。虽然多了很多无谓的交换,但可以让两个子集大小均衡。
另外,若集合元素很少,快速排序递归求解效率很低,甚至比插入排序低很多因此,在递归过程中,若集合元素个数少于某个值,就使用插入排序
下面是改进后的代码:
/**
* 选择排序
* @param array
* @param start
* @param end
*/
public static void quickSort(int[] array, int start, int end) {
//数组元素不小于cutoff时才使用快速排序
int cutoff=3;
if(start+cutoff<=end){
//主元,位置为right-1
int privot = median3(array, start, end);
int i = start, j = end - 1;
for (; ; ) {
while (array[++i] < privot) {
}//i向右遍历
while (array[--j] > privot) {
}//j向左遍历
if (i < j)
swap(array, i, j);
else {
break;
}
}
//for循环终止条件为i和j相遇,此时再将主元归位
swap(array, i, (end - 1));
quickSort(array, start, i - 1);//对左半部进行递归
quickSort(array, i + 1, end);//对右半部进行递归
}else{
//可以使用插入排序
array=insertionSort(array);
}
}
插入排序代码:
/**
* 插入排序
* @param array
* @return
*/
public static int[] insertionSort(int[] array) {
int len;
// 基本情况下的数组可以直接返回
if(array == null || (len = array.length) == 0 || len == 1) {
return array;
}
int current;
for (int i = 0; i < len - 1; i++) {
// 第一个数默认已排序,从第二个数开始
current = array[i + 1];
// 前一个数的下标
int preIdx = i;
// 拿当前的数与之前已排序序列逐一往前比较,
// 如果比较的数据比当前的大,就把该数往后挪一步
while (preIdx >= 0 && current < array[preIdx]) {
array[preIdx + 1] = array[preIdx];
preIdx--;
}
// while循环跳出说明找到了位置
array[preIdx + 1] = current;
}
return array;
}
median3和swap方法的代码:
/**
* 取最开始、最后、中间三个元素的中值
* @param array
* @param left
* @param right
* @return
*/
private static int median3(int[] array, int left, int right) {
int center = (left + right) / 2;
if (array[left] > array[center])
swap(array, left, center);
if (array[left] > array[right])
swap(array, left, right);
if (array[center] > array[right])
swap(array, center, right);
swap(array, center, right - 1);
return array[right - 1];
}
/**
* 交换数组中两个索引的值
* @param array
* @param i
* @param j
*/
private static void swap(int[] array, int i, int j) {
int temp;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
效率分析
-
最好情形下,每次划分都将原序列划分成两个基本等长的序列
-
随着递归层次的加深,子序列的数量翻倍
-
但在每一递归层次上(可以将递归过程看成一棵树),比较总次数都O(n)次
-
递归层次(深度)是log2n因此,快速排序的最好情形时间复杂度为O(nlogn)
-
平均时间复杂度也是O(nlogn)
-
最坏情形可能导致效率为O(n2)
-