C语言数据结构易错知识点(6)(快速排序、归并排序、计数排序)

快速排序属于交换排序,交换排序还有冒泡排序,这个太简单了,这里就不再讲解。

归并排序和快速排序都是采用分治法实现的排序,理解它们对分支思想的感悟会更深

计数排序属于非比较排序,在数据集中的情况下可以考虑使用。这时效率也比较高。

 

下面讲解一下这三种排序在代码实现上易错的地方:

一、快速排序

快速排序通过每趟排序确定一个数的最终位置最终实现将数组变得有序的功能。主要有三种方法,第一种方法偏向常规,第二种方法是双指针法,第三种方法是非递归法,除此之外,还有一些优化的方案,如挖坑法,随机确定key,三数取中,后面代码实现上会简要说明。

方法一:常规法

1.代码实现:


void QuickSort1(int* arr, int left, int right)
{
	int start = left, end = right, key = left;

	if (left >= right)
		return;

	while (left < right)
	{
		while (left < right && arr[right] >= arr[key])
			right--;
		while (left < right && arr[left] <= arr[key])
			left++;

		swap(&arr[left], &arr[right]);
	}

	swap(&arr[key], &arr[left]);

	key = left;

	QuickSort1(arr, start, key - 1);
	QuickSort1(arr, key + 1, end);
}

2.下面是单趟排序的逻辑:

2aa774f668f8497b97894ffcadca0e70.png

至于需要注意的点,就是当key在左侧时,一定要右侧先动,左侧再动,这样才能保证相遇点的值小于等于arr[key](这个结论可以画图证明,这里就不展开了),当然,逆序同理。

3.下面是整体逻辑的详细解读,写代码时一定要注意细节,后续快排代码不再重复:

4978faa9cd9e4b35b23afafac2f5ef67.png

4.由于相遇点的值小于等于arr[key]这个结论一定程度上难以理解,于是就出现了一种优化方案——挖坑法,代码如下:


void QuickSort5(int* arr, int left, int right)
{
	if (left >= right)
		return;

	int start = left, end = right, key = left, piti = left;//存储坑的下标

	int tmp = arr[key];//用来初始坑里的值

	while (left < right)
	{
		while (left < right && arr[right] >= tmp)
			right--;

		if (left < right)
		{
			arr[piti] = arr[right];
			piti = right;
		}

		while (left < right && arr[left] <= tmp)
			left++;

		if (left < right)
		{
			arr[piti] = arr[left];
			piti = left;
		}
	}

	arr[piti] = tmp;

	key = piti;

	QuickSort5(arr, start, key - 1);
	QuickSort5(arr, key + 1, end);
}

5.当数组接近有序时,每次调用后key的位置都会很偏,导致递归次数接近N,总时间复杂度来到O(N ^ 2),因此取key尤为关键,关键在于不要一来就取到最小值,因此随机取key和三数取中可以一定程度上缓解这个问题:,代码如下:

三数取中:


int GetMid(int* arr, int left, int right)
{
	int mid = (left + right) / 2;

	if (arr[mid] >= arr[left])
	{
		if (arr[mid] < arr[right])
			return mid;
		else
		{
			if (arr[left] < arr[right])
				return right;
			else
				return left;
		}
	}

	else
	{


		if (arr[mid] > arr[right])
			return mid;
		else
		{
			if (arr[right] < arr[left])
				return left;
			else
				return right;
		}
	}

}

随机取key:

int key = rand() % (right - left + 1) + left;//随机值

但是这些方法依旧不能完全消除快排的缺陷,当数组是1111111111这种时,时间复杂度依然会变成O(N ^ 2)

6.时间复杂度分析

快排的时间复杂度是O(NlogN),分析方法如下:

1abdf1502e1641ca8d215a2406310bbf.png

需要补充的是,logN与N的差别很大,N-logN依然是N的量级,举个例子:已知2的10次方是1024,假如有1024个数据,在快排中,单趟排序的遍历次数为1024、1023、1022一直到1014(末项可能会更小,但不会小多少),我们发现,这些数字差别很小,几乎是由N来决定的,所以上面图中说我们一般可以将单趟遍历的次数看作N

方法二:双指针法

这个方法的代码实现比较简单,关键在于prev指针和cur指针之间嵌入大于arr[key]的值,注意每次arr[prev]的下一个元素都是大于arr[key]的(当然也有可能相同或者没有元素,但是在这两种特殊情况下对我们功能的实现没有影响),最后交换arr[key]和arr[prev]达到一样的效果,使右侧大于arr[key],左侧小于等于arr[key]。后续逻辑相同。

代码实现如下:


void QuickSort3(int* arr, int left, int right)
{
	int prev = left, cur = left + 1, key = left;

	if (left > right)
		return;

	while (cur <= right)
	{
		if (arr[cur] > arr[key])
			cur++;
		else
			swap(&arr[++prev], &arr[cur++]);
	}

	swap(&arr[key], &arr[prev]);

	key = prev;

	QuickSort3(arr, left, key - 1);
	QuickSort3(arr, key + 1, right);
}

方法三:非递归法

这种方法利用了前序遍历和栈的相似性。为什么说是前序遍历呢?因为在遍历时,该层遍历还同时保存了左递归和右递归的必要信息,就像二叉树的父节点能够找到子节点,而子节点找不到父节点。如果是中序或后序就没有这样的特性,就不适合用栈或队列等数据结构来实现。这也是为什么归并排序不适合用栈和队列来实现非递归。

代码如下:


void QuickSort4(int* arr, int left, int right)
{
	Stack s;
	StackInit(&s);
	StackPush(&s, right), StackPush(&s, left);//从右向左入栈

	while (!isStackEmpty(&s))
	{
		int left = StackTop(&s);
		StackPop(&s);
		int right = StackTop(&s);
		StackPop(&s);

		int prev = left, cur = left + 1, key = left;//从左向右取数据

		while (cur <= right)
		{
			if (arr[cur] > arr[key])
				cur++;
			else
				swap(&arr[++prev], &arr[cur++]);
		}

		swap(&arr[key], &arr[prev]);

		key = prev;

		if(key + 1 < right)
			StackPush(&s, right), StackPush(&s, key + 1);//后取得
		if(left < key - 1)
			StackPush(&s, key - 1), StackPush(&s, left);//先取得

	}

	StackDestroy(&s);
}

相关栈的功能实现如下:


void StackInit(Stack* ps)
{
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}

void StackPush(Stack* ps, int val)
{
	if (ps->capacity == ps->size)
	{
		ps->capacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		int* tmp = (int*)realloc(ps->arr, sizeof(int) * ps->capacity);
		assert(tmp);
		ps->arr = tmp;
	}

	ps->arr[ps->size++] = val;
}

int StackTop(Stack* ps)
{
	return ps->arr[ps->size - 1];
}

void StackPop(Stack* ps)
{
	ps->size--;
}

bool isStackEmpty(Stack* ps)
{
	return ps->size == 0;
}

void StackDestroy(Stack* ps)
{
	free(ps->arr), ps->arr = NULL;
	ps->capacity = ps->size = 0;

}

需要注意入栈顺序是从右向左,右下标先入,左下标后入,右递归先入,左递归后入。基本逻辑和递归相似,理解了原理就很好写。

二、归并排序

归并排序一定程度上比较好理解,但非递归法需要注意的细节比较多

方法一:递归法


void _MergeSort(int* arr, int* tmp, int start, int end)
{
	if (start == end)
		return;

	int mid = (start + end) / 2;
	_MergeSort(arr, tmp, start, mid);
	_MergeSort(arr, tmp, mid + 1, end);

	int start1 = start, end1 = mid;
	int start2 = mid + 1, end2 = end;
	int i = start1;

	while (start1 <= end1 && start2 <= end2)
	{
		if (arr[start1] <= arr[start2])
			tmp[i++] = arr[start1++];
		else
			tmp[i++] = arr[start2++];
	}
	
	while (start1 <= end1)
	{
		tmp[i++] = arr[start1++];
	}

	while (start2 <= end2)
	{
		tmp[i++] = arr[start2++];
	}

	memmove(arr + start, tmp + start, sizeof(int) * (end - start + 1));
}


void MergeSort1(int* arr, int size)
{
	int* tmp = (int*)malloc(sizeof(int) * size);
	assert(tmp);

	_MergeSort(arr, tmp, 0, size - 1);

	free(tmp), tmp = NULL;
}

整体逻辑:

0de86724086243989b9150329223f24f.png

7e75af49074e4b4b87edbf835f8b5a28.png

方法二:非递归法

非递归法理解起来较为困难,下面详细分析:

代码实现:


void MergeSort2(int* arr, int size)
{
	int* tmp = (int*)malloc(sizeof(int) * size);
	assert(tmp);

	int gap = 1;

	while (gap <= size)
	{

		for (int i = 0; i < size; i += 2 * gap)
		{
			int start1 = i, end1 = start1 + gap - 1;
			int start2 = end1 + 1, end2 = start2 + gap - 1;
			int j = start1;

			if (end1 >= size || start2 >= size)
				break;
			while (end2 >= size)
				end2--;

			while (start1 <= end1 && start2 <= end2)
			{
				if (arr[start1] <= arr[start2])
					tmp[j++] = arr[start1++];
				else
					tmp[j++] = arr[start2++];
			}

			while (start1 <= end1)
			{
				tmp[j++] = arr[start1++];
			}

			while (start2 <= end2)
			{
				tmp[j++] = arr[start2++];
			}

			memmove(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));

		}

		gap *= 2;
	}

	free(tmp), tmp = NULL;
}

1.整体逻辑

ace813579db94594a8961f1fb9b515d1.png

它的逻辑大体上和递归法相似,但非递归法是直接从最小元素开始向上归并。

2.非递归归并的难点在于数组前面归并是两两为一组,会导致有落单的情况,需要进行处理,上面这张图就展示出了一种情况,下面分析一下:

90cef48aca034af8b525250f38bf479d.png

三、计数排序

这个排序非常简单,不做过多分析

代码实现:


void CountSort(int* arr, int size)
{
	int max = arr[0], min = arr[0];

	for (int i = 0; i < size; i++)
	{
		if (arr[i] > max)
			max = arr[i];
		if (arr[i] < min)
			min = arr[i];
	}

	int range = max - min + 1;

	int* count = (int*)calloc(range, sizeof(int));
	assert(count);

	for (int i = 0; i < size; i++)
	{
		count[arr[i] - min]++;
	}

	for (int i = 0, j = 0; i < range; i++)
	{
		while (count[i]--)
		{
			arr[j++] = i + min;
		}
	}

}

注意这个方法用于数据集中时使用,这个时候效率很高。但分散数据就别用,会浪费大量空间。这个排序讨论其稳定性没有意义,因为它只能做到数值的排序,这些排序后没有意义。

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

四年旅程,一路成长——小雨的创作纪念日

四年旅程&#xff0c;一路成长——小雨的创作纪念日 收到来信&#xff0c;回顾与再开始回首起点&#xff0c;初探技术世界持续前行&#xff0c;从坚持到自信今日之感&#xff0c;持续分享与感恩【3.19故事对话】我一定可以&#xff01;“新”认知状态变化感受复盘 朝着未来&…

探索AI技术创业的未来机遇

目录 前言1 特定行业定制化AI解决方案1.1 数字化转型的迅猛发展1.2 AI技术的广泛应用1.3 定制化的需求与机会 2 智能产品和服务智慧改变生活2.1 智能硬件产品的崭新机遇2.2 基于AI的软件服务的新视野 3 教育和培训AI智能赋能未来人才3.1 AI技术引发的人才需求增长3.2 AI相关教育…

springboot实战---7.springboot制作Docker镜像

&#x1f388;个人主页&#xff1a;靓仔很忙i &#x1f4bb;B 站主页&#xff1a;&#x1f449;B站&#x1f448; &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;SpringBoot &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&…

二. CUDA编程入门-Stream与Event

目录 前言0. 简述1. 执行一下我们的第九个CUDA程序2. Stream是什么3. Streams实验(单流vs多流)4. 如何隐藏延迟(memory)5. 如何隐藏延迟(kernel)6. 如何隐藏延迟(kernelmemory)7. 代码分析总结参考 前言 自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》&#xff0c;链接。记…

AMD GPUs - Radeon™ PRO W7900与NVIDIA 4000系列GPU性能

文心一言 RTX 4090的性能高于AMD Radeon PRO W7900。 RTX 4090具有760亿个晶体管、16384个CUDA核心和24GB高速镁光GDDR6X显存&#xff0c;在4K分辨率的游戏中持续以超过100FPS运行。RTX 4090采用全新的DLSS 3技术&#xff0c;相比3090TI&#xff0c;性能提升可达2~4倍&#x…

Python程序设计 多重循环

教学案例六 多重循环 1.n之内的素数 输入n&#xff0c;显示n之内的所有素数 每行显示10个素数 例如&#xff0c;若输入500&#xff0c;结果如图所示 neval(input()) #代码开始 c 0for i in range(2, n1):for j in range(2, i):if i % j 0:breakelse:c 1print("{:5d}…

机器人---人形机器人之技术方向

1 背景介绍 在前面的文章《行业杂谈---人形机器人的未来》中&#xff0c;笔者初步介绍了人形机器人的未来发展趋势。同智能汽车一样&#xff0c;它也会是未来机器人领域的一个重要分支。目前地球上最高智慧的结晶体就是人类&#xff0c;那么人形机器人的未来会有非常大的发展空…

MyBatis 参数重复打印的bug

现象 最近有个需求&#xff0c;需要在mybatis对数据库进行写入操作的时候&#xff0c;根据条件对对象中的某个值进行置空&#xff0c;然后再进行写入&#xff0c;这样数据库中的值就会为空了。 根据网上查看的资料&#xff0c;选择在 StatementHandler 类执行 update 的时候进…

什么品牌的护眼台灯比较好?收获好评的护眼台灯十大品牌推荐!

在这个数字时代&#xff0c;保护双眼的健康愈发受到人们的重视。而护眼台灯&#xff0c;作为守护视力的得力助手&#xff0c;其品牌选择至关重要。究竟什么品牌的护眼台灯比较好呢&#xff1f;今天&#xff0c;我将为大家推荐收获好评的护眼台灯十大品牌&#xff0c;这些品牌凭…

前端-css-01

1.CSS 长度单位和颜色设置 1.1CSS 中的长度单位 px 像素 em 字体大小的倍数&#xff08;字体默认是16px&#xff09; % 百分比 1.2CSS 中的颜色设置方式 1.2.1使用颜色名表示颜色 red、orange、yellow、green、cyan、blue、purple、pink、deeppink、skyblue、greenyellow .…

火鸟门户—团购秒杀

团购杀秒 简介 团购秒杀是一种基于社交裂变的电商模式&#xff0c;用户可以发起或参与拼团&#xff0c;以后续的价格购买商品。团购秒杀可以有效提升商品销量、引流新用户、增强用户粘性。 功能 团购&#xff1a;用户可以发起或参与拼团&#xff0c;以不同的价格购买商品。 秒杀…

docker部署修改主机网络

教学版教程&#xff1a;docker 部署教学版本-CSDN博客文章浏览阅读1.1k次&#xff0c;点赞23次&#xff0c;收藏18次。1&#xff09;docker 部署mysql、redis、nginx ;2)docker compose一键单机部署&#xff1b;3&#xff09;docker网络&#xff1b;4&#xff09;dcocker swarn…

混合现实(MR)开发工具

混合现实&#xff08;MR&#xff09;开发工具是一系列软件和框架&#xff0c;它们使得开发者能够创建和优化能够在虚拟与现实世界之间无缝交互的应用程序。以下是一些在MR领域内广泛使用的开发工具。 1.Microsoft Mixed Reality Toolkit (MRTK) MRTK是一个跨平台的工具包&…

石煤酸浸提钒工艺-树脂

摘要&#xff1a;海普提钒树脂在使用中拥有更高交换容量、树脂处理量更大、吸附精度高&#xff0c;对钒的选择性更好&#xff0c;配合海普提钒离子交换富集纯化工艺设备&#xff0c;更好的保证了系统运行平稳性与可靠性。​​ #石煤酸浸提钒工艺-树脂 ​钒是一种重要的战略物资…

【小黑送书—第十八期】>>让工作自动化起来!无所不能的Python(文末送书)

随着我国企业数字化和信息化的深入&#xff0c;企业对办公自动化的效率和灵活性要求越来越高。Python作为一种开源的软件应用开发方式&#xff0c;通过提供强大丰富的库文件包&#xff0c;极大地简化了应用开发过程&#xff0c;降低了技术门槛。Python开发有哪些优势、挑战以及…

设备健康监测系统:保障设备安全与稳定运行!

前言 随着科技的不断发展&#xff0c;各种设备的使用也越来越广泛&#xff0c;从而带来了更多的设备管理和维护问题。设备健康监测系统应运而生&#xff0c;它是一种能够监测、分析和管理设备健康状况的系统&#xff0c;及时发现设备故障和降低维修成本。该系统通过实时监测设…

shell脚本发布docker springboot项目示例

docker、git、Maven、jdk8安装略过。 使git pull或者git push不需要输入密码操作方法 约定&#xff1a; 路径&#xff1a;/opt/springbootdemo&#xff0c; 项目&#xff1a;springbootdemo&#xff0c; 打包&#xff1a;springbootdemo.jar&#xff0c; docker容器名字&#x…

蓝桥杯刷题day13——自助餐【算法赛】

一、问题描述 食堂最近推出了自助取餐功能,可以通过盘子的形状自动计算费用。你参与到自助计算价格的项目工作中。视觉组的同学已经帮你通过图像识别把盘子图片转换为了字符串,你只需要计算具体的价格即可。 餐盘的费用如下表所示: 你将会得到n 个字符串,请按照价格表计算…

【Linux】动态库与静态库

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.为什么要有库&…

数字人克隆系统源码部署,该如何选择源码厂家

短视频领域成2024年风口&#xff0c;数字化概念成为我们生活中不可或缺的一部分。在数字化的大潮中&#xff0c;有很多创业者开始瞄准了数字人源码部署这个商业风口项目。而许多企业开始考虑采用数字人系统进行业务拓展和服务升级&#xff0c;那么如何选择适合自己的数字人源码…