手搓交换排序、归并排序、计数排序

news2024/9/24 15:18:20

文章目录

  • 交换排序
    • 冒泡排序
    • 快速排序
      • hoare版本
      • 挖坑法
      • lomuto前后指针
    • 非递归快速排序
  • 归并排序
  • 实现计数实现排序
  • 代码测试排序算法性能

交换排序

冒泡排序

void BubbleSort(int* arr, int n)
{
	for (int i = 0; i < n; i++)
	{
		int flag = 0;
		for (int j = 0; j < n - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				Swap(&arr[j], &arr[j + 1]);
				flag = 1;
			}
		}
		if (0 == flag)
			break;
		
	}
}

时间复杂度:O(N^2)

快速排序

快速排序是一种二叉树结构的交换排序方式,基本思想:任取待排元素序列中的某元素作为基准值,按照该基准值将待排序列分割成两子序列,左子序列所有元素均小于该基准值,右子序列均大于该基准值,然后在左子序列,和右子序列重复上述过程,直到待排元素符合预期结果。

void QuickSort(int* arr, int left, int right)
{
	if (left >= right)//left 等于 right说明此时子序列里只有一个数据了,若left 大于 right说明此时子序列为空
	{
		return;
	}
	//int meet = hoare_QuickSort(arr, left, right);
	//int meet = hole_QuickSort(arr, left, right);
	//int meet = lomuto_QuickSort(arr, left, right);
	QuickSort(arr, left, meet - 1);
	QuickSort(arr, meet + 1, right);
}

时间复杂度:O(nlogn~n^2),在以下查找基准值的方法,是以待排序列首元素位基准值,这种方法存在缺陷,使得快速排序的性能下降,时间复杂度及较高。

情景:

待排序列位有序,降序、升序、所有元素大小相同,这三种场景中将待排序列排序位升序的序列,时间复杂度位O(n^2),挖坑,找基准值的方法后续补充。(我在详细讲解为什么这三种场景,这么找的基准值)

空间复杂度:O(logn ~ n)

hoare版本

  • 随机选择一个基准值例如:待排序列首元素,定义两个指针,left和riight。
  • left指针负责从左到右寻找比基准值大的元素
  • right指针负责从右向左寻找比基准值小的元素
  • 找到后将left和right交换,重复以上过程直到left的值大于right的值时,将基准值与right交换。
  • 此时right所在的位置就是基准值应该在的位置
int hoare_QuickSort(int* arr, int left, int right)
{
    int key = left;
    left++;
    while(left <= right)
    {
        while(left <= right && arr[right] > arr[key])
        {
            right--;
        }
        while(left <= right && arr[left] < arr[key])
        {
            left++;
        }
        if(left <= right)
            Swap(&arr[left++], &arr[right]--);
    }
    Swap(&arr[right], &arr[key]);
    return right;
}

在这里插入图片描述

此时走完循环的最后一步,在left7的位置和right3的位置发生交换后,此时right–,left++,若最外层循环的条件是left < right,此时跳出循环,right和key发生交换,交换完的结果不满足左子序列都小于基准值的标准,是一个错误的代码。不止最外层循环的条件必须是 left <= right,包括内层的两个while循环和if判断语句都是避免这种情况的发生,使得最后一次跳出循环right在left的左边,left在right的右边。

如果不等与使最后的left越过right,是用left < right的限制条件,不保证left和right同时所指的数据满足右子序列大于基准值,或左子序列小于基准值,而直接与rgiht交换

找基准值的函数,看似有两层循环,实际上是一层循环,通过left和right遍历数组元素,时间复杂度O(N)

时间复杂度:找相遇点的时间复杂度为O(n)

挖坑法

不断挖坑找坑填坑的过程

  • 随机选择一个基准值,设待排序列首元素为坑位hole,通过变量key保存基准值,此时这个位置就是一个坑,可以用别的数据进行填坑,再定义两个指针,left和riight。
  • right指针负责从右向左寻找比基准值小的元素,找到后与将其填入坑中,让right此时所指向的位置设位新的坑
  • left指针负责从左到右寻找比基准值大的元素,找到后与将其填入坑中,让left此时所指向的位置设位新的坑
  • 如此循环直到,不满足left < right后跳出循环,最后将key填入坑hole里,此时hole的位置就是基准值的位置。
int hole_QuickSort(int* arr, int left, int right)
{
	int hole = left;
	int key = arr[left];
	while (left < right)
	{
		while (left < right && arr[right] > key)
		{
			right--;
		}
		arr[hole] = arr[right];
		hole = right;
		while (left < right && arr[left] < key)
		{
			left++;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = key;
	return hole;
}

不同于hoare版本的找基准值,通过挖坑法,在循环里,当left == right就直接跳出,hoare版本的left和right相等时需要判断,当前它们所指的值比基准值大还是小,而挖坑法就不需要考虑,left和right相等时,同时指向的位置是一个坑,坑内不存在有效数据,最后直接将基准值填坑即可。

lomuto前后指针

定义两个指针prec,cur,它们只负责找比基准值小的元素。

  • 初始条件prev指向数组首元素,cur指向prev的下一个元素,初始基准值位于基准值首元素位置
  • 交换条件:当cur此时指向的值比基准值还要小的话prev加1,然后交换两者指向的值,大的跑到后边,小大跑到前面,若jprev和cur相等就不需要交换,让cur继续向走,找比基准值小的元素。
  • 直到cur越过边界right,跳出循环,此时prev指向的位置就为基准值的位置
int lomuto_QuickSort(int* arr, int left, int right)
{
    int prev = left;
    int cur = left + 1;
    int key = left;
    while(cur <= right)
    {
        if(arr[cur] < arr[key] && ++prev != cur)
        {
            Swap(&arr[cur], &arr[prev]);
		}
        cur++;
	}
    Swap(&arr[prev], &arr[key]);
    return prev;
}

非递归快速排序

实现非递归的快速排序,需要借组数据结构栈。栈:先进后出

递归版本的快速排序,通过基准值分割左右子序列,定义了left和right来限定左右子序列的取值范围,也是通过left和right来实现对序列的分割。找完待排序列的基准值后,通过栈保存左右子序列的取值范围,来模拟递归的场景

由于栈的特殊结构,入栈时先入右区间,最后入左区间,出栈的顺序就为左区间、右区间。

  • 先将初始左右区间入栈
  • 在循环里出栈,为了和left,right区分开来,使用begin个end来接收序列的左右区间。
  • 找基准值,找到基准值后,通过基准值分割新的左右子序列。[begin, key - 1]和[key + 1, begin]
  • 可以先模拟递归左子序列,然后再模拟递归右子序列。先入栈右区间,再入栈左区间
  • 直至栈为空时,结束循环,排序完成
void QuickSort_NonR(int* arr, int left, int right)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, right);
	StackPush(&st, left);
	while (st.top)
	{
		int begin = StackTop(&st);
		StackPop(&st);
		int end = StackTop(&st);
		StackPop(&st);
        
		int prev = begin;
		int cur = begin + 1;
		int key = begin;
		while (cur <= end)
		{
			if (arr[cur] < arr[key] && ++prev != cur)
			{
				Swap(&arr[prev], &arr[cur]);
			}
			cur++;
		}
		Swap(&arr[prev], &arr[key]);
		key = prev;
        
        
		if (begin < key -1)
		{
			StackPush(&st, key - 1);
			StackPush(&st, begin);
		}
		if (key + 1 < end)
		{
			StackPush(&st, end);
			StackPush(&st, key + 1);
		}
        
	}
	StackDestory(&st);
}

时间复杂度:O(nlogn)

归并排序

归并排序是一种稳定的排序算法,该算法采用分治法,通过递归数组进行分半,递归的对两半序列进行排序,通过不断的将两组有序序列合并为一个有序序列
在这里插入图片描述

  • 通过将两个有序序列合并为一个有序序列,称为二路归并
  • 将1 和 3合并为一个有序序列, 1 3
  • 1 39合并为 一个有序序列,1 3 9
  • 将10 和 6 合并为一个有序序列 6 10
  • 6 101 3 9合并为一个有序序列,1 3 6 9 10,排序完成。
    在这里插入图片描述
void _MergeSort(int* arr, int left, int right, int* temp)
{
	if (left >= right)//递归结束条件 [left, right]
	{//一但left与right重合或left大于right,结束递归
		return;
	}
	int mid = (left + right) / 2;
	_MergeSort(arr, left, mid, temp);
	_MergeSort(arr, mid + 1, right, temp);//递归,分半

	int begin1 = left;
	int end1 = mid;//第一个序列的区间,左区间
	int begin2 = mid + 1;
	int end2 = right;//第二个序列的区间,右区间
	int index = begin1;//临时数组的下标起始,从左区间开始


	while (begin1 <= end1 && begin2 <= end2)//对两个有序序列进行排序
	{
		if (arr[begin1] < arr[begin2])
		{
			temp[index++] = arr[begin1++];
		}
		else
		{
			temp[index++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)//避免该区间还有剩余
	{
		temp[index++] = arr[begin1++];
	}
	while (begin2 <= end2)//避免该区间还有剩余
	{
		temp[index++] = arr[begin2++];
	}
	for (int i = left; i <= right; i++)//将临时数组所有元素拷贝到原数组中。
	{
		arr[i] = temp[i];
	}
}
void MergeSort(int* arr, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	_MergeSort(arr, 0, n - 1, temp);
	free(temp);
}
  • 实现对两个有序序列排序,合成为一个有序序列,并不会在数组里发生,一但这样做,在归并过程会造成数据丢失、覆盖,这里创建一个同等大小的临时数组,来对序列进行排序。
  • 这样做就无法在 MergeSort函数里实现递归,需要在创建一个函数命名为 _MergeSort,别忘了将临时数组传递过去
  • 使用 (left + right) / 2;,来完成对数组进行分半 [left, mid]和[mid + 1, right]
  • 这里的递归步骤与二叉树的后续遍历,特别相似。

时间复杂度:O(nlogn)

空间复杂度:O(n)

实现计数实现排序

计数排序:

  • 基于原序列的最大值和最小值相减后加1的结果开辟一个新数组,用来统计相同元素出现个数
  • 根据该元素的大小所对应下标,放置该元素出现的个数
  • 根据统计的结果将序列回收到原来的序列中

适用范围

  • 与数据范围集中的情况

  • 只适用于整数排序,不适用小数

  • 数据过多,可能会对内存消耗造成负担

假设现在有一组数据:6 1 2 9 4 2 4 1 4

  • 先根据最大值最小值的差值开辟空间,若是根据最大值开辟,当最大值为109万,最小值为100,一共有10个数据,此时根据最大值直接开辟空间会很浪费,109个整形大小的空间,只用来存放10个数据。

  • 1:2次, 2:2次,4:3次, 6:1次, 9:1次。其余没有出现的数据,默认使用0
    在这里插入图片描述

  • 根据下标打印,下标为1的元素出现2次,打印两个1,下标为2的元素出现两次,打印两个2,依次类推,最终打印的结果为 1 1 2 2 4 4 4 6 9,这就排序完成。


  • 此时对 100 101 109 105 101 105,进行计数排序。

  • 更具最大值减去最小值后加1开辟空间,109 - 100 + 1 = 10,给count开辟10个整形大小空间。

  • 统计每个数据出现的个数,根据该数据对应下标存放在count数组里,而此时数组里下标从0开始,待排序列最小值从100开始,无法一一对应。

  • 通过将待排序列的每个元素大小减去最小值 0 1 9 5 1 5,即可解决上述问题。

  • 0:出现1次, 1:出现2次, 5,出现2次, 9:出现1次
    在这里插入图片描述

  • 此时,根据下对对应的此时,打印下标的值,和原序列并不相同,既然之前减去最小值,打印的时候加上最小值即可。100 101 101 105 105 109


  • 当待排序列中存在负数,此时还是更具将待排序列中每个元素减去最小值,更具这个值来对应count下标存放它出现的次数。

  • -5 -3 2 4 2 1,将每个数据减去最小值,0 2 7 9 2 6,在根据count的次数取下标时,加上最小值即可。

void CountSort1(int* arr, int n)
{
	int max = arr[0];
	int min = arr[0];
	for (int i = 1; i < n; i++)
	{
		if (arr[i] < min)
			min = arr[i];
		if (arr[i] > max)
			max = arr[i];
	}
	//确定数组大小
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc");
		exit(1);
	}
	memset(count, 0, sizeof(int) * range);
	//统计数组中每个元素出现的次数,并放在对应下标的count数组里
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}
	int index = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			arr[index++] = i + min;//放完数据,index++到下一个位置
		}
	}
}

时间复杂度:O(N + range)

空间复杂度:O(range)

代码测试排序算法性能

10万个数据(单位:ms):

没有看错,很夸张,计数排序的运行结果0ms
在这里插入图片描述

100万个数据(单位:ms):

直接选择排序、直接插入排序、冒泡排序性能,没有放出来。
在这里插入图片描述

1000万个数据(单位:ms):
在这里插入图片描述

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

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

相关文章

day13 Java基础——逻辑运算符,位运算符及面试题

day13 Java基础——逻辑运算符&#xff0c;位运算符及面试题 1. 逻辑运算符&#xff1a;与&#xff0c;或&#xff0c;非 package operator;public class Demo07 {public static void main(String[] args) {boolean a true;boolean b false;System.out.println("a &…

【网络问题】网络诊断:远程计算机或设备将不接受连接的解决办法/DNS服务器可能不可用

当网络出现问题时&#xff0c;一定要点击“请尝试运行Windows网络诊断”来获取具体的网络问题&#xff0c; 今天碰到且得以解决的两个问题&#xff1a; 一、远程计算机或设备将不接受连接的解决办法 打开控制面板——点击“网络和Internet”——点击“Internet选项”&#xf…

电脑自动重启是什么原因?重启原因排查和解决办法!

当你的电脑突然毫无预警地自动重启&#xff0c;不仅打断了工作流程&#xff0c;还可能导致未保存的数据丢失&#xff0c;这无疑令人很懊恼&#xff0c;那么&#xff0c;电脑自动重启是什么原因呢&#xff1f;有什么方法可以解决呢&#xff1f;别担心&#xff0c;在大多数情况下…

《从零开始:使用Python构建简单Web爬虫》

前言 随着互联网信息的爆炸性增长&#xff0c;如何高效地获取和处理这些数据变得越来越重要。Web爬虫作为一种自动化工具&#xff0c;可以帮助我们快速抓取所需的网页内容。本文将介绍如何使用Python编写一个简单的Web爬虫&#xff0c;并通过实例演示其基本用法。 准备工作 …

创建互动照片墙:HTML、CSS 和 JavaScript 实战

在这个数字化时代&#xff0c;照片已经成为我们生活中不可或缺的一部分。无论是记录重要时刻&#xff0c;还是分享日常生活&#xff0c;我们都离不开照片。今天&#xff0c;我们将一起探索如何使用 HTML、CSS 和 JavaScript 创建一个互动的照片墙程序&#xff0c;让您可以轻松展…

四步构建App跨渠道归因分析方法

通常来讲&#xff0c;在互联网场景中&#xff0c;最简单也最常用的App归因模型就是基于最后一次点击来源进行归因转化&#xff0c;因为越靠近决策环节的时刻通常影响就越大。 不过有机构对营销测量的研究发现&#xff0c;只有11%的营销人员对他们的归因模型的准确性感到“非常…

大语言模型(LLM)快速理解

自2022年&#xff0c;ChatGPT发布之后&#xff0c;大语言模型&#xff08;Large Language Model&#xff09;&#xff0c;简称LLM掀起了一波狂潮。作为学习理解LLM的开始&#xff0c;先来整体理解一下大语言模型。 一、发展历史 大语言模型的发展历史可以追溯到早期的语言模型…

视频孪生:如何有效利用智慧机房里的视频监控系统?

机房是存储设备和数据的重要场所。常见的机房安全隐患有电源不稳定、设备温度异常、空调及新风系统故障、机房漏水等&#xff0c;因此需要管理人员全天轮班值守巡检。传统机房运维工作繁琐且效率低下&#xff0c;对监控设备的利用率不高&#xff0c;而视频孪生技术能很好地解决…

02 pip指令的使用

pip 是一个现代的&#xff0c;通用的 Python 包管理工具 。提供了对Python 包的查找、下载、安装、卸载的功能。 1. 在安装好的python环境下&#xff0c;进入以下目录可以查看到pip命令。 同样在windows命令窗口进行测试&#xff0c;pip命令是否可用。WindowsR键&#xff0c;使…

“职场中,不要和上司作对”,真的很重要吗?你认同这句话吗?

在职场上&#xff0c;领导对下属的期望永远都只有两个字&#xff0c;不是忠诚&#xff0c;也不是能力&#xff0c;而是省心。 领导对下属的要求就是别让我操心。 在职场中&#xff0c;通常面临的首要问题就是如何与领导相处。 把职场中的前辈当作老师来尊重&#xff0c;你尊…

基础复习(多线程)

线程创建方式 1.继承Thread类 2.实现Runable接口 3.Callable接口实现有返回值的线程 &#xff08;1&#xff09;第一种 提供了一个类叫做Thread&#xff0c;此类的对象用来表示线程。创建线程并执行线程的步骤如下 1.定义一个子类继承Thread类&#xff0c;并重写run方法 2.创建…

无密码sudo

文件路径&#xff1a;/etc/sudoers 修改sudoers文件 进去root 权限&#xff1a;sudo su 加入sudoers 写权限&#xff1a;chmod w sudoers 修改sudoers文件&#xff1a;vim sudoers 根据下面图片修改 wq退出编辑

华为LTC流程体系详解

LTC&#xff0c;全称Lead to Cash&#xff0c;中文翻译为从线索到现金&#xff0c;是一种企业运营管理思想&#xff0c;也是一个集成的业务流程。它涵盖了企业从接触客户到收到客户回款的整个流程&#xff0c;通过科学化管理&#xff0c;实现更高效地将线索客户转化为付费客户。…

学习web前端三大件之HTML篇

HTML的全称为超文本标记语言&#xff0c;是一种标记语言。它包括一系列标签&#xff0c;通过这些标签可以将网络上的文档格式统一&#xff0c;使分散的Internet资源连接为一个逻辑整体。HTML文本是由HTML命令组成的描述性文本&#xff0c;HTML命令可以说明文字&#xff0c;图形…

每日一面系列之美团面试拷打:ConcurrentHashMap 为何不能插入 null?HashMap 为何可以

ConcurrentHashMap 为什么 key 和 value 不能为 null&#xff1f; ConcurrentHashMap 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值&#xff0c;表示没有对象或没有引用。如果你用 null 作为键&#xff0c;那么你就无法区分这个键是否存在于 Concu…

1. shiro的基本使用

一、现存问题 1. 认证&#xff08;登录&#xff09;&#xff1a;认证操作流程都差不多&#xff0c;但是每次都需要手动的基于业务代码去实现&#xff0c;很麻烦&#xff01; 2. 授权&#xff1a;如果权限控制粒度比较粗&#xff0c;可以自身去实现&#xff0c;但是如果控制粒度…

fastadmin插件市场暂不可用,是否切换到本地插件

今天调试时需要安装一个富文本插件&#xff0c;结果在插件管理模块提示如下错误&#xff1a; 经过参考网上资料&#xff0c;最终解决方案&#xff1a; 修改backend/config目录下&#xff0c;fastadmin.php 中代码&#xff1a; //API接口地址 api_url > https://api.iuok.c…

canopenfestival生成字典使用的总结

添加地图变量 1、不带索引计数的 子索引的值类型固定 2、带索引计数 子索引的值类型固定,子索引名字不可更改 3、带索引计数 子索引的值类型不固定&#xff0c;子索引名字可更改

HarmonyOS多目标产物构建最佳实践

背景 在Android或iOS开发时经常会有打“马甲”包的场景&#xff0c;就是一套代码打出不同主题的包&#xff0c;一个公司的产品可能针对不同用户提供不同的应用&#xff0c;比如抖音有国内版也有国外版&#xff0c;滴滴有个人版还有企业版&#xff0c;同样的在鸿蒙平台也有类似…

百元内性价比最高的随身WiFi!格行随身WiFiVS京东云VS先机随身WiFi真实测评!哪个随身WiFi网速最快?口碑最好的随身WiFi!

随身WiFi是大家都熟知的便携式上网设备不管是出差旅行✈还是学生党租房都非常合适。但是现在市面上的随身WiFi产品良莠不齐‼价格也千差万别&#xff01;今天给大家挑选三款百元内性价比最高的随身WiFi出期测评看下哪款最好用——先机、格行和京东云的详细测评&#xff0c;帮助…