【排序算法】冒泡排序、选择排序、插入排序

news2024/11/17 1:37:44

端午学习

冒泡排序

依次比较相邻的两个元素,将比较小的数放在前面,比较大的数放在后面,直到所有元素排列完。

最容易理解的版本

对一个数组的n个整型数据进行n趟排序,每趟排序都尝试将较大值放到数组右侧。
每趟排序比较两个相邻的数据,由于n个数据有n-1个间隔,所以每趟需要比较n-1次。
代码如下:

Java

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] ints = {5, 2, 4, 3};
        //比较n趟
        for (int i = 0; i < ints.length; i++) {
            //每趟比较n-1次
            for (int m = 0; m < ints.length - 1; m++) {
                //将较大值置于数组右侧
                if (ints[m] > ints[m + 1]) {
                    int tmp = ints[m];
                    ints[m] = ints[m + 1];
                    ints[m + 1] = tmp;
                }
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

C/C++

#include<stdio.h>
int  main() {
	int ints[] = { 5,2,4,3 };
	//比较n趟
	for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
		//每趟比较n-1次
		for (int m = 0; m < sizeof(ints) / sizeof(ints[0]) - 1; m++) {
			将较大值置于数组右侧
			if (ints[m] > ints[m + 1]) {
				int tmp = ints[m];
				ints[m] = ints[m + 1];
				ints[m + 1] = tmp;
			}
		}
	}
	//检验结果
	for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
		printf("%d ", ints[i]);
	}
}

进一步优化

上述代码存在优化空间:
在第一趟排序结束后,数组最右端已是当前的最大值5。剩余元素均小于5,后续排序无需再与5进行比较。
在第二趟排序结束后,数组最右侧是4,5,剩余元素均小于4,5,后续排序无需再对4,5进行比较。
即对于内层循环:

  • 在第i趟排序中,只需要比较n-i次(i1开始)。
  • 如果外层循环是从0开始计数的,那么需要每轮需要比较n-1-i次。

对于外层循环,在执行第n-1趟排序时,内层循环只比较了第1个元素和第2个元素。
此时已经排列完成,不需要再执行下一趟排序。
即对于外层循环:

  • 只需要排序n-1趟。
  • 对于给定的数组,n-1的结果也是确定的。因此无论外层循环的计数器从几开始,需要比较的次数都是n-1


上面的例子比较简单,还有一种情况存在优化空间:在第n-1趟排序执行之前就已经排序完成。
这种情况的特征是:在某一趟比较之后,没有发生元素交换。
我们可以:

  1. 定义一个标记flag并初始化为1
  2. 在每趟比较开始前,通过flag检查是否发生元素交换。
  3. 在每趟比较开始时,将flag置为0
  4. 当发生元素交换时,将flag置为1

在第2步中,如果flag值为1,则表明发生交换,继续下一步。
如果flag值为0,则表明没有发生交换,即已经排序完成,结束即可。

  • flag初始值为1,可以正常进入第一趟排序。
  • JavaBoolean类型不能赋值为10,将对应的10改为truefalse即可。

总结

  • 外层循环控制轮数,总共执行n-1轮。
  • 内层循环控制每轮的比较次数,第i轮比较n-i次(i1开始)。
  • 通过标记flag判断是否已经排序完成。

C/C++

#include<stdio.h>
int  main() {
	int ints[] = { 5,2,4,3 };
	//标记完成状态
	char flag = 1;
	//比较n-1趟
	for (int i = 0; i < sizeof(ints) / sizeof(ints[0]) - 1 && flag; i++) {
		//将标记置为0
		flag = 0;
		//计数器从0开始,每趟比较n-1-i次
		for (int m = 0; m < sizeof(ints) / sizeof(ints[0]) - 1 - i; m++) {
			//将较大值置于数组右侧
			if (ints[m] > ints[m + 1]) {
				int tmp = ints[m];
				ints[m] = ints[m + 1];
				ints[m + 1] = tmp;
				//当发生交换时,将flag置为1
				flag = 1;
			}
		}
	}
	//检验结果
	for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
		printf("%d ", ints[i]);
	}
}

Java

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] ints = {5, 2, 4, 3};
        //标记完成状态
        boolean flag = true;
        //比较n-1趟
        for (int i = 0; i < ints.length - 1 && flag; i++) {
            //将标记置为0
            flag = false;
            //每趟比较n-1次
            for (int m = 0; m < ints.length - 1 - i; m++) {
                //将较大值置于数组右侧
                if (ints[m] > ints[m + 1]) {
                    int tmp = ints[m];
                    ints[m] = ints[m + 1];
                    ints[m + 1] = tmp;
                    //当发生交换时,将flag置为1
                    flag = true;
                }
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

需要注意的是,内层循环的结束条件与m无关。

选择排序

分别从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。选择排序是不稳定的排序算法,即对于值相同的数据元素,彼此的前后顺序可能会发生改变。

对比冒泡排序

与冒泡排序不同:

  • 冒泡排序是逐趟选出未排序序列中的最大值,置于右侧。
  • 选择排序是逐趟选出未排序序列中的最小值,置于左侧。
  • 冒泡排序会两两比较相邻元素,将较大值通过多次交换移动到数列右侧,第i趟最多交换n-i次。
  • 选择排序会通过多次比较记录最小元素的下标,在这一趟结束时才发生交换,每趟最多交换1次。

即重复这两个步骤:

  1. 遍历无序序列,记录无序数列的最小值的下标。
  2. 将下标对应的元素与无序数列的最左端元素交换位置。


代码如下:

Java

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] ints = {5, 2, 4, 3};
        //用于记录最小值的下标
        int minIndex = 0;
        //控制轮数
        for (int i = 0; i < ints.length - 1; i++) {
            minIndex = i;
            //在未排序的子序列中找到最小元素的位置
            for (int j = i + 1; j < ints.length; j++) {
                if (ints[j] < ints[minIndex]) {
                    //用minIndex记录最小元素的位置
                    minIndex = j;
                }
            }
            //交换元素,将较小值置于左侧
            if (minIndex != i) {
                int tmp = ints[minIndex];
                ints[minIndex] = ints[i];
                ints[i] = tmp;
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

C/C++

#include<stdio.h>
int  main() {
	int ints[] = { 5,2,4,3 };
	//标记最小值下标
	char minIndex = 0;
	//比较n-1趟
	for (int i = 0; i < sizeof(ints) / sizeof(ints[0]) - 1; i++) {
		minIndex = i;
		for (int m = i + 1; m < sizeof(ints) / sizeof(ints[0]); m++) {
			if (ints[m] < ints[minIndex]) {
				minIndex = m;
			}
		}
		//交换元素,将较小值置于左侧
		if (minIndex != i) {
			int tmp = ints[i];
			ints[i] = ints[minIndex];
			ints[minIndex] = tmp;
		}
	}
	//检验结果
	for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
		printf("%d ", ints[i]);
	}
}

需要注意:

  • 对于n个元素,需要排序的趟数仍然是n-1
  • 不同于冒泡排序,选择排序每趟排序最多只会改变两个元素的位置。不能设置flag检查是否排序完成,也无法通过flag检查。
  • 选择排序需要遍历剩余所有元素,内层循环不能同冒泡循环一样修改右边界。并且要保证能访问到数列的最后一个元素。

插入排序

逐步将无序序列中的元素,插入到前面已排好的有序序列的合适位置。

C/C++

#include<stdio.h>
int  main() {
	int ints[] = { 5,2,4,3 };
	//遍历数列
	for (int i = 1; i < sizeof(ints) / sizeof(ints[0]); i++) {
		//定义临时变量存储当前序列取出的值
		int tmp = ints[i];
		int j = 0;
		//寻找合适位置
		for (j = i - 1; j >= 0; j--) {
			//需要移动的数据向后覆盖
			if (ints[j] > tmp) {
				ints[j + 1] = ints[j];
			}
			else break;
		}
		if (ints[j + 1] != tmp) {
			ints[j + 1] = tmp;
		}
	}
	//检验结果
	for (int i = 0; i < sizeof(ints) / sizeof(ints[0]); i++) {
		printf("%d ", ints[i]);
	}
}

Java

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] ints = {5, 2, 4, 3};
        //遍历数列
        for (int i = 1; i < ints.length; i++) {
            //定义临时变量存储当前序列取出的值
            int tmp = ints[i];
            int j = 0;
            //寻找合适位置
            for (j = i - 1; j >= 0 && ints[j] > tmp; j--) {
                ints[j + 1] = ints[j];
            }
            //插入到合适位置
            if (ints[j + 1] != tmp) {
                ints[j + 1] = tmp;
            }
        }
        //检验结果
        System.out.println(Arrays.toString(ints));
    }
}

要点如下:

  • 如果数据结构是数组,那么每次插入都需要将插入位置以后的元素向后移动,移动元素会覆盖该元素的下一个元素。会导致ints[i]被覆盖,因此必须要使用临时变量tmp存储每趟排序中的ints[i]的值。
  • 我们需要对数组的每个元素都进行遍历,以保证每个元素都被取出并插入到合适位置。因此外重循环的结束条件为元素个数n而不是n-1
  • 在第一趟插入中,我们将原数列的第1个元素取出作为有序数列,将第2个元素取出作为新元素插入,对应的下标从1开始。虽然结束条件是n,外重循环的次数仍然是n-1
  • 在插入元素时,已经内层循环的结束条件,此时j小于零,或者已经指向合适位置的前一个位置。因此需要对ints[j+1]进行赋值,而非ints[j]

补充

交换两个变量的值,除了可以使用第三个变量tmp,也可以使用加减法的方式处理,仅适用于数字类型。

ints[m] = ints[m] + ints[n];
ints[n] = ints[m] - ints[n];
ints[m] = ints[m] - ints[n];

总结

三种排序方法每趟都只能确保一个数据加入有序数列后仍有序,外层循环执行的趟数都为n-1n即元素个数。但由于计数器开始位置不同,可能为0,也可能为1,或者其它计数方式,不需要死记硬背,只需要保证能执行n-1趟即可。
对于内层循环,还是由于三种排序方法每趟都只能确保一个数据加入有序数列后仍有序。有序序列每趟排序后长度都在增加,我们不需要对有序序列的元素再取出排序。我们只需要保证能遍历到无序序列中的每一个元素即可。

  • 对于冒泡排序,有序序列默认在右端,左侧为无序序列,我们采取的方式是调整右边界。而内层循环每次从0开始,是为了能够遍历到左侧的无序序列的每一个元素。
  • 对于选择排序,有序序列默认在左端,右侧为无序序列,我们采取的方式是调整左边界。内层循环的计数器初始值随趟数改变而改变,是为了保证每趟都指向无序序列的第一个元素,并能够遍历无序序列的每一个元素。
  • 对于插入排序,有序序列默认在左端,我们需要取出无序序列中的元素之后遍历有序序列,寻找合适位置。由于有序序列是有序的,我们可以选择一个方向,寻找介于两个元素之间的位置插入。在有序序列寻找合适位置时需要考虑数组边界。

对于临时变量的定义,编译器可能存在零优化,如果定义在循环内部,那么可能出现频繁的创建和释放,提高占用时间。并且在插入排序中,如果数据结构是数组,那么数据的移动方式就是向后覆盖,可能导致无序数列的最左端元素被覆盖,我们需要使用临时变量提前保存数据。上面的代码中,为了便于理解,并没有将所有循环内的变量在循环内定义。但出于以上两点原因,建议将变量在循环外定义,在循环内使用

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

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

相关文章

相机模型概述

相机模型 如图:假设P是现实世界中的一个点,P是三维世界中的点 Pr(Xr,Yr,Zr) 光心O视作摄像头 Pc(Xc,Yc,Zc) 在相机平面中,Pc的坐标为(0,0,0) 在物理成像平面 Pp(Xp,Yp,0) 在像素平面 P(Xp,Yp,0) 但是!!! 到了像素平面,坐标就不一样了,像素平面坐标顶点(最左上角)才是…

【Java系列】深入解析 Lambda表达式

序言 你只管努力&#xff0c;其他交给时间&#xff0c;时间会证明一切。 文章标记颜色说明&#xff1a; 黄色&#xff1a;重要标题红色&#xff1a;用来标记结论绿色&#xff1a;用来标记一级论点蓝色&#xff1a;用来标记二级论点 希望这篇文章能让你不仅有一定的收获&#xf…

揭秘ChatGPT背后的传奇崛起,探索其引爆引爆网络的隐藏故事

文章目录 前言一、ChatGPT的诞生背景二、ChatGPT的技术原理三、ChatGPT的推广策略四、ChatGPT的未来展望五、橙子送书第3期 前言 ChatGPT是一款基于人工智能技术的聊天机器人&#xff0c;它的出现引起了广泛的关注和热议。在短短的时间内&#xff0c;ChatGPT就成为了全球范围内…

TLS SSL

HTTPS HTTPS&#xff0c;全称为 Hypertext Transfer Protocol Secure&#xff0c;是一种通过加密通道传输数据的安全协议。它是 HTTP 协议的安全版本&#xff0c;用于在 Web 浏览器和 Web 服务器之间进行安全的数据传输。HTTPS 在传输过程中使用了 SSL&#xff08;Secure Sock…

使用Compose开发一款桌面端APK逆向工具

目录 1.前言2.小感慨3.逆向工具简介3.1.ApkTool3.2.Jadx3.3.其他工具3.3.1.Dex文件反编译为Jar文件dex2jar 3.3.2.Jar文件反编译为Java文件JavaDecompilerProcyonFernflowerCFR 4.桌面端逆向APK应用的开发4.1.文件拖拽4.2.构造工程目录4.3.文件标签页4.4.关键字高亮4.5.本地图片…

大模型没有壁垒吗?开源模型和chatgpt已经没有差距了吗?

近期有很多工作比如Alpaca、Vicuna、Koala等论文宣称通过收集到的大量chatgpt output&#xff0c;在基于开源大模型如LLaMA上进行微调后的模型就接近甚至超过chatgpt效果。有些看热闹不嫌事大的媒体渲染诸如“复制chatgpt&#xff0c;仅需100美元“&#xff0c;”开源大模型超过…

行为型模式--观察者模式

目录 概述 结构 案例实现 优缺点 优点&#xff1a; 缺点&#xff1a; 使用场景 概述 又被称为发布-订阅&#xff08;Publish/Subscribe&#xff09;模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;让多个观察者 对象同时监听某一个主题对象。这个主题对象在…

vscode c++ 环境配置(终极版)

1. window系统 c 环境配置 1.1 配置MinGw编译器 &#xff08;1&#xff09;下载mingw64 mingw64 的按照包&#xff0c;我已经放在百度网盘上了&#xff0c;搭建可自行下载: 链接: https://pan.baidu.com/s/1NoPGAYFuP5ysXTf8wtvbEA?pwdwd6w 提取码: wd6w &#xff08;2&…

目标检测基础

MTCNN 人脸检测 MTCNN&#xff0c;Multi-task convolutional neural network&#xff08;多任务卷积神经网络&#xff09;&#xff0c;将人脸区域检测与人脸关键点检测放在了一起&#xff0c;它的主题框架类似于cascade。总体可分为P-Net、R-Net、和O-Net三层网络结构。这三个…

IOS工程使用OpenCV库完整步聚

1.打开Xcode15并点击Create New Project 2.引用编译好的opencv2.framework框架 选择添加其它库 选择Add Files ... 选择OpenCV源码编译生成输入的IOS平台的opencv2.framework库 opencv库要放在工程目录下,不然会找不到 成功添加opencv库的引用,现在可在工程中使用openc…

《网络安全0-100》多级安全

1.多级安全 建立安全模型的方法&#xff1a; 信息流模型 访问控制模型 1.1 BLP模型 1.1模型构建 不能下写、不能上读&#xff0c;保持数据的机密性。 例子&#xff1a;军事、商务、外交的机密性强场景。下级可以给上级进行汇报&#xff0c;但下级不能读取上级的作战计划…

【C++学习】C++入门 | 缺省参数 | 函数重载 | 探究C++为什么能够支持函数重载

写在前面&#xff1a; 上一篇文章我介绍了C该怎么学&#xff0c;什么是命名空间&#xff0c;以及C的输入输出&#xff0c; 这里是传送门&#xff1a;http://t.csdn.cn/Oi6V8 这篇文章我们继续来学习C的基础知识。 目录 写在前面&#xff1a; 1. 缺省参数 2. 函数重载 3…

浅谈【AI、算力赋能】“大算力”时代的到来

&#x1f53b;一、【&#x1f4a3; 话题引入&#xff1a;“AI算力最强龙头”&#xff0c;你怎么看&#xff1f;】 &#x1f648; AI人工智能是否可以取代人类&#xff1f;    &#x1f648; 应不应该限制人工智能的发展&#xff1f;      &#x1f648; AI研究及龙头行业迎…

011-从零搭建微服务-接口文档(一)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff08;后端&#xff09;&#xff1a;https://gitee.com/csps/mingyue 源码地址&#xff08;前端&#xff09;&#xff1a;https://gitee.com/csps…

【P2】VMware 下 docker 快速搭建漏洞靶场 DVWA

文章目录 一、docker 快速搭建漏洞靶场指南二、执行步骤三、为 kali 配置 docker 加速器四、访问 dockerhub 的 dvwa 镜像五、漏洞利用初探&#xff0c;修改 requests 请求参数远程执行命令六、vulhub 搭建漏洞复现 包括什么是 docker、docker 和虚拟机的的区别、docker 搭建 D…

阿里云服务器的虚拟化技术和资源隔离如何?是否支持私有云部署?

阿里云服务器的虚拟化技术和资源隔离如何&#xff1f;是否支持私有云部署&#xff1f;   一、阿里云服务器的虚拟化技术   阿里云服务器采用了业界领先的虚拟化技术&#xff0c;为用户提供了强大而灵活的计算性能。这主要体现在以下几个方面&#xff1a;   1.1 弹性伸缩 …

强化学习从基础到进阶-案例与实践[3]:表格型方法:Sarsa、Qlearning;蒙特卡洛策略、时序差分等以及Qlearning项目实战

【强化学习原理项目专栏】必看系列&#xff1a;单智能体、多智能体算法原理项目实战、相关技巧&#xff08;调参、画图等、趣味项目实现、学术应用项目实现 专栏详细介绍&#xff1a;【强化学习原理项目专栏】必看系列&#xff1a;单智能体、多智能体算法原理项目实战、相关技巧…

【CMake 入门与进阶(13)】 CMake如何设置交叉编译(附代码)

cmake如果不设置交叉编译&#xff0c;默认情况下&#xff0c;会使用主机系统&#xff08;运行 cmake 命令的操作系统&#xff09;的编译器来编译我们的工程&#xff0c;那么得到的可执行文件或库文件只能在 Ubuntu 系统运行&#xff0c;如果我们需要使得编译得到的可执行文件或…

javaWeb医药管理系统

一、引言 二、项目截图 2.1 首页设计 2.2一级页面设计 2.2-1注册界面 2.2-2管理员登录界面 2.3二级页面设计 药品信息模块 药品销售 用户信息 三、项目基本要求 1.主要功能 医药管理系统的主要功能为&#xff1a;、药品更新、药品查询 药品更新功能分为三部分&…

前端Vue自定义支付密码输入键盘Keyboard和支付设置输入框Input

前端Vue自定义支付密码输入键盘Keyboard和支付设置输入框Input&#xff0c; 下载完整代码请访问uni-app插件市场地址&#xff1a;https://ext.dcloud.net.cn/plugin?id13166 效果图如下&#xff1a; # cc-defineKeyboard #### 使用方法 使用方法 <!-- ref:唯一ref pas…