我叫水水,很高兴认识大家!

这是专栏的第七篇文章。其实本专题已经在我的公众号(公众号中不只有学习专题,还有很多大学学习资源分享、工具分享等等,文末有相关指路哦,欢迎关注撒~【微信搜索“CodingBugott”即可~】)中作为寒假学习专题持续更新了,所以想更新到知乎上,让更多的小伙伴可以看到啦~

好了,说回正题,本次的主题是排序算法。

那么我们正式开始吧~

排序,可能是对于计算机相关专业的小伙伴而言,会学习到的第一个算法了。在平常的编码中,我们也经常会用到排序。因此排序算法的重要性显而易见,而以下的篇幅便是简略展开经典的排序算法了。

而在学习算法之前,我们首先来看看应该从哪几个方面去评价、分析一个排序算法呢?以下有几个方面可供参考。

1、排序算法的执行效率:主要从其时间复杂度(系数、常数、低阶还是高阶)以及算法中的比较次数和交换次数;

2、排序算法的内存消耗:粗略而言,也就是空间复杂度;

3、排序算法的稳定性:针对排序算法,还有一个重要的度量指标——稳定性。这是指如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。举个栗子,比如我们有一组数据3,8,4,6,7,3,按照大小排序之后就是3,3,4,6,7,8。这组数据里有两个3。经过某种排序算法排序之后,如果两个3的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法;如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法。

以下开始简单讲解各类常用的排序算法。

冒泡排序(Bubble Sort)

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

举个栗子,比如要对38,49,65,76,13,27,30,97进行排序(见下图),结合前面对于冒泡排序的定义,下图中的“每一趟”代表着每一次冒泡,而每一次冒泡的过程其实也即是从数据首部开始,让相邻的两个元素进行比较,将其中较大的元素放在后面,然后依次进行比较直至到达数据末端,即完成“一次冒泡”——将最大的元素放置于最后。然后依次类推,排除开最后的最大元素不参与排序外,其余元素继续进行新的一轮冒泡,重复上述操作。在n(n代表需要排序的元素个数)次冒泡以后,完成排序。

那么我们下面我们看看代码是如何实现的。

 1// 冒泡排序,a 表示数组,n 表示数组大小2public void bubbleSort(int[] a, int n) {3 if (n <= 1) return;4 for (int i = 0; i < n; ++i) {5    // 提前退出冒泡循环的标志位6    boolean flag = false;7    for (int j = 0; j < n - i - 1; ++j) {8         if (a[j] > a[j+1]) { // 交换9           int tmp = a[j];
10           a[j] = a[j+1];
11           a[j+1] = tmp;
12           flag = true;  // 表示有数据交换
13         }
14       }
15    if (!flag) break;  // 没有数据交换,提前退出
16  }
17}

从以上代码可以分析得出,

冒泡排序的空间复杂度为O(1),即是一个原地排序算法。

而且在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法

而对于平均时间复杂度而言,下面我们来利用以下两个新的概念——“有序度”和“逆序度”来分析一下:有序度是数组中具有有序关系的元素对的个数。比如 6,5,4,3,2,1,有序度是 0;对于一个完全有序的数组,比如 1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是15。我们把这种完全有序的数组的有序度叫作满有序度逆序度的定义正好跟有序度相反(默认从小到大为有序)。关于这三个概念,还可以得到一个公式:逆序度 = 满有序度 - 有序度。因此排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。

那我们回到冒泡排序中,它包含两个操作原子,比较交换。每交换一次,有序度就加 1。不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是[n*(n-1)/2–初始有序度]。

那么,对于包含 n 个数据的数组进行冒泡排序,平均交换次数是多少呢?最坏情况下,初始状态的有序度是 0,所以要进行 n*(n-1)/2 次交换。最好情况下,初始状态的有序度是 n*(n-1)/2,就不需要进行交换。我们可以取个中间值 n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。

换句话说,平均情况下,需要 n*(n-1)/4 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 O(n2),所以平均情况下的时间复杂度就是 O(n2)

对于上面粗略的分析,学有余力的同学可以深入理解一下,实在理解不了的话,可以直接记忆结论【冒泡排序的平均时间复杂度为O(n2)】即可,就像站在巨人的肩膀上一样。

插入排序(Insertion Sort)

顾名思义,这个排序算法是一个动态排序的过程,即动态地往有序集合中添加数据,我们可以通过遍历数组,找到数据应该插入的位置将其插入这种方法保持集合中的数据一直有序。而对于一组静态数据,我们也可以借鉴上面讲的插入方法,来进行排序,于是就有了插入排序算法。

那该算法实际上到底是如何实现的呢?其实,首先将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

就拿上图的排序作为例子,i表示每一轮排序的序号,括号内的数据表示已排序区间,因此括号外表示未排序区间。然后在每一轮的排序中,取一个未排序区间中的元素,在已排序区间中通过遍历找到合适的插入位置将其插入,以此保证已排序区间数据一直有序。重复该过程,直到未排序区间中元素为空,排序结束。

那么我们下面我们看看代码是如何实现的。

 1// 插入排序,a 表示数组,n 表示数组大小2public void insertionSort(int[] a, int n) {3  if (n <= 1) return; 4  for (int i = 1; i < n; ++i) {5    int value = a[i];6    int j = i - 1;7    // 查找插入的位置8    for (; j >= 0; --j) {9      if (a[j] > value) {
10        a[j+1] = a[j];  // 数据移动
11      } else {
12        break;
13      }
14    }
15    a[j+1] = value; // 插入数据
16  }
17}

同样的,从以上代码可以分析得出,

插入排序算法的运行并不需要额外的存储空间,即空间复杂度是O(1)——原地排序算法

插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法

插入排序的每次插入操作(平均时间复杂度是O(n))都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为O(n2)

选择排序(Selection Sort)

选择排序算法的实现思路类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间中的尾部。具体的实现步骤就不做多解释了,可以结合下图进行学习理解。

同样的,选择排序空间复杂度为 O(1),是一种原地排序算法。选择排序的平均情况时间复杂度为O(n2)。但是需要注意的是,选择排序是一种不稳定的排序算法。从图中可以看出,选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。

归并排序(Merge Sort)

对于如何对一个数组进行排序,其实我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。比如下图的这个例子:

归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。

不难看出,分治思想跟递归思想很像,分治是一种解决问题的处理思想,递归是一种编程技巧,分治算法一般都是用递归来实现的

那么我们下面我们看看代码是如何实现的。

 1//以下为伪代码展示,仅供参考2merge(A[p...r], A[p...q], A[q+1...r]) {3  var i := p,j := q+1,k := 0 // 初始化变量 i, j, k4  var tmp := new array[0...r-p] // 申请一个大小跟 A[p...r] 一样的临时数组5  while i<=q AND j<=r do {6    if A[i] <= A[j] {7      tmp[k++] = A[i++] // i++ 等于 i:=i+18    } else {9      tmp[k++] = A[j++]
10    }
11  }
12
13  // 判断哪个子数组中有剩余的数据
14  var start := i,end := q
15  if j<=r then start := j, end:=r
16
17  // 将剩余的数据拷贝到临时数组 tmp
18  while start <= end do {
19    tmp[k++] = A[start++]
20  }
21
22  // 将 tmp 中的数组拷贝回 A[p...r]
23  for i:=0 to r-p do {
24    A[p+i] = tmp[i]
25  }
26}

同样的,从以上代码可以分析得出,归并排序保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。而归并排序的时间复杂度是 O(nlogn),在此不作过多推导。

快速排序(Quick Sort)

简称“快排”。它利用的也是分治思想。但与归并排序的思路却完全不一样。

快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到r 之间的任意一个数据作为 pivot(分区点)

我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

根据分治、递归的处理思想,我们可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。就像下图所示。

那么我们下面我们看看代码是如何实现的。

 1//以下为伪代码展示,仅供参考2//快速排序,A 是数组,n 表示数组的大小3quick_sort(A, n) {4  quick_sort_c(A, 0, n-1)5}6// 快速排序递归函数,p,r 为下标7quick_sort_c(A, p, r) {8  if p >= r then return9
10  q = partition(A, p, r) // 获取分区点
11  quick_sort_c(A, p, q-1)
12  quick_sort_c(A, q+1, r)
13}
14partition(A, p, r) {
15  pivot := A[r]
16  i := p
17  for j := p to r-1 do {
18    if A[j] < pivot {
19      swap A[i] with A[j]
20      i := i+1
21    }
22  }
23  swap A[i] with A[r]
24  return i
25}

现在来分析一下快速排序的性能。其实,从其实现原理我们不难得出,快排是一种原地、不稳定的排序算法。快排也是用递归来实现的,其时间复杂度求解较为复杂在此也不做深究,只需要知道它的时间复杂度也是O(nlogn)即可。

桶排序(Bucket Sort)

桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。就像下图的例子一样。

如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)

实际上,桶排序对要排序数据的要求是非常苛刻的,因此使用范围上并没有前几种广泛。桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

小练习

Questions:

 1//(1)    给出适用于计数排序的存储结构定义;2int[] arr = { 78, 48, 41, 74, 50, 40, 21, 16 };    //整型数组arr用于存储待排序数表3int[] sortedArr = new int[arr.length];             //数组sortedArr大小与待排序数组一样大,用于存储排序完成了的数组4//(2)    实现计数排序的算法;5public static int[] countSorting(int[] arr) {      //传入参数为待排序的数组arr6        int[] sortedArr = new int[arr.length];     //定义与待排序数组一样大,用于存储排序完成的数组7        for (int i = 0; i < arr.length; i++) {     //对参数数组每个元素走一趟8          //定义计数器,用于存储数组中元素比当前元素小的元素个数9          int count = 0;
10          //对于当前元素寻找找出该数组中元素比当前元素小的元素个数
11          for (int j = 0; j < arr.length; j++)
12                if (arr[i] > arr[j])               //通过if语句判断大小
13                    count++;                       //若符合要求,计数器+1
14          //内循环完成后,计数器的值即为排序后的数组元素的下标
15          sortedArr[count] = arr[i];
16        }
17        return sortedArr;
18}
19//(3)    对于有n个记录的表,关键码比较次数是多少?
20//该问题的本质即是询问本方法的时间复杂度,而由该方法体描述可知,对于有n个记录的表,关键码比较次数是【n2】次。


本次排序算法专题

就完整结束啦

下期的主题将是查找算法

祝学习愉快鸭~

归并排序执行次数_肯定能懂的常见算法讲解(1)——排序算法相关推荐

  1. 归并排序执行次数_十大排序算法,看这篇就够了

    排序算法分类[1][2] 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序. 非比较类排序:不通过比较来决定元素间的相对次序,它可以 ...

  2. 归并排序执行次数_一文了解C/C++经典排序算法

    0.算法概述 0.1 算法分类 十种常见排序算法可以分为两大类: 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序. 非比较类排序: ...

  3. 归并排序执行次数_归并排序过程、时间复杂度分析及改进

    前言 上一篇文章,介绍过第一种基于分治策略的排序算法--快速排序.接下来我们来讨论另一种基于分治策略的排序算法,归并排序.归并排序也被认为是一种时间复杂度最优的算法,我们还是按照基本过程,代码,最坏时 ...

  4. 程序员面试算法_程序员的前20个搜索和排序算法面试问题

    程序员面试算法 大家好,如果您正在准备编程工作面试或正在寻找新工作,那么您知道这不是一个容易的过程. 在您职业的任何阶段,您都必须幸运地接到电话并进行第一轮面试,但是在初学者方面,当您寻找第一份工作时 ...

  5. java 排序算法总结,Java排序算法总结之归并排序

    本文实例讲述了Java排序算法总结之归并排序.分享给大家供大家参考.具体分析如下: 归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作.和快速排序类似,让我们一起来看 ...

  6. 归并排序比较次数_归并排序「从入门到放弃」

    归并排序 归并排序,是创建在归并操作上的一种有效的排序算法,效率为O(nlogn).1945年由约翰·冯·诺伊曼首次提出.该算法是采用分治法(Divide and Conquer)的一个非常典型的应用 ...

  7. 9个元素换6次达到排序序列_程序员必须掌握的:10大排序算法梳理已整理好

    从数组中选择最小元素,将它与数组的第一个元素交换位置.再从数组剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置.不断进行这样的操作,直到将整个数组排序. 动态过程 算法原理参考:图解选择排 ...

  8. c++ 不插入重复元素但也不排序_面试官爱问的 10 大经典排序算法,20+ 张图来搞定...

    (给算法爱好者加星标,修炼编程内功) 作者:技术让梦想更伟大 / 李肖遥 (本文来自作者投稿) 冒泡排序 简介 冒泡排序是因为越小的元素会经由交换以升序或降序的方式慢慢浮到数列的顶端,就如同碳酸饮料中 ...

  9. python制作酷炫动画_厉害了!Python+matplotlib制作8个排序算法的动画

    1 算法的魅力 深刻研究排序算法是入门算法较为好的一种方法,现在还记得4年前手动实现常见8种排序算法,通过随机生成一些数据,逐个校验代码实现的排序过程是否与预期的一致,越做越有劲,越有劲越想去研究,公 ...

最新文章

  1. 虚幻引擎5–环境设计学习教程
  2. latin1_swedish_ci gbk_chinese_ci
  3. java23中设计模式——结构模式——Composite(组合)
  4. gitlab安装配置、备份恢复
  5. IAR建立stm32工程
  6. java项目使用mybatis
  7. Ch5702-Count The Repetitions【字符串,倍增,dp】
  8. 修改linux默认启动级别(包括Ubuntu)
  9. 深度学习 autoencoder_笔记:李淼博士-基于模仿学习的机器人抓取与操控
  10. python 获取本地视频信息_python获取视频文件信息
  11. linux之history使用技巧
  12. RabbitMQ消息队列入门篇(环境配置+Java实例+基础概念)
  13. fdisk、parted无损调整普通分区大小
  14. 1. 路过面了个试就拿到2个offer。是运气吗?
  15. C语言volatile关键字
  16. 简易抽奖软件逻辑实现
  17. 收集下载电影的好网站
  18. 规格说明书-吉林市一日游
  19. 发票信息提取系统解决方案(纸质发票、电子发票)
  20. mark一下江南一点雨的微人事开源项目

热门文章

  1. python可以开多少线程_Python开启线程,在函数中开线程的实例
  2. 常用WebService一览表
  3. easyUI tabs 显示与隐藏 tab 页
  4. ATT汇编leave指令
  5. STM32开发 -- 4G模块开发详解(3)
  6. SDL及扩展库在ARM-Linux 完整移植
  7. 内核中的page fault copy_from_user
  8. android-async-http 源码分析
  9. Android Hook (1) Dexposed原理
  10. rows是横着的还是cols_在Flask中如何自定义TextAreaField的rows和cols且将表单渲染为bootstrap的样式?...