数据结构|排序算法详解

news2024/11/15 21:35:32

 ​​​​​​​目录

一.插入类 

1.直接插入排序

2.希尔排序

二.选择类,排一次选出一个最值

1.选择排序

2.堆排序

 三.交换类,通过一直交换一次确定数字的位置

1.冒泡排序

2.快速排序

2.1 hoare版本

2.2挖坑法

2.3前后指针法

四.归并类

1.归并排序


一.插入类 

1.直接插入排序

 

 像整理扑克牌一样,把后面的乱序牌依次插入到前面有序的地方,使得整体有序。​​​​​​​

步骤:

1.从第一个元素开始,认为有序范围为下标0-0
2.取有序范围的下一个元素,将该元素与有序范围里的元素不断比较交换,插入到该元素应该在的位置,有序范围加1
3.重复2直到有序范围是全体

// 插入排序
void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		for (int j = i; j > 0; j--)
		{
			if (a[j] < a[j - 1])
				swap(&a[j], &a[j - 1]);
			else
				break;
		}

	}
}
void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

 

时间复杂度:最坏情况下为O(N^2),此时待排序列为逆序,或者整体接近逆序
      最好情况下为O(N),此时待排序列为升序,或者整体接近升序。
空间复杂度:O(1)

2.希尔排序

上述的直接插入排序算法时间复杂度是O(n^2),如果要令此排序算法的时间复杂度要低于O(n^2),必须减少交换的工作量。那通过什么减少交换的工作量呢?希尔排序可以解决这个问题。

希尔在直接排序的基础上对序列进行预处理,然后进行直接插入排序,目的是为了最后一步直接插入排序的时候可以减少交换次数,同时也减少时间上的消耗。预处理时是通过增加交换的两个数字的距离,使得大数字先到较后面避免交换次数过多。

假定数组初始状态:5,1,9,3,7,4,8,6,2

 

首先是预处理,设定初始增量是gap=length/2 =9/2=4,每两个元素之间比较和交换的距离都是4,数据被分成4组,【5,7,2】,【1,4】,【9,8】,【3,6】对这4组分别进行组内直接插入排序。

接着逐步缩小增量,gap=4/2=2,说明两个元素待会比较和交换的距离都是2,被分为两组,对这两组也进行排序。每次gap都缩为一半。

最后增量缩小为1,这时候就是纯正的直接插入排序了,预处理使得这整个序列大致有序,真正减少了交换的次数,也真正减少了时间上的消耗。

// 希尔排序
void ShellSort(int* a, int n)
{
	for (int gap = n / 2; gap >= 1; gap /= 2)
	{
		//根据gap不同,处理一次
		for (int i = 0; i < n; i++)
		{
			//每躺对下标为i的元素所属组进行处理,此时该组有序范围是:i-gap,i-2*gap,……,i-n*gap
			//j控制两两比较的后面元素下标:i i-gap,……i-n*gap      范围大于gap
			for (int j = i; j >= gap; j -= gap)
			{
				if (a[j] < a[j - gap])
				{
					swap(&a[j], &a[j - gap]);
				}
				else
					break;
			}
		}
	}
}

void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

时间复杂度平均:O(N^1.3)
空间复杂度:O(1)
 

二.选择类,排一次选出一个最值

1.选择排序

步骤:

1.未排序列下标为0-n-1,每次从未排序列中选出一个最小值,然后放在未排序序列的起始位置,此时将未排序列下标范围缩小1

2.重复1,直到未排序列只有一个数

 实际中,我们一般一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,一次缩小两个数字的范围,提高排序的效率。

​
// 选择排序
void SelectSort(int* a, int n)
{

	int min = 0, max = 0,minindex=0,maxindex=0;
	for (int i = 0,j=n-1; i<j; i++,j--)
	{
		min = a[i], max = a[i],minindex=i,minindex=i;
		for (int k = i; k <= j; k++)
		{
			if (a[k] < min)
			{
				min = a[k], minindex = k;
			}
			if (a[k] > max)
			{
				max = a[k], maxindex = k;
			}

		}
		swap(&a[i], &a[minindex]);
		//交换后min到了前面,但是有可能最大值是最前面的数,maxindex就应该变动
		if (i == maxindex)
		{
			maxindex = minindex;
		}
		swap(&a[j], &a[maxindex]);

	}

}
void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

​

时间复杂度:最坏情况:O(N^2)
      最好情况:O(N^2)
空间复杂度:O(1)

2.堆排序

我们知道对于数据建立一个大(小)堆,跟节点就是最大(小)值,和选择算法一样,我们每次都取一个最值放到序列前面或者后面,未排序列下标范围缩小1。

步骤:

1.未排序列下标为0-n-1,每次对未排序列进行建堆,选出一个最小值,然后放在未排序序列的起始位置,此时将根节点删除调整堆,堆的个数减1,也就是没排序的数字个数减1

2.重复1,直到未排序列只有一个数

具体可以查看上篇文章二叉树


// 堆排序
void AdjustDown(int* p, int size, int index)
{
	int parent = index;
	int child = parent * 2 + 1;
	//大堆
	while (child < size)
	{
		if (child + 1 < size && p[child + 1] > p[child])
		{
			child++;
		}
		if (p[parent] > p[child])
		{
			break;
		}
		else
		{
			swap(&p[child], &p[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
	}

}
void HeapSort(int* a, int n)
{
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	while (n > 1)
	{
		swap(&a[0], &a[n - 1]);
		n--;
		AdjustDown(a, n, 0);
	}
}

时间复杂度:O(N*log2N)

空间复杂度: O(1)

 三.交换类,通过一直交换一次确定数字的位置

1.冒泡排序

步骤:

1.记未排序范围为0-n-1,在该范围两两交换,大的去后面,然后范围减1

2.重复直到范围里只有一个数

i控制范围的最右边下标,j为两两交换的后面一个数的下标 

// 冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = n-1; i >= 0; i--)
	{
		int flag = 1;
		for (int j = 1; j <= i; j++)
		{
			if (a[j] < a[j - 1])
			{
				swap(&a[j], &a[j - 1]);
				flag = 0;
			}
		}
		if (flag == 1)
			break;
	}
}

时间复杂度:最坏情况:O(N^2)
      最好情况:O(N)
空间复杂度:O(1)

2.快速排序

基本思想:在序列里任意选择一个数字key(一般选第一个),通过一趟排序将要排序的数据分割成独立的两部分,其中第一部分的所有数据都比key都要小,第二部分的所有数据都比key都要大。如图:

这样就将问题转换成排好第一部分和第二部分的子序列的子问题。再对第一部分和第二部分数据分别进行同样的快排,整个问题就解决了。一般可以通过递归和非递归两种方法实现。


递归

整个排序过程可以递归进行,直到key两边数据只有1个或者0个。伪代码:

// 快速排序递归实现
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;
	int keyi = PartSort1(a, left, right);
    //PartSort2(a, left, right);
    //PartSort3(a, left, right);
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1,right);
}

 

单次排序PartSort函数将数据分成三段如何实现?一般有三种实现方式:

2.1 hoare版本

  ​​​​​​​ 

​

// 单次排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int key = left;
	int p2 = right;
	//不能+1, 数据:2 3 4 5    p1==p2==1   2与3会交换
	int p1 = left;
	while (p1 < p2)
	{
		//相等的数也跳过,没必要交换
		while (p2 > p1 && a[p2] >= a[key])
		{
			p2--;
		}
		while (p2 > p1 && a[p1] <= a[key])
		{
			p1++;
		}
		swap(&a[p1], &a[p2]);
	}
	swap(&a[key], &a[p1]);
	return p1;

}

​

2.2挖坑法

​
// 单次挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	int p1 = left, p2 = right;
	while (p1 < p2)
	{
		while (p2 > p1 && a[p2] >= key)
		{
			p2--;
		}
		a[hole] = a[p2];
		hole = p2;
		while (p2 > p1 && a[p1] <= key)
		{
			p1++;
		}
		a[hole] = a[p1];
		hole = p1;
	}

	a[hole] = key;
	return hole;
}

​

2.3前后指针法

基本步骤:

首先确定两个指针位置和key值,key值一般选择最左边的。

 接着cur遍历所有值。

 遍历完成后,最后一步,将key与prev交换


// 单次前后指针法
int PartSort3(int* a, int left, int right)
{
	int key = a[left];
	int pre = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < key&&++pre != cur)
			swap(&a[cur], &a[pre]);
		cur++;
	}
	swap(&a[pre], &a[left]);
	return pre;
}

非递归 

基本思路是将由递归完成的子区间的单词排序由栈实现,由栈记录来每次自区间左右的下标,再循环对栈内区间单次排序处理 。

 

//快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);
	while (!Stackempty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);
		int right = StackTop(&st);
		StackPop(&st);
		int keyi=PartSort3(a,left, right);
		if (keyi + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}
		if (keyi - 1 > left)
		{
			StackPush(&st, keyi-1);
			StackPush(&st, left);
		}
	}
	StackDestroy(&st);

}

时间复杂度:O(N*logN)

空间复杂度:O(N)

 

四.归并类

1.归并排序

步骤:

 而第一步的子区间的排序又可以采用同样的方法,可以使用递归或者迭代实现

 递归


// 归并排序递归实现
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}
	_MergeSort(a, left, left+(right-left) / 2, tmp);
	_MergeSort(a, left + (right - left) / 2+1,right, tmp);
	int p1 = left, p2 = left + (right - left) / 2 + 1;
	int i = left;
	while (p1 <= left + (right - left) / 2  && p2 <= right)
	{
		if (a[p1] < a[p2])
		{
			tmp[i++] = a[p1++];
		}
		else
		{
			tmp[i++] = a[p2++];
		}
	}
	while (p1 <= left + (right - left) / 2)
	{
		tmp[i++] = a[p1++];
	}
	while (p2 <= right)
	{
		tmp[i++] = a[p2++];
	}
	memcpy(a + left, tmp + left, 4*(right-left+1));

}


void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	_MergeSort(a, 0, n - 1, tmp);


	free(tmp);
	tmp = NULL;
}

 迭代

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	for (int i = 1; i < n; i *= 2)
	{
		for (int left1 = 0; left1 < n; left1 += 2 * i)
		{
			//left1 ,left+i-1    left+i,left+2*i-1
			int end1 = left1 + i - 1;
			int end2 = left1 + 2 * i - 1;
			int p1 = left1, p2 = left1 + i;
			int j = left1;
			
			if (end1>=n)
			{
				end1 = n - 1;
				end2 = n-1;
			}
			if (left1 + i  >= n)
			{
				end2 = p2 - 1;
			}
			if (end2 >= n)
			{
				end2 = n-1;
			}
			
			while (p1 <= end1 && p2 <= end2)
			{
				if (a[p1] < a[p2])
				{
					tmp[j++] = a[p1++];
				}
				else
				{
					tmp[j++] = a[p2++];
				}
			}
			while (p1 <= end1)
			{
				tmp[j++] = a[p1++];
			}
			while (p2 <= end2)
			{
				tmp[j++] = a[p2++];
			}
			memcpy(a + left1, tmp + left1, 4 * (end2-left1+1));
		}
		
	}


	free(tmp);
	tmp = NULL;
}

 时间复杂度:O(N*logN)

空间O(N)

 

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

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

相关文章

Go第 5 章:程序流程控制

第五章程序流程控制 5.1程序流程控制介绍 在程序中&#xff0c;程序运行的流程控制决定程序是如何执行的&#xff0c;是我们必须掌握的&#xff0c;主要有三大流程控 制语句。 1)顺序控制 2)分支控制 3)循环控制 5.2 顺序控制 程序从上到下逐行地执行&#xff0c;中间没有任…

C++标准库的智能指针:shared_ptr、weak_ptr和unique_ptr

文章目录智能指针shared_ptr模版类week_ptr模版类unique_ptrC中是没有内存回收机制的&#xff0c;我在之前的一篇文章中提到使用指针的一些基本方法。C在std标准库中也提供了三种封装过的指针模版类&#xff0c;称作为智能指针&#xff1a;shared_ptrunique_ptrweek_ptr 我这里…

JVM性能调优详解

前面我们学习了整个JVM系列&#xff0c;最终目标的不仅仅是了解JVM的基础知识&#xff0c;也是为了进行JVM性能调优做准备。这篇文章带领大家学习JVM性能调优的知识。 性能调优 性能调优包含多个层次&#xff0c;比如&#xff1a;架构调优、代码调优、JVM调优、数据库调优、操…

最优二叉树(哈夫曼树)

一、最优二叉树 1、定义 官方定义&#xff1a;在权值为w1&#xff0c;w2&#xff0c;…&#xff0c;wn的 n个叶子所构成的所有二叉树中&#xff0c;带权路径长度最小(即代价最小)的二叉树称为最优二叉树或哈夫曼树。 通俗来讲&#xff0c;就是给定N个权值作为N个叶子结点&…

仿牛客论坛项目总结

一.数据库中每一张表有哪些字段 user表 用户表 &#xff08;1&#xff09;id 用户的id &#xff08;2&#xff09; username 用户名 &#xff08;3&#xff09;password 密码 &#xff08;4&#xff09;salt 盐 &#xff08;5&#xff09;emai邮箱 &#xff08;6&…

PAT甲级1008 Elevator C++/C语言

1008 Elevator 分数 20 作者 CHEN, Yue 单位 浙江大学 The highest building in our city has only one elevator. A request list is made up with N positive numbers. The numbers denote at which floors the elevator will stop, in specified order. It costs 6 seconds …

联邦学习将如何影响您的日常生活?

人工智能 (AI) 被认为是下一次工业革命的最大创新之一&#xff0c;其中包括机器学习。另一方面&#xff0c;随着原油和电力成为现代工业的基础资源&#xff0c;数据成为人工智能和机器学习的关键要素。 数据隐私与需求之间的冲突 训练的数据样本的大小决定了可用于增强 AI 性能…

CPT203-Software Engineering(2)

文章目录5. Scrum Framework5.1 Scrum Roles5.2 Scrum Activities and Artifacts6. Requirements Engineering6.1 User requirements and system requirements6.2 Functional & Non-functional requirements6.2.1 Functional requirements6.2.2 Non-functional requirement…

第一章:C++算法基础之基础算法

系列文章目录 文章目录系列文章目录前言一、排序&#xff08;1&#xff09;快速排序核心思想思路分析模板&#xff08;2&#xff09;归并排序核心思想思路分析模板稳定性时间复杂度二分查找&#xff08;1&#xff09;整数二分核心思想思路分析模板&#xff08;2&#xff09;浮点…

jetson nano系统引导安装(无外设安装方式)

文章目录一.硬件设置二.系统设置一.硬件设置 插入烧写好系统的SD卡将micro USB线接到jetson nano上&#xff0c;另一端USB A接到电脑上为jetson nano插入电源&#xff0c;开机等待电脑检测到如下盘符说明jetson nano连接成功 二.系统设置 进入电脑的设备管理器&#xff0c;查…

【linux】三种权限的使用和更改、粘滞位和yum的使用

目录 1.权限问题 ①什么是权限&#xff1f; ②小问题 ③默认权限 ④如何更改“人”的权限呢&#xff1f; ⑤更改权限的八进制方案 ⑥强制改权限里的“人”&#xff08;权限人文件属性&#xff09; 2.粘滞位 2.yum的使用 1.权限问题 ①什么是权限&#xff1f; 权限人&a…

HTTP协议解析

HTTP概述 HTTP (全称为 "超文本传输协议") 是一种应用非常广泛的应用层协议~~我们平时打开一个网站, 就是通过 HTTP 协议来传输数据的。 HTTP工作过程&#xff1a; 当我们在浏览器中输入一个 "网址"&#xff0c;此时浏览器就会给对应的服务器发送一个 H…

CTF中的PHP特性函数(上)

前言 对于PHP大家一定不陌生&#xff0c;但你知道PHP在CTF中是如何考察的吗&#xff0c;本文给大家带来的是通过PHP特性来进行CTF比赛中解题出题的知识&#xff0c;会介绍一下CTF中常见的php特性以及围绕该知识点的相关案例&#xff0c;因为内容过多这里分成上中下三篇来讲&am…

操作系统的特征

文章目录&#x1f380;前言&#xff1a;本篇博客知识总览&#x1f354;并发&#x1f387;概念&#xff1a;&#x1f354;共享&#x1f387;概念&#xff1a;&#x1f354;虚拟&#x1f387;概念&#xff1a;&#x1f354;异步&#x1f387;概念&#xff1a;&#x1f3f3;️‍&a…

ThinkPHP 多应用模式之Api路由分组+中间件

ThinkPHP 6.1 在多应用模式下实现API路由分组中间件验证业务 目录 1.创建中间件文件 2.迁移中间件到子应用目录中 3.编辑中间件验证业务 修改命名空间 编写handle处理代码 4.注册中间件 编辑中间件文件 TP内置中间件 5.设置路由分组 优化相同控制器前缀 最终效果&am…

【信息论与编码 沈连丰】第六章:连续信息和连续信道

【信息论与编码 沈连丰】第六章&#xff1a;连续信息和连续信道第六章 连续信息和连续信道6.1 连续消息的信息6.2 连续消息在信道上的传输问题6.3 香农信道容量公式6.4 连续消息的识别和理想接收机6.5 连续信源的数字处理及其编码第六章 连续信息和连续信道 6.1 连续消息的信息…

在 KubeSphere 上部署 OpenLDAP 并进行对接使用

在 KubeSphere 上部署 OpenLDAP 并进行对接-进阶背景前置条件KubeSphere 中部署 LDAP部署 LDAP 应用ApacheDirectoryStudio 验证 LDAP下载部署 ApacheDirectoryStudioApacheDirectoryStudio 测试 LDAP创建 Ldap Search &#xff0c;KS 对接时可选择使用KubeSphere 对接 LDAPHar…

【区块链 | EVM】深入理解学习EVM - 深入Solidity数据存储位置:内存

图片来源: Mech Mind on Unsplash 这是深入Solidity数据存储位置系列的另一篇。在今天的文章中,我们将学习EVM内存的布局,它的保留空间,空闲内存指针,如何使用memory引用来读写内存,以及使用内存时的常规最佳做法。 我们将使用 Ethereum Name Service (ENS)中的合约代码…

实模式和保护模式的区别

实模式和保护模式的区别 实模式和保护模式的来历 最早期的8086 CPU只有一种工作方式 ---- 实模式。数据总线为16位&#xff0c;地址总线为20位。实模式下所有寄存器都是16位。 从80286开始就有了保护模式&#xff0c;从80386开始CPU数据总线和地址总线均为32位&#xff0c;而且…

开发板测试手册——SPI FLASH 读写、USB WIFI 模块(2)

目录 1.8 SPI FLASH 读写测试 20 1.9 USB 接口读写测试 21 1.10 网络接口测试 23 1.10.1 网络连通测试 23 1.10.2 网络速度测试 25 2 网络静态 IP 设置 27 3 USB WIFI 模块测试 31 3.1 WIFI STA 功能测试 32 3.2 WIFI AP 功能测试 35 3.3 USB WIFI 驱动编译 39 前 言…