11 | 排序(上):为什么插入排序比冒泡排序更受欢迎?

划重点:特定算法是依赖特定的数据结构的,带着问题去学习,是最有效的学习方法

本节分析冒泡排序、插入排序、选择排序三种排序算法

如何分析一个排序算法?

分析一个排序算法,要从以下几个方面入手:

排序算法的执行效率

  1. 最好、最坏、平均时间复杂度:给出最好情况、最坏情况、平均情况下的时间复杂度。除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。
  2. 时间复杂度的系:系数、常数、低阶:时间复杂度反应的是数据规模n很大时候的增长趋势,常常忽略了系数、常数、低阶。但是实际开发中排序的可能是规模很小的数据例如100个、1000个,因此对比算法性能时候需要考虑系数、常数、低阶
  3. 比较次数和交换(移动)次数:基于比较的排序算法的执行过程,会涉及两种操作,一是元素比较大小,二是元素交换或移动,因此分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去

排序算法的内存消耗

内存消耗可以通过空间复杂度来衡量。对于排序算法的空间复杂度,引入原地排序这个概念。原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。

排序算法的稳定性

针对排序算法,我们还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

举例:有一组数据 2,9,3,4,8,3,按照大小排序之后就是 2,3,3,4,8,9。这组数据里有两个 3。经过某种排序算法排序之后,如果两个 3 的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法;如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法。

为什么有时候需要稳定的排序算法?

很多数据结构和算法课程,在讲排序的时候,都是用整数来举例,但在真正软件开发中,我们要排序的往往不是单纯的整数,而是一组对象,我们需要按照对象的某个 key 来排序。比如说,要给电商交易系统中的“订单”排序。订单有两个属性,一个是下单时间,另一个是订单金额。如果我们现在有 10 万条订单数据,我们希望按照金额从小到大对订单数据排序。对于金额相同的订单,我们希望按照下单时间从早到晚有序。对于这样一个排序需求,我们怎么来做呢?

方法一:先按照金额对订单数据进行排序,然后,再遍历排序之后的订单数据,对于每个金额相同的小区间再按照下单时间排序。这种排序思路理解起来不难,但是实现起来会很复杂。

方法二:先按照下单时间给订单排序,注意是按照下单时间,不是金额。排序完成之后,用稳定排序算法,按照订单金额重新排序。两遍排序之后,我们得到的订单数据就是按照金额从小到大排序,金额相同的订单按照下单时间从早到晚排序的;稳定排序算法可以保持金额相同的两个对象,在排序之后的前后顺序不变。

分析排序算法的三个问题:是原地排序算法吗?是稳定的排序算法吗?时间复杂度是多少?

冒泡排序(Bubble Sort)

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

冒泡过程还可以优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。

冒泡排序的实现代码如下:

  //冒泡排序算法 a表示数组,n表示数组大小public static void bubbleSort(int[] a, int n) {if (n <= 1) return;for (int i = 0; i < n; i++) {for (int j = 0; j < n - i - 1; j++) {if (a[j] > a[j + 1]) {int temp = a[j];a[j] = a[j + 1];a[j+1] = temp;}}}}

冒泡排序算法回答:

  • 冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法
  • 在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法
  • 最好情况时间复杂度是 O(n)。而最坏的情况是,要排序的数据刚好是倒序排列的,我们需要进行 n 次冒泡操作,所以最坏情况时间复杂度为 O(n2)
  • 平均时间复杂度分析:n个数据有n!种组合顺序

数组有序情况下改进版冒泡排序算法

public void bubbleSort(int arr[]) {boolean flag;for(int i = 0, len = arr.length; i < len - 1; i++) {didSwap = false;for(int j = 0; j < len - i - 1; j++) {if(arr[j + 1] < arr[j]) {swap(arr, j, j + 1);flag= true;}}if(flag == false)return;}    
}

对于包含 n 个数据的数组,这 n 个数据就有 n! 种排列方式。不同的排列方式,冒泡排序执行的时间肯定是不同的。比如我们前面举的那两个例子,其中一个要进行 6 次冒泡,而另一个只需要 4 次。如果用概率论方法定量分析平均时间复杂度,涉及的数学推理和计算就会很复杂。这里一种思路,通过“有序度”和“逆序度”这两个概念来进行分析

有序度是数组中具有有序关系的元素对的个数。有序元素对用数学表达式表示就是这样:有序元素对:a[i] <= a[j], 如果i < j。

逆序度的定义正好跟有序度相反(默认从小到大为有序):对于一个倒序排列的数组,比如 6,5,4,3,2,1,有序度是 0;对于一个完全有序的数组,比如 1,2,3,4,5,6,有序度就是 n*(n-1)/2,也就是 15。我们把这种完全有序的数组的有序度叫作满有序度。关于这三个概念,我们还可以得到一个公式:逆序度 = 满有序度 - 有序度。我们排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。

冒泡排序包含两个操作原子,比较和交换。每交换一次,有序度就加 1不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是n*(n-1)/2–初始有序度。此例中就是 15–3=12,要进行 12 次交换操作。最坏情况初始的有序度是0,进行n*(n-1)/2交换操作。最好情况下不需要进行交换。平均情况下,需要 n*(n-1)/4 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 O(n2),所以平均情况下的时间复杂度就是 O(n2)。这个平均时间复杂度推导过程其实并不严格,但是很多时候很实用,毕竟概率论的定量分析太复杂不实用。快排时会再次用这种“不严格”的方法来分析平均时间复杂度。

插入排序(Insertion Sort)

有序数组插入一个数据,找到对应的位置然后进行数据搬移,接着插入数据即可。

插入排序的核心:

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

示意图:

插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入。对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度

插入排序代码:

//插入排序算法 a表示数组,n表示数组大小public static void InsertionSort(int[] a, int n) {if (n <= 1) return;for (int i = 1; i < n; i++) {int value = a[i];int j = i - 1;//查找插入的位置for (; j >= 0; --j) {if (a[j] > value) {a[j + 1] = a[j];//数据移动} else {break;}}a[j + 1] = value;//插入数据}}

总结:插入排序不需要额外的存储空间,因此是原地排序算法,空间复杂度O(1);插入排序可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法;

插入排序的时间复杂度:如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为 O(n)。注意,这里是从尾到头遍历已经有序的数据;数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为 O(n2)。

选择排序(Selection Sort)

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

选择排序空间复杂度为 O(1),是一种原地排序算法。选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n2)。你可以自己来分析看看。选择排序是一种不稳定的排序算法。从我前面画的那张图中,你可以看出来,选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。

问题思考:插入排序和冒泡排序的时间复杂度相同,都是 O(n2),在实际的软件开发里,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?

解答:冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。但是,从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。


冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换int tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;flag = true;
}插入排序中数据的移动操作:
if (a[j] > value) {a[j+1] = a[j];  // 数据移动
} else {break;
}

我们把执行一个赋值语句的时间粗略地计为单位时间(unit_time),然后分别用冒泡排序和插入排序对同一个逆序度是 K 的数组进行排序。用冒泡排序,需要 K 次交换操作,每次需要 3 个赋值语句,所以交换操作总耗时就是 3*K 单位时间。而插入排序中数据移动操作只需要 K 个单位时间。虽然冒泡排序和插入排序在时间复杂度上是一样的,都是 O(n2),但是如果我们希望把性能优化做到极致,那肯定首选插入排序。

评价一个排序算法,需要从执行效率、内存消耗和稳定性三个方面来看

我们今天讲的几种排序算法,都是基于数组实现的。如果数据存储在链表中,这三种排序算法还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?

一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/419133.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

12 | 排序(下):如何用快排思想在O(n)内查找第K大元素?

算法对比&#xff1a; 算法时间复杂度适合场景冒泡排序、插入排序、选择排序O(n2)小规模数据归并排序、快速排序O(nlogn&#xff09;大规模数据 归并排序和快速排序都用到了分治思想&#xff0c;非常巧妙。我们可以借鉴这个思想&#xff0c;来解决非排序的问题&#xff0c;比如…

13 | 线性排序:如何根据年龄给100万用户数据排序?

三种时间复杂度是 O(n) 的排序算法&#xff1a;桶排序、计数排序、基数排序。因为这些排序算法的时间复杂度是线性的&#xff0c;所以我们把这类排序算法叫作线性排序&#xff08;Linear sort&#xff09;。之所以能做到线性的时间复杂度&#xff0c;主要原因是&#xff0c;这三…

前端学习(1728):前端系列javascript之状态栏分析

<template><view class"content"><view class"todo-header"><view class"todo-header_left"><text class"active-text">全部</text><text>10条</text></view><view class&q…

MySQL索引知识复习

在你享受工作舒适的同时&#xff0c;你的危机也已经在慢慢靠近 正确的创建合适的索引才是保证数据库性能保证的基础 1、索引的底层数据结构&#xff1a;hash&#xff0c;b树&#xff0c;b树的区别&#xff0c;逐层分析为什么最后选用了b树作为索引结构&#xff1f; Mysql数据…

前端学习(1729):前端系列javascript之内容卡片布局

<template><view class"content"><view class"todo-header"><view class"todo-header_left"><text class"active-text">全部</text><text>10条</text></view><view class&q…

面试必问之JVM

Java的一次编译到处运行背后&#xff1a;JVM从软件层面屏蔽了底层硬件、指令字节码的细节&#xff08;JVM充当了适配器的角色和功能&#xff09; JVM\JDK\JRE关系 2、JVM运行时数据区 所有学过的知识是用来推导新的未知的知识的&#xff0c;踏入社会要学会运用自己的知识能力去…

前端学习(1730):前端系列javascript之发布窗口布局上

index.vue <template><view class"content"><view class"todo-header"><view class"todo-header_left"><text class"active-text">全部</text><text>10条</text></view><v…

赵雅智:service_startService生命周期

案例演示 布局文件 <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"android:layout_width"match_parent"android:layout_height"match_parent"tools:co…

16 | 二分查找(下):如何快速定位IP对应的省份地址?

问题&#xff1a;假设我们有 12 万条这样的 IP 区间与归属地的对应关系&#xff0c;如何快速定位出一个 IP 地址的归属地呢&#xff1f; 二分查找的变形问题&#xff1a; 变体一&#xff1a;查找第一个值等于给定值的元素 public int bsearch(int[] a, int n, int value) {int…

17 | 跳表:为什么Redis一定要用跳表来实现有序集合?

问题&#xff1a;如果数据存储在链表中&#xff0c;就真的没法用二分查找算法了吗&#xff1f;可以对链表进行“改造”&#xff0c;就可以支持类似“二分”的查找算法。 跳表 定义&#xff1a;对链表经过改造之后的数据结构叫做跳表&#xff08;Skip list&#xff09;&#x…

18 | 散列表(上):Word文档中的单词拼写检查功能是如何实现的?

问题引入 在 Word 里输入一个错误的英文单词&#xff0c;它就会用标红的方式提示“拼写错误”&#xff0c;Word 文本编辑器的拼写检查功能是如何实现的呢&#xff1f;散列表&#xff08;Hash Table&#xff09; 散列表 散列表定义&#xff1a;散列表的英文叫“Hash Table”&…

19 | 散列表(中):如何打造一个工业级水平的散列表?

问题引入&#xff1a;如何实现一个工业级的散列表&#xff1f; 主要要求&#xff1a; 设计一个合适的散列函数&#xff1b;定义装载因子阈值&#xff0c;并且设计动态扩容策略&#xff1b;选择合适的散列冲突解决方法。 对于动态散列表来说&#xff0c;不管我们如何设计散列函…

RTTI: dynamic_cast typeid

dynamic_cast&#xff1a;将基类类型的指针向派生类指针安全转换。多用于下行转换。上行转换时&#xff0c;和static_cast是一样的。C类型转换看这里。而const_cast用来修改类型的const或volatile属性。。。下面主要说多态下的RTTI&#xff1a; 使用条件:  基类应有虚函数。 …

20 | 散列表(下):为什么散列表和链表经常会一起使用?

有两种数据结构&#xff0c;散列表和链表经常会被放在一起使用。常见的使用方式有&#xff1a; 用链表来实现 LRU 缓存淘汰算法&#xff0c;链表实现的 LRU 缓存淘汰算法的时间复杂度是 O(n)&#xff0c;通过散列表可以将这个时间复杂度降低到 O(1)。Redis 的有序集合是使用跳…

冬季小学期 NIIT公司 web前端培训 CSS

外边距合并 http://www.w3school.com.cn/css/css_margin_collapsing.asp div、p、h1 块元素 span行元素 浮动&#xff1a;浮动的框可以向左或向右移动&#xff0c;直到它的外边缘碰到包含框或另一个浮动框的边框为止。 http://www.w3school.com.cn/css/css_positioning_floatin…

BZOJ 1491: [NOI2007]社交网络( floyd )

floyd...求最短路时顺便求出路径数. 时间复杂度O(N^3) -------------------------------------------------------------------------------------------#include<cstdio>#include<algorithm>#include<cstring>using namespace std;typedef long long ll;con…