您的位置:首页 > 编程语言

排序算法——快速排序的图解、代码实现以及时间复杂度分析

2018-02-26 13:19 711 查看

快速排序

在C++的泛型排序中,拷贝对象需要很大的开销,而比较对象常常是相对省时的(编译器的自动优化)。在这种情况下,如果我们能够使用更少的数据移动,那么有理由让一个算法多使用一些比较。而快速排序(Quicksort)满足了这种特点,实际上C++中通常所使用的排序例程就是使用的快速排序。

快速排序也是一种分治的递归算法。它的平均运行时间是O(NlogN),最坏情形性能为O(N2)。

经典快速排序

将数组S排序的基本算法由下列简单的四步组成:

如果S中元素个数是0或1,则返回

取S中的任一元素V,称之为枢纽元(pivot)

将S-{V}(S中的其他元素)划分成两个不相交的集合:S1={小于V的元素},S2={大于V的元素}。

返回{quicksort(S1)后跟V,继而返回quicksort(S2)}

实现第2步和第3步有很多方法,下面介绍的方法是大量分析和实验的结果。

一、选取枢纽元

虽然上面说随机选取一个元素作为枢纽元,但是有些选择显然优于其他选择。

一种错误的方法

通常的、无知的选择是将第一个元素用作枢纽元。如果输入是随机的,那么这是可以接受的,而如果输入是预排序的或是反序的,那么所有的元素不是都被划入S1就是都被划入S2,这将花费二次时间。而且,预排序的输入(或具有一大段预排序数据)是相当常见的。因此,使用第一个元素作为枢纽元是绝对可怕的坏主意。

一种安全的做法

一种安全的方法是随机选取枢纽元。一般来说这种策略非常安全,因为随机的枢纽元不可能总在接连不断地产生劣质的分割。另一方面,随机数的生成一般开销很大,根本减少不了算法其余部分的平均运行时间。

三数中值分割法(Median-of-Three Partitioning)

枢纽元的最好选择是数组的中值。不幸的是,这很难算出并且会明显减慢快速排序的速度。一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的坏情形,并且实际减少了14%的比较次数。

二、分割策略

暂时假设所有的元素互异。一种高效的做法是,将枢纽元与最后一个元素交换使得枢纽元脱离分割,i从第一个元素开始而j从倒数第二个元素开始。

在分割阶段要做的就是把所有小元素移到数组的左边而把所有的大元素移到数组的右边,当然,小和大是相对于枢纽元而言的。

i右移到大于枢纽元的位置,j左移到小于枢纽元的位置,如果i < j ,那么交换i、j对应的元素,其效果是把一个大元素推向右边而把小元素推向左边。以此类推,知道i=j。分割的最后一步是将枢纽元与i所指向的元素交换。

三、图解演示

以序列8,1,4,9,6,3,5,2,7,0为例,最左边元素为8,右边元素是0,中心位置元素是6,于是选定枢纽元pivot=6。

一轮快速排序的图解步骤如下:



递归进行,直至最终有序。

注意,对于很小的数组(N<=20),快速排序不如插入排序。通常的解决方法是对于小的数组不使用递归的快速排序,而使用诸如插入排序这样的对小数组有效的排序算法。使用这种策略实际上可以节省大约15%的运行时间。

四、代码实现

完整的Java代码实现

public class QuickSort2 {

private static final int CUTOFF = 3;
/**
* 快速排序的外部接口,供用户调用
* @param a 需要排序的数组
* @param <AnyType> 数组的类型
*/
public static <AnyType extends Comparable<? super AnyType>>
void quicksort( AnyType [ ] a )
{
quicksort( a, 0, a.length - 1 );
}

/**
* 3数中值分割法求枢纽值。
* 将左、中、右3个位置上的数组进行排序,并将中值放到倒数第二位隐藏。
* @param a 需要排序的数组
* @param left 左起始位置
* @param right 右终止位置
* @param <AnyType> 数组类型
* @return 枢纽值pivot
*/
private static <AnyType extends Comparable<? super AnyType>> AnyType median3(AnyType[] a,int left, int right){
int center = (left+right)/2;
if (a[center].compareTo(a[left])<0)
swapReferences( a, left, center );
if( a[ right ].compareTo( a[ left ] ) < 0 )
swapReferences( a, left, right );
if( a[ right ].compareTo( a[ center ] ) < 0 )
swapReferences( a, center, right );
//处理特殊情形
if (center == right)
return a[right];
//将枢纽值放到倒数第二位
swapReferences( a, center, right-1);
return a[ right - 1 ];
}

/**
* 实现递归调用的快速排序内部方法。
* 使用3数中值分割法median3来获取枢纽值。
* @param a 需要排序的数组
* @param left 排序的左起始位置
* @param right 排序的右终止位置
* @param <AnyType> 数组类型
*/
private static <AnyType extends Comparable<? super AnyType>>
void quicksort( AnyType [ ] a, int left, int right ){
if( left + CUTOFF <= right ) {
//获取枢纽值
AnyType pivot = median3(a, left, right);
int i = left, j = right - 1;
for (; ; ) {
/*
从左右两边开始向中间遍历,注意++i和--j,它们是自增(减)操作,再进行比较,
之所以这样,是因为median3方法中已经将左、中、右3个位置排过序了
*/
while (a[++i].compareTo(pivot) < 0) {
}
while (a[--j].compareTo(pivot) > 0) {
}
if (i < j)
swapReferences(a, i, j);
else
break;
}
//最后一步,将枢纽值放回恰当的位置
swapReferences(a, i, right - 1);   // Restore pivot

//对左右子数组进行递归排序
quicksort(a, left, i - 1);
quicksort(a, i + 1, right);
}
else
median3(a,left,right);
}

/**
* 交换数组指定两个位置的值
* @param a 执行交换的数组
* @param index1 执行交换的位置1
* @param index2 执行交换的位置2
* @param <AnyType> 数组类型的泛型替代
*/
public static <AnyType> void swapReferences(AnyType[] a,int index1, int index2){
AnyType tmp = a[index1];
a[index1] = a[index2];
a[index2] = tmp;
}
}


测试代码

public static void main(String[] args) {
Integer a[]={7,1,4,5,9,8,10,2};
QuickSort2.quicksort(a);
for (int i :a) {
System.out.println(i);
}
}


测试结果

1
2
4
5
7
8
9
10


五、快速排序的复杂度分析

正如归并排序那样,快速排序也是递归的。它的分析需要求解递推公式(有兴趣的可以参考《数据结构与算法分析 Java语言描述 第三版》203页)。

最坏情况下(例如前面所说,数组倒序,枢纽元选第一位)快速排序将花费O(N2)时间。

最好情况下,快速排序和归并排序一样,花费O(NlogN)时间。

平均情况下,快速排序花费O(NlogN)时间。

六、总结

完整项目已经更新到github,访问地址为:https://github.com/Dodozhou/Algorithm,包路径为:src/main/java/algorithm/quicksort
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐