快速排序算法详解(原理、实现和时间复杂度)

快速排序是对冒泡排序的一种改进,由 C.A.R.Hoare(Charles Antony Richard Hoare,东尼·霍尔)在 1962 年提出。

快速排序的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。

快速排序的原理

排序算法的思想非常简单,在待排序的数列中,我们首先要找一个数字作为基准数(这只是个专用名词)。为了方便,我们一般选择第 1 个数字作为基准数(其实选择第几个并没有关系)。接下来我们需要把这个待排序的数列中小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了;接着把两个分区的元素分别按照上面两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。

这是典型的分治思想,即分治法。下面我们对一个实际例子进行算法描述,讲解快速排序的排序步骤。

快速排序详解

以 “6 1 2 7 9 3 4 5 10 8” 的待排序的数列为例进行排序

接下来开始移动元素。怎么移动呢?其实冒泡排序也涉及对元素的移动,但是那样移动起来很累,比如把最后一个元素移动到第 1 个,就需要比较 n-1 次,同时交换 n-1 次,效率很低。其实,只需把第 1 个元素和最后一个元素交换就好了,这种思想是不是在排序时可以借鉴呢?之前说快速排序就是对冒泡排序的一个改进,就是这个原因。

快速排序的操作是这样的:首先从数列的右边开始往左边找,我们设这个下标为 i,也就是进行减减操作(i–),找到第 1 个比基准数小的值,让它与基准值交换;接着从左边开始往右边找,设这个下标为 j,然后执行加加操作(j++),找到第 1 个比基准数大的值,让它与基准值交换;然后继续寻找,直到 i 与 j 相遇时结束,最后基准值所在的位置即 k 的位置,也就是说 k 左边的值均比 k 上的值小,而 k 右边的值都比 k 上的值大。

假如用 “6 1 2 7 9 3 4 5 10 8” 这个初始序列来进行快速排序

分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。

首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。


现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:
6 1 2 5 9 3 4 7 10 8


到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:

6 1 2 5 4 3 9 7 10 8

第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:

3 1 2 5 4 6 9 7 10 8


到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。

OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。

左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧

如果你模拟的没有错,调整完毕之后的序列的顺序应该是:

2 1 3 5 4

OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下:

1 2 3 4 5 6 9 7 10 8

对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下

1 2 3 4 5 6 7 8 9 10

到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面上个霸气的图来描述下整个算法的处理过程。

这是为什么呢?

快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。我们后面还会遇到“二分”思想,到时候再聊。先上代码,如下

快速排序代码实现

其实快速排序有一个比较简单的思想,就是递归。对于每一趟排序都是一样的思想,只不过需要进行排序的数组的范围越来越小了,使用递归实现这种排序最适合不过了。

import com.sun.deploy.util.StringUtils;public class tets {public static void main(String[] args) {int[] data = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};System.out.println("排序之前:\n" + java.util.Arrays.toString(data));quickSort(data, 0, data.length - 1);System.out.println("排序之后:\n" + java.util.Arrays.toString(data));}public static void quickSort(int[] data, int low, int high) {int i, j, temp, t;if (low > high) {return;}i = low;j = high;//temp就是基准位temp = data[low];System.out.println("基准位:" + temp);while (i < j) {//因为所排列顺序是递增,而且每次哨兵排查结束在于左右哨兵是否相遇,所以右边哨兵负责寻找大于基准的数,左边哨兵寻找小于基准的数// ,如若需要改变快排后的结果顺序只需要改变左右哨兵对于基准数的大小判断//先看右边,依次往左递减 在未和左边哨兵相遇前寻找大于或者等于基准的数while (temp <= data[j] && i < j) {j--;}//再看左边,依次往右递增 在未和右边哨兵相遇前寻找小于或者等于基准的数while (temp >= data[i] && i < j) {i++;}//如果满足条件则交换  左右哨兵未相遇的情况下寻找到满足条件的数if (i < j) {System.out.println("交换:" + data[i] + "和" + data[j]);t = data[j];data[j] = data[i];data[i] = t;System.out.println(java.util.Arrays.toString(data));}}//最后将基准位与i和j相等位置的数字交换 左右哨兵相遇System.out.println("基准位" + temp + "和i、j相遇的位置" + data[i] + "交换");data[low] = data[i];data[i] = temp;System.out.println(java.util.Arrays.toString(data));//此时基准数左边都比基准数小,右边都比基准数大,故而基准数位置不变,基准数左右两边重新进行快排//递归调用左半数组quickSort(data, low, j - 1);//递归调用右半数组quickSort(data, j + 1, high);}
}

快速排序的特点及性能
快速排序是在冒泡排序的基础上改进而来的,冒泡排序每次只能交换相邻的两个元素,而快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。

但是快速排序在最坏情况下的时间复杂度和冒泡排序一样,是 O(n2),实际上每次比较都需要交换,但是这种情况并不常见。我们可以思考一下如果每次比较都需要交换,那么数列的平均时间复杂度是 O(nlogn),事实上在大多数时候,排序的速度要快于这个平均时间复杂度。这种算法实际上是一种分治法思想,也就是分而治之,把问题分为一个个的小部分来分别解决,再把结果组合起来。

快速排序只是使用数组原本的空间进行排序,所以所占用的空间应该是常量级的,但是由于每次划分之后是递归调用,所以递归调用在运行的过程中会消耗一定的空间,在一般情况下的空间复杂度为 O(logn),在最差的情况下,若每次只完成了一个元素,那么空间复杂度为 O(n)。所以我们一般认为快速排序的空间复杂度为 O(logn)

快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。

快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。

快速排序算法详解(原理,时间复杂度,实现代码)相关推荐

  1. 十大经典排序算法-快速排序算法详解

    十大经典排序算法 十大经典排序算法-冒泡排序算法详解 十大经典排序算法-选择排序算法详解 十大经典排序算法-插入排序算法详解 十大经典排序算法-希尔排序算法详解 十大经典排序算法-快速排序算法详解 十 ...

  2. 最小生成树-Prim算法详解(含全部代码)

    目录 适用条件 测试所用图 算法详解 Prim算法代码 全部代码 实验结果 适用条件 加权连通图 测试所用图 所用原图及生成过程 其中,(a) 为原图,圆圈里面是节点的名称,边上的数字是边的权值.由实 ...

  3. C语言实现扫雷完整算法详解~(附完整代码~)

    扫雷是一个常见小游戏,那么如何用C语言实现扫雷呢?学习了二维数组之后,我们可将扫雷的网格区域存储为二维数组,从而使用C语言实现扫雷. 目录 1.算法基本思路 2.算法详解 1.初始化数组与打印数组 2 ...

  4. 寻路算法——A*算法详解并附带实现代码

    一.前言 前天看了一篇博客介绍A*算法,按照自己的理解记录一下A*算法. 二.应用场景 一副地图中有坐标A和B,需要找到一条路径(如果有的话)能从A到B,地图中可能有河流或墙壁不能直接穿过,我们需要怎 ...

  5. 经典排序算法(2)——快速排序算法详解

    快速排序(Quick Sort)也是一种典型的交换排序算法,通过交换数据元素的位置进行排序. 一.算法基本思想 (1)基本思想 快速排序的基本思想就是:通过一趟排序将要排序的数据分割成独立的两部分,其 ...

  6. 快速排序 算法 详解 及 深度优化

    下面简介经典算法  快速排序算法 实现及优化. (欢迎大家指点,继续提出优化的方法,共同提高) 基本思想:(以按从小到大排序为例说明)通过多次的排序,每次的排序均将要排序的数组分为两部分,前一部分均比 ...

  7. 利用jquery操作ajax,利用jquery对ajax操作,详解原理(附代码)

    1.jQuery load() 方法 jQuery load() 方法是简单但强大的 AJAX 方法. load() 方法从服务器加载数据,并把返回的数据放入被选元素中. 语法: $(selector ...

  8. 解决最短路径的Dijkstra算法详解,附加Java代码

    1. 最短路径问题 最短路径问题是生活中经常碰到的一类问题,如机器人路径规划,数学竞赛以及真实的工程施工问题:甚至是我们程序员笔试必刷算法题.其实问题很简单,就是有很多个节点,我们要计算出一个初始点到 ...

  9. 模式搜索的KMP算法详解与C语言代码实现

    Table of Contents 模式搜索的KMP算法 如何更好地理解和掌握 KMP 算法? 推荐文章 Knuth-Morris-Pratt 字符串查找算法,简称为 "KMP算法" ...

最新文章

  1. Android 进程常驻(5)----开机广播的简单守护以及总结
  2. 文末赠书100本 | 当下最火爆的机器学习算法
  3. Vue3.x 使用ref和reactive、toRef
  4. MySQL的乱码解决方案
  5. 用Python实现磁盘IO操作全攻略,让数据流动起来!
  6. NGUI_2.6.3_系列教程二
  7. 人的效率还是机器的效率?
  8. 重庆航天职业技术学院计算机系在哪个校区,2020年重庆航天职业技术学院地址在哪里...
  9. 【图像重建】基于matlab GUI霍夫曼图像重建(带面板)【含Matlab源码 1168期】
  10. 分享一份非常强势的Android面试题
  11. SCSA---信息安全概述
  12. Unity学习笔记------用Unity2D制作打地鼠游戏
  13. KingabseES 锁机制
  14. powerdesigner 导入sql文件生成模型
  15. yocto编译linux,好实用!用 Yocto 在 Ubuntu 上创建最小化 Linux 发行版
  16. MNIST | 基于k-means和KNN的0-9数字手写体识别
  17. 区块链APP怎么开发?深入分析
  18. Ineffective mark-compacts near heap limit Allocation failed-JavaScript heap out of memory vue项目内存溢出
  19. 从单式记账到复式记账
  20. wordpress安装和使用图文教程(新手指南)

热门文章

  1. 机器学习作为人工智能的一个研究领域,主要研究哪些问题?
  2. 无盘服务器费用,无盘系统真的省钱吗?
  3. Consul的介绍、安装与使用
  4. Hasp HL加密狗模拟、解密硬复制Hasp HL
  5. Android TextView设置图标,调整图标大小
  6. javascript获取屏幕的可用宽度和高度
  7. 【VC皮肤】破解版skin++皮肤库使用
  8. 适合平面设计的无盘服务器配置,适合平面设计的电脑配置
  9. java找零钱_求一共有多少种方式系列问题(找零钱)
  10. Linux使用命令行复制文件的6个案例