快速排序介绍

快速排序(Quick Sort)使用分治法策略,其基本思想是:通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另外一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

快排流程:

  1. 从数列中选取一个基数
  2. 将所有比基数小的摆放在基数前面,所有比基数大的摆在基数的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
  3. 递归地把"基数前面的子数列"和"基数后面的子数列"进行快速排序。

举例说明:

(PS:本例参考一个大神的博客:http://www.cnblogs.com/skywang12345/p/3596746.html#a1)

数列a={30,40,60,10,20,50}为例,演示它的快速排序过程(如下图)



上图只是给出了第1趟快速排序的流程,按照同样的方法,对子数列进行递归遍历。最后可以得到有序序列。

代码实现:

  1. //对顺序表elem进行快速排序
  2. public void quickSort(int[] elem) {
  3. QSort_2(elem, 1, elem.length - 1);
  4. }
  5. //对顺序表elem中的子序列elem[start...end]做快速排序
  6. public void QSort(int[] elem, int start, int end) {
  7. int pivot;
  8. if(start < end) {
  9. pivot = Partition(elem ,start, end);
  10. QSort(elem, start, pivot - 1);
  11. QSort(elem, pivot + 1, end);
  12. }
  13. }
  14. /**
  15. * 交换顺序表elem中字表记录,使基数记录到位,并返回其所在位置
  16. * 此时在它之前(后)的记录均不大(小)于它
  17. * @param elem
  18. * @param low
  19. * @param high
  20. * @return
  21. */
  22. public int Partition(int[] elem, int low, int high) {
  23. int pivotkey = elem[low];
  24. while(low < high) {
  25. while((low < high) && (elem[high] >= pivotkey)) {
  26. high--;
  27. }
  28. swap(elem, low, high);
  29. while((low < high) && (elem[low] <= pivotkey)) {
  30. low++;
  31. }
  32. swap(elem, low, high);
  33. }
  34. return low;
  35. }

快速排序复杂度分析

(PS:由于本人数学功底太弱,并没有理解快排复杂度的推演公式,在此只是摘抄于《大话数据机构》)

在最优情况下,如果排序n个关键字,其递归树的深度就是[log2log_2log2​n] + 1([x]表示不大于x的最大整数),即仅需要递归log2log_2log2​n次,需要时间为T(n)的话,第一次Partiation应该是需要对整个数组扫描一遍,做n次比较。然后获得的基数将数组一分为二,那么各自还需要T(n2\frac{n}{2}2n​)的时间(注意这是最好情况下,所以平分两半)。于是不断划分下去,就有下面不等式推断

T(n) ≤ 2T(n2\frac{n}{2}2n​) + n, T(1)=0

T(n) ≤2(2T(n4\frac{n}{4}4n​)+n2\frac{n}{2}2n​) + n=4T(n4\frac{n}{4}4n​) +2n

T(n) ≤ 4(2T(n8\frac{n}{8}8n​)+n4\frac{n}{4}4n​) + 2n=8T(n8\frac{n}{8}8n​) +3n



T(n) ≤ nT(1) + (log2log_2log2​n) * n= O(nlog⁡\loglogn)

也就是说在最优情况下,快排算法的时间复杂度为O(nlog⁡\loglogn)

在最坏情况下,待排序的序列为正序或者逆序,每次划只得到一个比上次划分少一个记录的子序列,注意另一个是空。如果画出递归树,那么就是一棵斜树。此时需要执行n-1次递归调用,且第i次划分需要经过n-i次关键字的比较才能找到第i个记录,也就是基数的位置,因此比较次数为:

∑i=1n−1(n−i)=n−1+n−2+...+1=n(n−1)2\sum_{i=1}^{n-1}(n-i)=n-1+n-2+...+1= \frac{n(n-1)}{2}i=1∑n−1​(n−i)=n−1+n−2+...+1=2n(n−1)​

最终其时间复杂度为O(n2n^2n2)

平均情况下,设基数的关键字应该在第k的位置(1≤k≤n),那么

T(n)=1n∑k=1n(T(k−1)+T(n−k))+n=2n∑k=1nT(k)+nT(n)=\frac{1}{n}\sum_{k=1}^{n}(T(k-1) + T(n-k))+n= \frac{2}{n}\sum_{k=1}^{n}T(k)+nT(n)=n1​k=1∑n​(T(k−1)+T(n−k))+n=n2​k=1∑n​T(k)+n

有数学归纳法可证明,其数量级为O(nlog⁡\loglogn)

就空间复杂度来说,主要是递归造成的栈空间的使用,最好情况下递归树的深度为log2log_2log2​n,其空间复杂度也就为O(log⁡\loglogn),最坏情况下,需要进行n-1次递归调用,其空间复杂度为O(n),平均情况下,空间复杂度为O(log⁡\loglogn)。

由于关键字的比较和交换是跳跃式的进行,所以快速排序是一种不稳定的排序方法。

快速排序的优化

优化选取基数

采用三数取中法,即取三个关键字先进性排序,将中间数做为基数,一般是取左端,右端和中间三个数。

  1. //三分取中法
  2. public int Partition_1(int[] elem, int low, int high) {
  3. int pivotkey;
  4. int m = low + (high - low) / 2;
  5. if(elem[low] > elem[high]) {
  6. swap(elem, low, high);
  7. }
  8. if(elem[m] > elem[high]) {
  9. swap(elem, high, m);
  10. }
  11. if(elem[m] > elem[low]) {
  12. swap(elem, low, m);
  13. }
  14. pivotkey= elem[low];
  15. while(low < high) {
  16. while((low < high) && (elem[high] >= pivotkey)) {
  17. high--;
  18. }
  19. swap(elem, low, high);
  20. while((low < high) && (elem[low] <= pivotkey)) {
  21. low++;
  22. }
  23. swap(elem, low, high);
  24. }
  25. return low;
  26. }
优化不必要的交换
  1. //优化不必要的交换
  2. public int Partition_2(int[] elem, int low, int high) {
  3. int pivotkey = elem[low];
  4. elem[0] = pivotkey;
  5. while(low < high) {
  6. while((low < high) && (elem[high] >= pivotkey)) {
  7. high--;
  8. }
  9. elem[low] = elem[high];
  10. while((low < high) && (elem[low] <= pivotkey)) {
  11. low++;
  12. }
  13. elem[high] = elem[low];
  14. }
  15. elem[low] = elem[0];
  16. return low;
  17. }

采用替换而不是交换的方式进行操作,在性能上得到部分提升。

优化小数组时的排序

数组非常小,其快速排序不如直接插入(直插是简单排序中性能最好的)。

  1. public final int MAX_LENGTH_INSERT_SORT = 7;
  2. //优化小数组时的排序方案
  3. public void QSort_1(int[] elem, int start, int end) {
  4. int pivot;
  5. if((end - start) > MAX_LENGTH_INSERT_SORT) {
  6. pivot = Partition(elem ,start, end);
  7. QSort(elem, start, pivot - 1);
  8. QSort(elem, pivot + 1, end);
  9. }
  10. else insertSort(elem);
  11. }
  12. //直接插入
  13. public void insertSort(int[] elem) {
  14. int i, j;
  15. for (i = 2; i < elem.length; i++) {
  16. if(elem[i] < elem[i - 1]) {
  17. elem[0] = elem[i];
  18. for (j = i - 1; elem[j] > elem[0]; j--) {
  19. elem[j + 1] = elem[j];
  20. }
  21. elem[j + 1] = elem[0];
  22. }
  23. }
  24. }
优化递归操作

在QSort函数在其尾部有两次递归操作,若待排序序列极端不平衡,递归深度趋近于n,每次递归调用都会浪费栈空间,因此能够减少递归,将会大大提升性能。

对QSort进行尾递归操作:

  1. //优化递归操作
  2. public void QSort_2(int[] elem, int start, int end) {
  3. int pivot;
  4. if((end - start) > MAX_LENGTH_INSERT_SORT) {
  5. while(start < end) {
  6. pivot = Partition(elem ,start, end);
  7. QSort(elem, start, pivot - 1);
  8. start = pivot + 1;
  9. }
  10. }
  11. else insertSort(elem);
  12. }

因为第一次循环后start就没了作用,可将pivot+1赋给start,再循环后,来一次Partition(elem, low, high),其效果等同于“QSort(elem, pivot+1, end)”,结果相同,但采用迭代而不是递归可以缩减堆栈深度,从而提高性能。

快速排序的理解和实现(Java)的更多相关文章

  1. Atitit  深入理解命名空间namespace  java c# php js

    Atitit  深入理解命名空间namespace  java c# php js 1.1. Namespace还是package1 1.2. import同时解决了令人头疼的include1 1.3 ...

  2. 理解和解决Java并发修改异常ConcurrentModificationException(转载)

    原文地址:https://www.jianshu.com/p/f3f6b12330c1 理解和解决Java并发修改异常ConcurrentModificationException 不知读者在Java ...

  3. 深入理解和探究Java类加载机制

    深入理解和探究Java类加载机制---- 1.java.lang.ClassLoader类介绍 java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字 ...

  4. 深入理解什么是Java泛型?泛型怎么使用?【纯转】

    本篇文章给大家带来的内容是介绍深入理解什么是Java泛型?泛型怎么使用?有一定的参考价值,有需要的朋友可以参考一下,希望对你们有所助. 一.什么是泛型 “泛型” 意味着编写的代码可以被不同类型的对象所 ...

  5. java中快速排序的理解以及实例

    所谓的快速排序的思想就是,首先把数组的第一个数拿出来做为一个key,在前后分别设置一个i,j做为标识,然后拿这个key对这个数组从后面往前遍历,及j--,直到找到第一个小于这个key的那个数,然后交换 ...

  6. [转载] 深入理解Android之Java虚拟机Dalvik

    本文转载自: http://blog.csdn.net/innost/article/details/50377905 一.背景 这个选题很大,但并不是一开始就有这么高大上的追求.最初之时,只是源于对 ...

  7. 如何理解和使用Java package包

    Java中的一个包就是一个类库单元,包内包含有一组类,它们在单一的名称空间之下被组织在了一起.这个名称空间就是包名.可以使用import关键字来导入一个包.例如使用import java.util.* ...

  8. 深入理解JVM(6)——Java内存模型和线程

    Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(“即Ja ...

  9. 理解JVM之Java内存区域

    Java虚拟机运行时数据区分为以下几个部分: 方法区.虚拟机栈.本地方法栈.堆.程序计数器.如下图所示: 一.程序计数器 程序计数器可看作当前线程所执行的字节码行号指示器,字节码解释器工作时就是通过改 ...

随机推荐

  1. "我们分手吧。"女的对男的说。 "为什么呢?亲爱的,你不要我了么?" "因为你幼稚。"女的坚定地语气回答道,然后转身准备走。 男的上前踩住女的影子,然后说...

    1."我们分手吧."女的对男的说. "为什么呢?亲爱的,你不要我了么?" "因为你幼稚."女的坚定地语气回答道,然后转身准备走. 男的上前踩 ...

  2. git将本地仓库强制替换掉远程仓库

    $ git remote add origin <url> $ git push --force --set-upstream origin master

  3. A标签中传递的中文参数到Servlet 后台request.getParameter()接收时出现中文乱码

    package util; import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequ ...

  4. 基础的linux学习

    学习了这几个命令分享一下: 文本文件内搜索数据 grep -n -e pattern1 -e pattern2 file1 -n 搜索到的数据显示行号展示 -e pattern1 多个匹配模式下可以通 ...

  5. AE(ArcEngine)定制工具Tool工具箱

    using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServi ...

  6. POJ3026 Borg Maze 2017-04-21 16:02 50人阅读 评论(0) 收藏

    Borg Maze Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 14165   Accepted: 4619 Descri ...

  7. hdu 2189 悼念512汶川大地震遇难同胞——来生一起走

    题目 这道题用了,埃式筛选法和背包,我自己没有做出来,看了别人的代码,我也做不出来,特别是c[j]+=c[j-b[i]];弄了好久都没有弄懂. 这道题的解题思路:主要是先把150以内的所有素数找出来, ...

  8. 层层递进Struts1(三)之Struts组成

    这篇博客我们来说一下Struts的主要组成我们,通过前几篇博客,我们知道这个框架最重要的几个步骤:获取路径.封装表单.获取转向列表.转向逻辑处理.转向,与此对应的是:ActionServlet.Act ...

  9. paip.双网卡多网卡不能上网的联网配置

    paip.双网卡多网卡不能上网的联网配置 作者Attilax ,  EMAIL:1466519819@qq.com  来源:attilax的专栏 地址:http://blog.csdn.net/att ...

  10. LINUX中关于SIGNAL的定义

    /* Signals. */ #define SIGHUP 1 /* Hangup (POSIX). */ #define SIGINT 2 /* Interrupt (ANSI). */ #defi ...