排序:为什么插入排序比冒泡排序更受欢迎?

news2024/10/2 8:30:43

文章来源于极客时间前google工程师−王争专栏。

需掌握的的排序:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、桶排序。按照时间复杂度可以分为三类:

image

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

如何分析一个“排序算法”?

算法的执行效率

1.最好情况、最坏情况、平均情况时间复杂度

对于要排序的数据,有的接近有序,有的完全无序。有序度不同的数据,对于排序的执行时间肯定是有影响的,我们要知道排序算法在不同数据下的性能表现。

2.时间复杂度的系数、常数、低阶

时间复杂度反映的是数据规模n很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。

但是在实际的软件开发中,我们排序的可能是10,100,1000这样数据规模很小的数据,所以在时间复杂度相同的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。

3.比较次数和交换(或移动)次数

基于比较的排序算法会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。所以分析排序算法的执行效率,应该把比较次数和交换(或移动)次数也考虑进去。

排序算法的内存消耗

算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。

针对排序算法的空间复杂度,还引入了一个新的概念,原地排序。原地排序算法,就是特指空间复杂度是O(1)的排序算法。

排序算法的稳定性

用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,还有一个重要的指标,稳定性

这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

比如有一组数据2,9,3,4,8,3排序之后就是2,3,34,8,9。

这组数据里有两个3,如果经过排序算法之后,两个3的前后顺序不变,那我们就把这种排序算法叫作稳定排序算法;如果前后顺序发生变化,对应的算法就叫作不稳定的排序算法

真正的软件开发中,我们要排序的往往不是单纯的整数,而是一组对象,我们需要按照对象的某个key来排序。

需求:现在要给电商交易系统中的“订单”排序。订单中有两个属性,一个是下单时间,另一个是订单金额。如果现在有10万条订单数据,我们希望按照金额从小到大排序,对于金额相同的订单,我们希望按照下单时间从早到晚有序。

最先想到的方法是:先按照金额排序,然后再遍历排序之后的订单数据,对于每个金额相同的小区间再按照下单时间排序。这种排序思路,理解简单,实现复杂。

借助稳定排序算法:先按照下单时间对订单数据进行排序,然后
用稳定排序算法,按照订单金额重新排序。两遍排序,实现需求。

稳定排序算法可以保持金额相同的两个对象,在排序之后的前后顺序保持不变

image

冒泡排序(Bubble Sort)

冒泡排序只会操作相邻的两个数据。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序。

对一组数据4,5,6,3,2,1,从小到大排序,第一次冒泡的详细过程如下图所示:
image

经过一次冒泡操作之后,6这个元素已经存储在正确的位置上。要想完成所有数据的排序,我们只需要进行6次这样的冒泡操作就行了。

image

优化:当某次冒泡操作已经没有数据交换时,说明已经达到了完全有序,不用再继续执行后续的冒泡操作。如下图所示,这里给6个元素排序,只需要4次冒泡操作就可以了。

image

代码如下:

public void bubbleSort(int[] array){
    int len = array.length;
    if(len < = 1) return;
    for(int i=0;i<len;++i){
        //提前退出冒泡循环的标志位
        boolean flag = false;
        for(int j=0;j<len-1-i;++j){
            if(array[j] > array[j+1]){
                //表示有数据交换
                flag = true;
                int temp = array[j];
                array[j] = array[j+1];
                array[j+1] = temp;
            }
        }
        if(flag){//没有数据交换提前退出
            break;
        }
    }
}

这里有三个问题:

第一,冒泡排序是原地排序算法吗?

冒泡过程中只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为O(1),是一个原地排序算法。

第二,冒泡排序是稳定的排序算法吗

只有交换才可以改变两个元素的前后顺序。当有相邻的两个元素大小相等的时候,我们不做交换,所以冒泡排序是稳定的排序算法。

第三,冒泡排序的时间复杂度是多少

最好情况下,要排序的数据已经是有序的,只需要进行一次冒泡操作,就可以,所以最好情况复杂度是O(n)。最坏情况,需要进行n次冒泡操作,所以最坏情况时间复杂度是O(n^2)。

image

用概率论的方法定量分析平均时间复杂度,涉及的数学推理和计算比较复杂。可以通过“有序度”和“逆序度”这两个概念来进行分析。

有序度是数组中具有有序关系的元素对的个数。数学表达式如下:

有序元素对:a[i] <= a[j], 如果 i < j。

image

对于一个倒叙排列的数组,比如6,5,4,3,2,1,有序度是0;1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是15.我们将这种完全有序的数组的有序度叫作满有序度

逆序度的定义正好跟有序度相反。

逆序元素对:a[i] > a[j], 如果 i < j。

根据这三个概念,我们还可以得出一个公式:逆序度 = 满有序度 - 有序度

排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。

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

插入排序(Insertion Sort)

先看一个问题。一个有序数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,只要遍历数组,找到数据应该插入的位置讲其插入即可。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
这是一个动态排序的过程,即动态地往有序集合中添加数据,我们可以通过这种方法保持集中的数据一直有序。而对于一组静态数据,我们也可以借鉴上面讲的插入方法,来进行排序,于是就有了插入排序算法

插入排序具体是如何借助上面的思想来实现排序的呢

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

如图所示,要排序的数据是4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间。
image
插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据a插入到已排序区间时,需要拿a与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素a插入。

对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数总是固定的,就等于逆序度。

如下图所示。满有序度是n*(n-1)/2=15,初始有序度是5,所以逆序度是10。插入排序中,数据移动的个数总和也等于3+3+4=10。
image
代码实现如下:

public void InsertSort(int[] array){
    int len = array.length;
    if(len <= 1){return;}
    //i默认为1,分为两个区间,开始遍历无序区间
    for(int i=1;i<n;++i){
       int j = i-1;
       //声明无序区间第一个元素
       int value = array[i];
       //循环有序区间
       for(;j>=0;--j){
           if(array[j]>value){
               //数据移动
               array[j+1] = array[j];
           }else{
               //小于无序区间元素跳出循环,记录j
               break;
           }
       }
       //将数据插入有序区间的正确位置
       array[j+1] = value;
    }
}

自行实现插入降序算法

第一,插入排序是原地算法吗?

从实现过程可以看出,插入排序的算法运行不需要额外的存储空间,所以空间复杂度为O(1),也就是说,这是一个原地排序算法。

第二,插入排序是稳定的排序算法吗?

排序之后,相同元素前后顺序不变,所以插入排序是稳定的排序算法。

插入排序的时间复杂度是多少?

最好时间复杂度是O(n),只需要从尾到头遍历已经有序的数据

如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为O(n^2)。

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

选择排序(Selection Sort)

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

选择排序空间复杂度为O(1),是一种原地排序算法。选择排序最好、最坏和平均情况时间复杂度都是O(n^2)。

选择排序是不稳定的排序算法。选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,破坏了稳定性。

比如5,8,5,2,9这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素2,与第一个5交换位置,那第一个5和中间的5顺序就变了,所以就不稳定了。相对于冒泡排序和插入排序,选择排序就稍微逊色了。

解答开篇

冒泡排序和插入排序的时间复杂度都是O(n^2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢?

不管如何优化,元素交换的次数是一个固定值,是原始数据的逆序度

从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要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*K单位时间。插入排序中数据移动操作只需要K个单位时间。

随机生成10000个数组,每个数组中包含200个数据,冒泡排序需要584ms完成,插入排序只需要105ms就能搞定!

//数组工厂
public class ArrayFactory {
	private static Random rand = new Random();
	//获得10000个包含200个随机数的数组
	public static int[][] get2Array(){
		int[][] array = new int[10000][200];
		for(int i=0;i<array.length;++i){
			array[i] = getArray();
		}
		return array;
	}
	public static int[] getArray(){
		int[] array = new int[200];
		for(int i=0;i<array.length;++i){
			array[i] = rand.nextInt(500);
		}
		return array;
	}
}
//排序效率测试
public class TestSort {
	//冒泡排序
	public static void bubbleSort(int[] a){
		if(a == null) return;
		int len = a.length;
		if(len <= 1)return;
		for(int i=0;i<len;++i){
			//退出标志
			boolean bol = true;
			for(int j=0;j<len-i-1;++j){
				if(a[j]>a[j+1]){
					//交换
					int temp = a[j+1];
					a[j+1] = a[j];
					a[j] = temp;
					bol = false;
				}
			}
			if(bol){
				break;
			}
		}
	}
	//插入排序
	public static void InsertSort(int[] a){
		if(a == null) return;
		int len = a.length;
		if(len <= 1) return;
		//用下标1分为两个区间左边为有序区间,右边为无序区间
		for(int i=1;i<len;++i){
			//记录无序区间第一个数
			int value = a[i];
			int j = i-1;
			//遍历无序区间数据个数
			for(;j>=0;--j){
				//无序区间每一个数与value比较
				if(a[j]>value){
					//移动
					a[j+1] = a[j];
				}else{
					break;
				}
			}
			//插入数据
			a[j+1] = value;
		}
	}
	//打印数组
	public static void printArray(int[] array){
		int len = array.length;
		for(int i=0;i<len;++i){
			if(i == len-1){
				System.out.print(array[i]);
			}else{
				System.out.print(array[i]+", ");
			}
		}
		System.out.println();
	}
	//打印10000个数组冒泡排序的时间
	public static void bubbleSortTime(){
		long start = System.currentTimeMillis();
		int[][] array = ArrayFactory.get2Array();
		for(int i=0;i<array.length;++i){
			bubbleSort(array[i]);
		}
		System.out.println(System.currentTimeMillis()-start);
	}
	//打印10000个数组插入排序的时间
	public static void insertSortTime(){
		long start = System.currentTimeMillis();
		int[][] array = ArrayFactory.get2Array();
		for(int i=0;i<array.length;++i){
			InsertSort(array[i]);
		}
		System.out.println(System.currentTimeMillis()-start);
	}
	public static void main(String[] args) {
		bubbleSortTime();//output:584
		insertSortTime();//output:105
	}
}

插入排序性能优化-希尔排序

评价一个排序算法,需要从执行效率、内存消耗和稳定性三个方面看。重点掌握分析方法。
image

这三种排序算法,实现代码简单,对于小规模数据的排序,用起来非常高效。但是在大规模数据排序中,这个时间复杂度还是有点高,所以我们更倾向于时间复杂度为O(nlogn)的排序算法。

思考

特定的算法是依赖特定的数据结构的。都是基于数组实现,如果数据存储在链表中,这三种排序算法还能工作吗?相应的时间、空间复杂度是多少?

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

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

相关文章

UML组件图综合指南:设计清晰、可维护的软件系统

介绍&#xff1a; UML&#xff08;Unified Modeling Language&#xff09;组件图是软件系统设计中的重要工具&#xff0c;用于描绘系统的物理结构和组件之间的关系。在软件工程中&#xff0c;通过创建清晰的组件图&#xff0c;团队能够更好地理解系统的模块化结构和组织关系&a…

PTE考试解析

Pte 考试题目 注入漏洞 空格被过滤 用/**/代替空格&#xff0c;发现#被过滤 对&#xff03;进行url编码为%23 输入构造好的payload http://172.16.12.100:81/vulnerabilities/fu1.php?id1%27)/**/and/**/11%23 http://172.16.12.100:81/vulnerabilities/fu1.php?id1%27)/*…

uniapp 一次性上传多条视频 u-upload accept=“video“ uni.chooseMedia uni.uploadFile

方式 一 部分安卓机 只能一条一条传视频 文档地址 uview 2.0 Upload 上传组件 html <view class"formupload"><u-upload accept"video":fileList"fileList3" afterRead"afterRead" delete"deletePic" name"…

解锁远程联机模式:使用MCSM面板搭建我的世界服务器,并实现内网穿透公网访问

文章目录 前言1.Mcsmanager安装2.创建Minecraft服务器3.本地测试联机4. 内网穿透4.1 安装cpolar内网穿透4.2 创建隧道映射内网端口 5.远程联机测试6. 配置固定远程联机端口地址6.1 保留一个固定TCP地址6.2 配置固定TCP地址 7. 使用固定公网地址远程联机 前言 MCSManager是一个…

动态资源平衡:主流虚拟化 DRS 机制分析与 SmartX 超融合的实现优化

资源的动态调度是虚拟化软件&#xff08;或超融合软件&#xff09;中的一项重要功能&#xff0c;主要指在虚拟化集群中&#xff0c;通过动态改变虚拟机的分布&#xff0c;达到优化集群可用性的目标。这一功能以 VMware vSphere 发布的 Distributed Resource Scheduler&#xff…

atoi函数及其模拟实现

这个函数的功能是将字符串转换为整形&#xff0c;那么具体是怎么样的呢 先看几个例子&#xff1a; 有一个转换为整形的最大值 刚开始就是非法字符 因此&#xff0c;我们模拟实现时&#xff0c;要考虑以上几种非法输入情况&#xff1a; 1.空字符串 2.空白字符 3.处理-号 4.过大…

C++算法:城市天际线问题

题目 城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度&#xff0c;请返回 由这些建筑物形成的 天际线 。 每个建筑物的几何信息由数组 buildings 表示&#xff0c;其中三元组 buildings[i] [lefti, righti, heighti] 表示&am…

【无标题】SpringMVC之WEB-INF下页面跳转@ModelAttributeIDEA tomcat控制台中文乱码问题处理

WEB-INF下页面跳转 ModelAttribute来注解非请求处理方法 用途&#xff1a;预加载数据&#xff0c;会在每个RequestMapping方法执行之前调用。 特点&#xff1a;无需返回视图&#xff0c;返回类型void IDEA tomcat控制台中文乱码问题处理 复制此段代码&#xff1a;-Dfile.e…

快速生成美观的二维码:专家级教程

首先&#xff0c;我们需要选择一个适合在线海报制作工具&#xff0c;比如乔拓云。乔拓云是一个非常流行的在线海报制作工具&#xff0c;它提供了大量的模板和编辑工具&#xff0c;让你可以轻松地制作出一张精美的Logo。 接下来&#xff0c;我们需要在乔拓云网站上注册并登录。在…

【运维笔记】VMWare 另一个程序已锁定文件的一部分,进程无法访问

情景再现 这里使用的是VMware 17 解决办法 进入设置 点击选项&#xff0c;全选复制里面内容 进入文件夹&#xff0c;删除所有包含.lck后缀的文件和文件夹 再启动虚拟机即可

关键词搜索快手商品列表数据,快手商品列表数据接口,快手API接口

在网页抓取方面&#xff0c;可以使用 Python、Java 等编程语言编写程序&#xff0c;通过模拟 HTTP 请求&#xff0c;获取快手网站上的商品页面。在数据提取方面&#xff0c;可以使用正则表达式、XPath 等方式从 HTML 代码中提取出有用的信息。值得注意的是&#xff0c;快手网站…

多目标水母搜索算法(Multi-Objective Jellyfish Search algorithm,MOJS)求解微电网优化--提供MATLAB代码

一、微网系统运行优化模型 微电网优化模型介绍&#xff1a; 微电网多目标优化调度模型简介_IT猿手的博客-CSDN博客 参考文献&#xff1a; [1]李兴莘,张靖,何宇,等.基于改进粒子群算法的微电网多目标优化调度[J].电力科学与工程, 2021, 37(3):7 二、多目标水母搜索算法MOJS …

@Mapper与@MapperScan注解

Mapper Mapper Mapper.xml文件 作用在dao&#xff08;mapper&#xff09;层上的一个注解&#xff0c;将接口生成一个动态代理类&#xff0c;有了这个注解就不用 再写Mapper.xml文件 如果缺少这个注解&#xff0c;运行项目就会报相应的错误 Field userMapper in com.example…

docker入门加实战—docker数据卷

docker入门加实战—docker数据卷 容器是隔离环境&#xff0c;容器内程序的文件、配置等都在容器的内部&#xff0c;要读写容器内的文件非常不方便。 因此&#xff0c;容器提供程序的运行环境&#xff0c;但是程序运行产生的数据、程序运行依赖的配置都应该与容器进行解耦。 …

dpdk/spdk/网络协议栈/存储/网关开发/网络安全/虚拟化/ 0vS/TRex/dpvs技术专家成长体系教程

课程围绕安全&#xff0c;网络&#xff0c;存储&#xff0c;云原生4个维度去讲解核心技术点。 6个专栏组成&#xff1a;dpdk网络专栏、存储技术专栏、安全与网关开发专栏、虚拟化与云原生专栏、测试工具专栏、性能测试专栏 一、dpdk网络 dpdk基础知识 多队列网卡&#xff0…

es6(三)—— set(集合) 和map的使用

ES6的系列文章目录 第一章 Python 机器学习入门之pandas的使用 文章目录 ES6的系列文章目录一、set&#xff08;集合&#xff09;0. 定义1. 基本使用2.常用方法&#xff08;1&#xff09;代码&#xff08;2&#xff09;效果&#xff08;3&#xff09;遍历 二、map0. 定义1. 基…

利用Python分析金融交易中的滚动Z值

大家好&#xff0c;在不断演变的证券交易领域&#xff0c;能够利用数据和统计学的力量提供重要的优势。无论是预测未来价格、分析市场趋势&#xff0c;还是简单地评估特定证券的波动性&#xff0c;数据驱动的见解已经改变了交易者对证券市场的处理方式。这就是Z值的用途&#x…

miRNA测序数据生信分析——第四讲,未知物种的生信分析实例

miRNA测序数据生信分析——第四讲&#xff0c;未知物种的生信分析实例 miRNA测序数据生信分析——第四讲&#xff0c;未知物种的生信分析实例1. 下载测序数据2. 原始数据质控——软件fastqc3. 注释tRNA和rRNA&#xff0c;使用Rfam数据库——软件blast&#xff0c;Rfam_statisti…

网页制作工具都有哪些?推荐这几款

随着网络的快速发展&#xff0c;网站迅速崛起&#xff0c;成为无数网络冲浪玩家的首选&#xff0c;网页设计师也成为各大互联网公司竞争的热点。当设计师开始时&#xff0c;他们总是不知道如何选择网页制作工具。以下将列出市场上流行的网页制作工具。设计师可以根据自己的需要…

Leetcode刷题详解——复写零

1.题目链接&#xff1a;复写零 2.题目描述 给你⼀个⻓度固定的整数数组 arr &#xff0c;请你将该数组中出现的每个零都复写⼀遍&#xff0c;并将其余的元素 向右平移。 注意&#xff1a;请不要在超过该数组⻓度的位置写⼊元素。请对输⼊的数组就地进⾏上述修改&#xff0c;不…