手搓排序算法:插入排序、选择排序

news2024/9/9 0:07:08

文章目录

  • 插入排序
    • 直接插入排序
    • 希尔排序
      • 内层循环时间复杂度计算
  • 选择排序
    • 直接选择排序
      • 优化
    • 堆排序

插入排序

直接插入排序

时间复杂度最差:大的数据都在左边,小的数据在右边,随着有序区间增大,交换次数增多

时间复杂度最优:小的数据在左边,大的数据在右边。

直接插入排序,通过构建有序序列,对于为排序的数据,在已排序的序列,从后向前寻找到适合位置插入。

  • 从第一个元素开始,认为前一个元素已经排序(假设)
  • 从为排序的第一个元素开始,将其与已经排序的元素从前向后依次进行比较,找到合适的位置插入。
  • 对于下一个为被排序的元素重复以上过程,直到所有元素排序完成

时间复杂度:考虑最坏的情况,对逆序的数组排成有序的时间复杂度为O(n^2),最好的情况,对有序的数组进行排序,时间复杂度为O(n)

空间复杂度:O(1)

void InsertSort(int* arr, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0 && arr[end] > tmp)
		{
			arr[end + 1] = arr[end];
			end--;
		}
		arr[end + 1] = tmp;
	}
}

现在假设对 8 4 5 3四个数据使用直接插入排序算法进行排序

  • 8包括8之前的数据被认为是有序的(如果有的话),使用temp保存8的下一个数据,用来与8之前的数据进行比大小
  • 进入内层循环,8 是大于 4,8的位置向前挪动一位,end此时减1,end指向8之后的位置,也就是-1,不在满足循环条件退出
  • 在8之前放入数据4 4 8 5 3
  • 接着,保存8之后的数据,这次是5,将其保存后与8进行比大小,8大,8向前移动,而5这与8之后的有序序列比较大小,寻找合适位置插入
  • 5 与 4 比大小,5大,5就放在了4之前 4 5 8 3
  • 保存8之后的数据,这次是3,将其保存后与8进行比大小,8大,8向前移动,而3这与8之后的有序序列比较大小
  • 5比3大,end–继续向后找,4比3大,end继续减1向后找,这次end减到-1也就说明没有数据可以比较,3就是最小的,将3放入end + 1的位置即可。3 4 5 8

希尔排序

希尔排序,是在直接插入算法的基础上进行优化的排序算法,通过增量序列来提高算法性能。首先对数据进行预排序,将小的数据在左边,大的数据在右边,使直接插入排序的时间复杂度最优。

  • 预排序,根据gap的大小进行分组,排序完更新gap
  • 直接插入排序(gap == 1)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
void ShellSort(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int temp = arr[end + gap];
			while (end >= 0 && arr[end] > temp)
			{
				arr[end + gap] = arr[end];
				end -= gap;
			}
			arr[end + gap] = temp;
		}
	}
}

希尔排序时间复杂度约为:O(n^1.3) 出自严蔚敏----- 《数据结构(C语言版)》

外层while循环,时间复杂度:gap = n / 3,每次循环一次,n除一次3,循环执行x次,就有
n / 3 x = 1 − > n = 3 x − > x = l o g 3 n n / 3^x = 1-> n = 3^x-> x = log_3^n n/3x=1>n=3x>x=log3n
外层循环时间复杂度
O ( l o g n ) O(log^n) O(logn)

内层循环时间复杂度计算

  • 假设有n个数据,一共有gap组,则每组有n / gap个,在gap组中,插入移动的次数最坏的情况下为:

g a p ∗ ( 1 + 2 + … … + ( n / g a p − 1 ) ) gap*(1+2+……+(n/gap - 1)) gap(1+2+……+(n/gap1))

  • 当gap为 n / 3时,移动的总次数为:

n / 3 ∗ ( 1 + 2 ) = n n / 3*(1 + 2) = n n/3(1+2)=n

​ 时间复杂度为:O(n)

  • 当gap为 n / 9时,移动的总次数为:

n / 9 ∗ ( 1 + 2 + 3 + 4 … … + 8 ) = 4 n n / 9 *(1+2+3+4……+8) = 4n n/9(1+2+3+4……+8)=4n

​ 时间复杂度为:O(4n)

  • 当gap为1时,此时插入移动的次数并不需要考虑最坏的情况,通过之前的预排序已经将小的数据在左边,大的数据在右边,将一个乱序的数据排为一个基本有序的,此时直接插入排序的时间复杂度时最优的情况。

​ 时间复杂度为:O(n)
在这里插入图片描述

总的说:希尔排序的平均情况性能通常比最坏情况要好,在某些情况下,希尔排序的实际运行时间可能比理论时间复杂度要快。

gap/3 + 1与 gap/2 + 1的区别:当n等于10

  • gap / 3 + 1 ——>4——>2——>1

  • gap / 2 + 1 ——>6——>4——>3——>2——>1

可以发现当gap除2时比gap除3时需要进行的预排序次数多2次,gap除3里每组的数据个数比gap除2多1个。假设当n非常大时为10w、100w,那gap除2需要执行的循环次数就远大于gap除3,增加了时间性能的消耗。而gap除3只比gap除2多移动一个数据,它带来的影响是小于增加循环的次数的。

选择排序

每⼀次从待排序的数据元素中选出最小(或最大)的⼀个元素,存放在序列的起始位置,直到全部待 排序的数据元素排完

直接选择排序

通过两个指针向后遍历,begin指针从第一个位置开始,mini指针负责遍历begin之后所有的数据,并记录最小的那个数据,mini遍历完数组后将最小的数据与begin交换,交换完成后begin向前走一步mini向前走一步,如此反复循环。

void SelectSort(int* arr, int n)
{
    for(int i = 0; i < n - 1; i++)
    {
        int begin = i;
        int mini = i;
        for(int j = begin + 1; j < n; j++)
        {
            if(arr[j] < arr[mini])
            {
                mini = j;
			}
        }
        Swap(&arr[mini], &arr[begin]);
    }
}

优化

对初代版本的优化,既然向后循环的过程是在不断找第i个数据之后最小的数据,那也可以向后找第i个数据之后最大的数据,让最大的数据和最后一个位置的数据进行交换。

使用begin,end分别记录数组的开始位置和数组末尾,mini负责从前向后找最小的数据然后与begin交换,maxi负责从前向后找最大的数据然后与end交换。

将单向排序的直接选择排序算法,优化为双向的排序算法。begin当为数组的起始位置,end为数组的结束位置,从两头向中间靠拢,当end <= begin时跳出循环结束排序。

void SelectSort(int* arr, int n)
{
	int mini, maxi;
	int begin = 0;
	int end = n-1;
	while(begin < end)
	{
		maxi = mini = begin;
		for(int j = begin + 1; j <= end; j++)
		{
			if(arr[j] < arr[mini])
			{
				mini = j;
			}
			if(arr[j] > arr[maxi])
			{
				maxi = j;
			}
		}
		if(maxi == begin)
			maxi = mini;
		Swap(&arr[maxi], &arr[end]);
		Swap(&arr[mini], &arr[begin]);
		begin++;
		end--;
	}
}

在代码中尤为重要的一点:if(maxi == begin),这串if语句。

若没有这串代码对 5 3 9排序的结果为:
在这里插入图片描述

9 6 3排序的结果为 :
在这里插入图片描述

  • 假设现在有三个数据待排序 9 6 3,刚开始进入循环时,begin指向9的位置,end指向3的位置,maxi和mini都处于begin指向的位置。

在这里插入图片描述

  • mini开始从前向后找小,3是待排数据里最小的,mini此时指向3,出了内层for循环后开始交换,此时begin指向9。

  • maxi开始从前向后找大,9为待排数据里最大的,maxi此时指向9,出了内层for循环后开始交换,此时end指向3。

  • 根据代码,Swap(&arr[maxi], &arr[end]);,maxi与end交换,9 和 3进行交换,此时数组的顺序为 3 6 9
    在这里插入图片描述

  • 到下一步,Swap(&arr[mini], &arr[begin]);,mini与begin交换,数组发生改变,此时mini指向的值为9,begin指向的值为3,发生交换结果为:9 6 3

在这里插入图片描述

这里多进行了一次重复交换,发生重复交换的源头是,begin == maxi 以及 end == mini,两者劈叉了~ ,各自找了个小3~
在这里插入图片描述

解决的办法,通过if语句maxi的位置放在end下面,或者mini的位置放在begin下面。
在这里插入图片描述

此时进行第一次交换 Swap(&arr[maxi], &arr[end]);,maxi与end交换,数组的顺序为 9 6 3,进行第二次交换,Swap(&arr[mini], &arr[begin]);,mini与begin交换,数组顺序为 3 6 9

堆排序

堆排序是借助数据结构堆,来实现的排序算法,借助堆的自上向下调整算法,实现建堆,排序堆。堆详解,

自上向下算法是向下找孩子节点更具传递过来的参数 parent 向下个找孩子。

与自下向上调整算法相比,它会进行更多的判断,因为向下找的孩子有两个。而我们默认的孩子起始是左孩子节点。

在对孩子节点与父亲节点比较大小交换之前还需要比较左孩子节点和右孩子节点,arr[child] > arr[child + 1],若左孩子节点大于有孩子,那child就需要加1,但还有个前提,万一child加1后刚好不满足 child < n的条件从而在后续的交换里导致数组越界访问,所以在if语句里还需要加上一条判断 child + 1 < n

执行孩子节点与父亲节点交换的if语句里,在交换完两个节点后需要更新新的父亲节点,和孩子节点来是否存在比父亲节点还小的值。最后若孩子节点大于父亲节点,那就说明不需要交换,使用break跳出循环即可。

左孩子:孩子 = 父亲 * 2 + 1,child < n

右孩子:孩子 = 父亲 * 2 + 2,child < n

需要注意的:对数据排升序,建大堆,排降序,建小堆

通过建立大堆,出堆顶数据时,会将堆顶数据与堆尾数据进行交换,然后进行向下调整,即大的数据向下浮,小的数据向上浮,堆顶数据出完后,现在数组就是一个降序序列。

建小堆同理,出堆顶数据时,会将堆顶数据与堆尾数据进行交换,然后向下调整,使得小的数据向下浮,大的数据向上浮,出完堆顶数据最终会得到一个,升序序列

使用自下向上调整建堆时间复杂度最有优O(n),自上向下建堆时间复杂度O(nlogn)

void AdjustDown(int* arr, int parent, int n)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && (arr[child] < arr[child + 1]))
		{
			child++;
		}
		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}
void HeapSort(int* arr, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, n);//建大堆,排升序
	}
	int end = n - 1;
	while (end > 0)
	{
		//模拟出堆顶
		Swap(&arr[0], &arr[end]);
		end--;
		AdjustDown(arr, 0, end);
	}
}






		else
			break;
	}
}
void HeapSort(int* arr, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, n);//建大堆,排升序
	}
	int end = n - 1;
	while (end > 0)
	{
		//模拟出堆顶
		Swap(&arr[0], &arr[end]);
		end--;
		AdjustDown(arr, 0, end);
	}
}

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

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

相关文章

C#知识|文本文件操作:删除、复制、移动文件的操作

哈喽,你好啊,我是雷工! 接下来学习文件的删除和复制,实际应用场景,当软件具有自动在线更新功能时,需要先检测服务器是否具有更新版本的安装包,如果有的话需要将其复制到本地进行升级安装,如果有勾选自动清理安装包功能的话,还可以将安装包删除。 01 删除文件 实现功能…

使用MultipartFile来上传单个及多个文件代码示例(前端传参数及后端接收)

背景 前端使用vue或vue+vant上传文件 后端java接收MultipartFile和其他参数 一、MultipartFile上传单个文件代码示例 1.1 MultipartFile上传单个文件,不包含其它参数 1.1.1 控制层代码如下: /*** 1、上传单个文件,不包含其它参数* */ @PostMapping( "/upload")…

docker 建木 发版 (详细教程)

先创建git仓库 Git勤勉 两种方式上传-CSDN博客 把项目送上去 进入建木 可以接着这个来 dockerfile部署镜像 -&#xff1e;push仓库 -&#xff1e;虚拟机安装建木 -&#xff1e;自动部署化 (详细步骤)-CSDN博客 创建分组项目 开始操作 git 上钩子 前面链接里有这个教…

MobaXterm tmux 配置妥当

一、事出有因 缘由&#xff1a;接上篇文章&#xff0c;用Docker搭建pwn环境后&#xff0c;用之前学过的多窗口tmux进行调试程序&#xff0c;但是鼠标滚动的效果不按预期上下翻屏。全网搜索很难找到有效解决办法&#xff0c;最后还是找到了一篇英文文章&#xff0c;解决了&…

upload-labs靶场练习

文件上传函数的常见函数&#xff1a; 在PHP中&#xff0c;‌文件上传涉及的主要函数包括move_uploaded_file(), is_uploaded_file(), get_file_extension(), 和 mkdir()。‌这些函数共同协作&#xff0c;‌使得用户可以通过HTTP POST方法上传文件&#xff0c;‌并在服务器上保存…

浅谈C语言整型类数据在内存中的存储

1、整型类数据 C语言中的整型类数据都归类在整型家族中&#xff0c;其中包括&#xff1a;char、short、int、long、long long这5个大类&#xff0c;而每个大类中又分为两类signed和unsigned,这些都是C语言中的内置类型。以下重点基于char和int这两种类型的数据进行阐述&#x…

妈吖,看过这个大厂的oracle主键自增,我的信心暴增!信创,国产数据库也能行。

创作不易 只因热爱!! 热衷分享&#xff0c;一起成长! “你的鼓励就是我努力付出的动力” 1.数据库oracle自增主键字段思维导图 在Oracle数据库中&#xff0c;可以通过创建序列&#xff08;SEQUENCE&#xff09;来实现自增功能。但也可以不在数据库中实现&#xff0c;而是通过程…

Sequential的使用

卷积前后尺寸不变的 Padding值计算&#xff1a; padding &#xff08;卷积核尺寸-1&#xff09;/2 Sequential 可以简化代码&#xff1a; def __init__(self):super(Tudui, self).__init__()self.model1 Sequential(Conv2d(3, 32, 5, padding2),MaxPool2d(2),Conv2d(32, 32…

ctfshow web入门 CMS web477--web479

web477 CMSEazy5.7 不让扫&#xff0c;那就尝试一下admin路由&#xff0c;成功了 admin登录进入后台 也看到了其实 首页可以看到提示 然后去自定义标签打 1111111111";}<?php phpinfo()?>刷新一下预览即可 11";}<?php assert($_POST[g]);?>也可…

Git和TortoiseGit的安装与使用

文章目录 前言一、Git安装步骤查看版本信息 二、TortoiseGit安装中文语言包TortoiseGit 配置不同语言 Git基本原理介绍及常用指令 GitLab添加TortoiseGIT生成SSH Key 前言 Git 提供了一种有效的方式来管理项目的版本&#xff0c;协作开发&#xff0c;以及跟踪和应用文件的变化…

【FPGA设计】千兆以太网

一. OSI 七层模型 OSI&#xff08;Open Systems Interconnection&#xff09;七层模型是由国际标准化组织&#xff08;ISO&#xff09;制定的一个概念模型&#xff0c;用于描述网络中各部分如何通信。这个模型将网络通信分解为七个不同的层次&#xff0c;每一层都有特定的功能…

嵌入式学习Day14---C语言进阶

目录 一、构造类型 1.1.结构体 1.存储 2.输入输出&#xff08;传参&#xff09; 3.结构体数组 1.2.共同体&#xff08;联合体&#xff09; 1.格式 2.存储 3.测试一个平台是打端还是小端 1.3.枚举 1.格式 2.特点 二、位运算&#xff08;操作二进制&#xff09; 2.1.&a…

研究人员在进行文献综述时可能面临哪些挑战以及如何解决这些挑战

VersaBot一键生成文献综述 对于研究人员来说&#xff0c;进行全面的文献综述可能是一种丰富但具有挑战性的经历。以下是一些常见的障碍以及如何克服这些障碍的技巧&#xff1a; 挑战 1. 信息过载&#xff1a; 已有大量已发表的研究成果&#xff0c;识别、选择和管理相关来源…

学到了一种新的技巧

1、通过erase删除方向&#xff0c;让原本很复杂的代码变得简洁。 2、通过return两个不同类型的答案&#xff0c;使得代码量变得更少。 3、通过bfs将状态转移给后面。 4、这种集成的技巧&#xff0c;根据相同点把不同类的代码组合成一个函数&#xff0c;这种技巧是需要学习的…

React 的 KeepAlive 实战指南:深度解析组件缓存机制

Vue 的 Keep-Alive 组件是用于缓存组件的高阶组件&#xff0c;可以有效地提高应用性能。它能够使组件在切换时仍能保留原有的状态信息&#xff0c;并且有专门的生命周期方便去做额外的处理。该组件在很多场景非常有用&#xff0c;比如&#xff1a; tabs 缓存页面 分步表单 …

C++类与对象-六大成员函数

默认成员函数就是用户没有显式实现&#xff0c;编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个空类编译器会默认⽣成8个默认成员函数。本文只介绍其中6个&#xff0c;C11增加两个函数见后续博客。 目录 一、构造函数 1.1 概念 1.2 特性 1.3 使用举例 1.4 初始化列表 1…

旷野之间32 - OpenAI 拉开了人工智能竞赛的序幕,而Meta 将会赢得胜利

他们通过故事做到了这一点&#xff08;Snapchat 是第一个&#xff09;他们用 Reels 实现了这个功能&#xff08;TikTok 是第一个实现这个功能的&#xff09;他们正在利用人工智能来实现这一点。 在人工智能竞赛开始时&#xff0c;Meta 的人工智能平台的表现并没有什么特别值得…

Java面试八股之@Qualifier的作用

Qualifier的作用 Qualifier 是 Spring 框架中的一个非常有用的注解&#xff0c;它主要用于解决在依赖注入过程中出现的歧义问题。当 Spring 容器中有多个相同类型的 Bean 时&#xff0c;Qualifier 可以帮助指明应该使用哪一个具体的 Bean 进行注入。 Qualifier 的作用&#x…

【error】AttributeError: module ‘cv2.dnn‘ has no attribute ‘DictValue‘(库冲突)

conda list conda remove opencv pip uninstall opencv-python conda list pip 同时卸载两个库 pip uninstall opencv-contrib-python opencv-python 没有and 直接写库名 module ‘cv2.dnn‘ has no attribute ‘DictValue‘解决办法_module cv2.dnn has no attribute d…

spark 3.0.0源码环境搭建

环境 Spark版本&#xff1a;3.0.0 java版本&#xff1a;1.8 scala版本&#xff1a;2.12.19 Maven版本&#xff1a;3.8.1 编译spark 将spark-3.0.0的源码导入到idea中 执行mvn clean package -Phive -Phive-thriftserver -Pyarn -DskipTests 执行sparksql示例类SparkSQLExam…