快速排序(附Java实现和分析)
2016-04-07 12:21
666 查看
总结一下快速排序,如有错误或者不足,欢迎交流讨论。
点击这里选择QUICK即可查看动态执行情况。
对比三种实现,第二种最不好,相比第一种实现方式,同样一趟排序,平均情况下其交换次数第二种要比第一种至少多一倍;对比第一种和第三种实现方式,第三种访问数组的次数要少些,因为第一种采取的是交换,第一种很好理解,实现起来也容易,第三种代码紧凑些,理解稍微难那么点。
其实还可以5取样切分,那样会更接近中数,但是过于繁琐。
最后附上改进后的快速排序Java实现
T(N)= T(i)+T(N-i-1)+cN
N为数组规模,i为切分后其中较小一部分的元素个数,c为某一常数
(1)最坏情况
枢轴元素始终是最小元素,此时i始终为0,T(0)=T(1)=1,与问题规模无关,在一个地推公式中可以忽略掉,故得到:T(N)=T(N-1)+cN,反复使用该公式,直到N为2,然后累加。
(2)最好情况
最好情况下,枢轴元素是中数,为简化分析,假设两个子数组大小均为原数组的一半,分析和归并排序类似。
(3)平均情况
所以快速排序的空间复杂度为O(log2N)
关于具体的比较次数和交换次数以及访问数组的总次数,可参考《Algorithms Fourth Edition》,就时间复杂度和空间复杂度的分析,可不必这样做。
参考资料:
1. 《Algorithms Fourth Edition》
2. 《数据结构与算法分析:C语言描述》
3. VisuAlgo
4. /article/1337332.html
5. /article/5357705.html
1、快速排序的思路
快速排序和归并排序的思路很相似,都是采取的分治思想。快速排序通过选择一个元素,该元素称为枢轴元素或切分元素,然后将它放到一个合适的位置上,使得它前面的元素不大于它,它后面的元素不小于它,然后将枢轴元素为分界点,两边的数组也采取类似的方法,即选取枢轴元素,使得前面的元素不大于它,后面的不小于它,重复进行下去,直到数组里面只有一个元素(递归退出条件)。2、partition函数
从上述的描述来看,快速排序是需要递归的,递归地选取枢轴元素进行切分。所以,快速排序的实现重点是切分(partition)函数,即如何实现对于某一切分元素,使得它前面的元素不大于它,后面的不小于它。3.1 partition函数实现之一
《Algorithm Fourth Edition》上的思路:对于某一枢轴元素,从第一元素开始往后扫描,找到第一个大于它的元素,然后从最后一个元素往前扫描,找到第一个小于它的元素,交换两个元素。要注意扫描不能出现数组访问越界,且扫描开始位置不能相交。package c2Sorting; /** * 快速排序的第一种partition实现 * @author 小锅巴 * @date 2016年4月2日上午10:03:53 * http://blog.csdn.net/xiaoguobaf */ public class QuickSort_1 { public static void sort(int[] a){//驱动程序 sort(a, 0, a.length-1); } private static void sort(int[] a, int lo, int hi){ if(lo >= hi)//递归退出判断条件 return; int p = partition(a, lo, hi);//对于某一元素,其本身不必参与递归了,因为其所在的位置已经满足前面的不大于,后面的不小于 sort(a, lo, p-1); sort(a, p+1, hi); } private static int partition(int[] a, int lo, int hi){ int left = lo;//左pointer,供扫描用 int right = hi+1;//右pointer,供扫描用,加1是为了方便扫描的推进, int pivot = a[lo]; while(true){ while(a[++left] <= pivot)//从lo开始,找到大于pivot的元素,在访问数组时使用前++更安全,后++可能会发生越界 if(left == hi)//防止越界 break; while(a[--right] >= pivot )//从hi开始,找到小于pivot的元素 if(right == lo)//防止越界 break; if(left >= right)//左右扫描相交,迭代结束判断条件,相等的时候说明就是和pivot相等的元素 break; swap(a, left, right);//交换pivot前面大于pivot的元素和pivot后面小于pivot的元素, //从这里可以看出快速排序不稳定,因为两者之间存在和此时的left或者right相等的元素时,原有的顺序就被破坏了 } swap(a, lo, right);//将枢轴元素放到合适的位置 //pivot未交换到合适的位置之前,其他位置的元素都满足扫描条件了(两个while里面为真),然后再进行一次扫描,扫描条件均为假了,right<=left,right所在位置的元素是不大于pivot的 return right;//返回切分元素的位置 } private static void swap(int[] a, int i, int j){ //对于待排序数组中无重复元素时,可以使用异或操作来实现,但是如果有重复的,那么就不可以,重复的元素会被置为0 int temp = a[i]; a[i] = a[j]; a[j] = temp; } //单元测试 public static void main(String[] args) { int[] a = {3,4,1,9,3,2,1,6,8,4,7,5}; sort(a); for (int i = 0; i < a.length; i++) System.out.print(" "+a[i]+" "); } } /** * 输出: 1 1 2 3 3 4 4 5 6 7 8 9 * */
3.2 partition函数实现之二
选择第一个元素为枢轴元素,使用index为当前扫描元素的pointer,storIndex表示枢轴元素后面最后一个小于枢轴元素的pointer,从枢轴元素后面的第一个元素开始从左往右扫描,若当前扫描的元素比枢轴元素小,则交换index与++storIndex的元素(即第一个不小于枢轴元素的元素),进行一趟扫描后,将枢轴元素与storIndex的元素相交换,以将枢轴元素放到合适的位置。点击这里选择QUICK即可查看动态执行情况。
package c2Sorting; /** * 快速排序的第二种partition实现 * @author 小锅巴 * @date 2016年4月2日下午4:24:47 * http://blog.csdn.net/xiaoguobaf */ public class QuickSort_2 { public static void sort(int[] a){ sort(a, 0, a.length-1); } private static void sort(int[] a, int lo, int hi){ if(lo >= hi) return; int p = partition(a, lo, hi); sort(a, lo, p-1); sort(a, p+1, hi); } private static int partition(int[] a, int lo, int hi){ //我在实现这个partition函数时,感觉访问数组时使用后++很不安全,搞不好会出现栈溢出、空指针异常 int index=lo;//当前扫描元素的pointer int storIndex = lo;//最后一个小于枢轴元素的pointer while(++index <= hi) if(a[index] < a[lo]) swap(a,index,++storIndex);//交换当前元素与第一个不小于枢轴元素的元素 swap(a,lo,storIndex);//将枢轴元素放到合适的位置 return storIndex;//返回枢轴元素的位置,即索引 } private static void swap(int[] a, int i, int j){ int temp = a[i]; a[i] = a[j]; a[j] = temp; } //单元测试 public static void main(String[] args) { int[] a = {3,4,1,9,3,2,1,6,8,4,7,5}; sort(a); for (int i = 0; i < a.length; i++) System.out.print(" "+a[i]+" "); } } /** 输出: 1 1 2 3 3 4 4 5 6 7 8 9 */
3.3 partition函数实现之三
先将枢轴元素临时保存起来,从右往左扫描,找到第一个小于枢轴元素的元素,将其放到枢轴元素的位置,然后从左往右扫描,找到第一个大于枢轴元素的元素,将它放到之前第一个小于枢轴元素的位置。package c2Sorting; /** * 快速排序的第三种partition实现 * @author 小锅巴 * @date 2016年4月2日上午11:39:05 * http://blog.csdn.net/xiaoguobaf */ public class QuickSort_3 { public static void sort(int[] a){ sort(a, 0, a.length-1); } private static void sort(int[] a, int lo, int hi){ if(lo >= hi) return; int p = partition(a, lo, hi); sort(a, lo, p-1); sort(a, p+1, hi); } private static int partition(int[] a, int lo, int hi){ int pivot = a[lo]; while(lo < hi){ while(lo < hi && a[hi] >= pivot) hi--; a[lo] = a[hi];//将从右到左第一小于pivot的元素放到切分元素的位置 while(lo < hi && a[lo] <= pivot) lo++; a[hi] = a[lo];//将上一步的位置填充为从左到右第一个大于pivot的元素,此时的lo位置的元素已经不是pivot了 } a[lo] = pivot;//退出时,lo=hi了,此位置即是切分元素应该插入的正确位置 return lo; } //单元测试 public static void main(String[] args) { int[] a = {3,4,1,9,3,2,1,6,8,4,7,5}; sort(a); for (int i = 0; i < a.length; i++) System.out.print(" "+a[i]+" "); } } /** 输出: 1 1 2 3 3 4 4 5 6 7 8 9 */
对比三种实现,第二种最不好,相比第一种实现方式,同样一趟排序,平均情况下其交换次数第二种要比第一种至少多一倍;对比第一种和第三种实现方式,第三种访问数组的次数要少些,因为第一种采取的是交换,第一种很好理解,实现起来也容易,第三种代码紧凑些,理解稍微难那么点。
4、快速排序的改进
4.1 改进枢轴元素的选取
最好情况下,枢轴元素应该是所有元素的平均值,即中值,这样就更接近归并排序的切分情况。但是前面的三种partition实现都是选取的第一个元素为枢轴元素,并不能有这个保证,采取三数中值法(三取样切分),比较lo,mid,hi的大小,选取中间的一个作为枢轴元素。//三取样切分 private static int threeMedium(int[] a, int lo, int mid, int hi){ return ( a[lo]<a[mid] ) ? ( a[mid]<a[hi] ? mid : (a[lo]<a[hi]) ? hi : lo ): ( a[lo]<a[hi] ? lo : a[mid]<a[hi] ? hi : mid ); }
其实还可以5取样切分,那样会更接近中数,但是过于繁琐。
4.2 切换到插入排序
对于小规模数组,插入排序够用了,用快速排序多次切分访问数组的次数将比插入排序多些,还不如用插入排序,故数组规模较小时,切换到插入排序。最后附上改进后的快速排序Java实现
package c2Sorting;
/**
* 改进的快速排序
* @author 小锅巴
* @date 2016年4月6日下午10:38:53
* http://blog.csdn.net/xiaoguobaf */
public class QuickSort {
private static final int CUTOFF = 10;//若数组大小不超过CUTOFF,则切换到插入排序
public static void sort(int[] a){
sort(a, 0, a.length-1);
}
private static void sort(int[] a, int lo, int hi){
if(lo+CUTOFF >= hi){//切换到插入排序,调用插入排序后直接返回
insertionSort(a);
return;
}
if(lo >= hi)
return;
//将三取样的中数和lo交换
int m = threeMedium(a, lo, lo+(hi-lo)/2, hi);
swap(a, m, lo);
int p = partiton(a, lo, hi);
sort(a, lo, p-1);
sort(a, p+1, hi);
}
private static int partiton(int[] a, int lo, int hi){
int pivot = a[lo];
while(lo < hi){
while(lo < hi && a[hi] >= pivot)
hi--;
a[lo] = a[hi];
while(lo < hi && a[lo] <= pivot)
lo++;
a[hi] = a[lo];
}
a[lo] = pivot;
return lo;
}
//三取样切分 private static int threeMedium(int[] a, int lo, int mid, int hi){ return ( a[lo]<a[mid] ) ? ( a[mid]<a[hi] ? mid : (a[lo]<a[hi]) ? hi : lo ): ( a[lo]<a[hi] ? lo : a[mid]<a[hi] ? hi : mid ); }
private static void swap(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
//切换到插入排序
private static void insertionSort(int[] a){
for(int i = 1 ; i < a.length;i++)
for(int j = i ; j > 0 && a[j] < a[j-1];j--)
swap(a, j, j-1);
}
//单元测试
public static void main(String[] args) {
int[] a = {3,4,1,9,3,2,1,6,8,4,7,5};
sort(a);
for (int i = 0; i < a.length; i++)
System.out.print(" "+a[i]+" ");
}
}
/**
输出: 1 1 2 3 3 4 4 5 6 7 8 9
*/
5、快速排序的分析
5.1 时间复杂度
快速排序和归并排序一样,都使用了递归,故可以借助递归公式来分析。对于一个未采取插入排序转换和三取样切分的快速排序,其运行时间等于两个子数列排序的时间加上在切分上花费的时间,因为扫描,显然切分花费的时间与数组规模正相关,故得到地推公式:T(N)= T(i)+T(N-i-1)+cN
N为数组规模,i为切分后其中较小一部分的元素个数,c为某一常数
(1)最坏情况
枢轴元素始终是最小元素,此时i始终为0,T(0)=T(1)=1,与问题规模无关,在一个地推公式中可以忽略掉,故得到:T(N)=T(N-1)+cN,反复使用该公式,直到N为2,然后累加。
(2)最好情况
最好情况下,枢轴元素是中数,为简化分析,假设两个子数组大小均为原数组的一半,分析和归并排序类似。
(3)平均情况
5.2 空间复杂度
在最好情况和平均情况下,sort递归的次数是log2N次,partition返回的枢轴元素的位置的局部变量所占用得空间就是log2N次,partition函数里面的局部变量也是与log2N成正比,即空间复杂度是O(log2N);在最坏情况下,sort递归次数是N^2,此时的空间复杂度将是O(N^2),但是这样的概率很小,经过三取样后就减小了,如果排序前打乱数组,那么这种情况出现的概率可以忽略不计,证明请参考《Algorithms Fourth Edition》。所以快速排序的空间复杂度为O(log2N)
5.3 稳定性
不稳定有两个地方,第一个地方已经在第一种实现里面提到了,第二地方在partition函数返回前将枢轴元素放到正确位置,若待放位置前有和枢轴元素值相等的元素,则破坏了稳定性。关于具体的比较次数和交换次数以及访问数组的总次数,可参考《Algorithms Fourth Edition》,就时间复杂度和空间复杂度的分析,可不必这样做。
参考资料:
1. 《Algorithms Fourth Edition》
2. 《数据结构与算法分析:C语言描述》
3. VisuAlgo
4. /article/1337332.html
5. /article/5357705.html
相关文章推荐
- 【算法】超大数组去重(Java语言实现)
- 应该被记住的 8 位 Java 人物
- Eclipse
- (MyEclipse配置Hibernate报错)SQL Error: 1146, SQLState: 42S02
- eclipse调试一个struts2例子时遇到的一些问题总结
- java.net.URISyntaxException的解决办法
- Spring中配置和读取多个Properties文件
- Java中 static/transient,final/volatile 说明
- 堆排序--Java实现
- Java ArrayList.toArray(T[]) 方法的参数类型是 T 而不是 E的原因分析
- Spring事务属性详解
- HIbernate4开发要注意的新特性
- 设计模式---责任链模式
- Spring框架的Portlet MVC
- 个人Java学习第一天总结
- Java操作Excel文件
- Java入门:构造方法
- RTL support api level < 15
- java中null与空字符串的区别
- spring无事务的数据源切换,和带事务的数据源切换