【数据结构入门】排序算法之交换排序与归并排序

news2024/12/23 22:15:51

前言

        在前一篇博客,我们学习了排序算法中的插入排序和选择排序,接下来我们将继续探索交换排序与归并排序,这两个排序都是重头戏,让我们接着往下看。

 一、交换排序

1.1 冒泡排序

冒泡排序是一种简单的排序算法。

1.1.1 基本思想

它的基本思想是通过相邻元素的比较和交换,让较大的元素逐渐向右移动,从而将最大的元素移动到最右边。

动画演示:

1.1.2 具体步骤

  1. 从列表的第一个元素开始,重复进行以下步骤,直到最后一个元素:
    • 遍历列表中的每一个相邻的元素。
    • 如果相邻元素的顺序错误(较大的元素在前),则交换它们的位置。
  2. 重复上述步骤,直到列表中的所有元素都按照从小到大的顺序排列。

1.1.3 算法特性

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2) ,其中n是列表的长度。因为算法需要对列表中的每个元素进行多次比较和交换操作。
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

1.1.4 算法实现

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

1.2 快速排序

快速排序是一种常用的排序算法。

1.2.1 基本思想

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

注意:

由于选取基准值是任意的,所以会有多种实现方式,每种方式最后的结果也不尽相同。

一般实现的大思路主要有三种:

1.hoare版本(左右指针法)

 注意事项:     

左边做key,右边先走;右边做key,左边先走。这样能够保证两个指针相遇时,以左边为key,相遇位置比key小正好可以交换;以右边为key时,相遇的位置比key大,正好可以交换。

原理如下:

以左边为例:

1.R找小,L找大没有找到,L遇到R

2.R找小,没有找到,直接遇到L,要么就是一个比key小的位置或者直接到keyi

类似的,右边做key,左边先走,相遇的位置就比key要大。

2.挖坑版本


 

3.前后指针版本

实现思路:

1.cur找到比key小的值,++prev curprev位置的值交换,++cur;

2.cur找到比key大的值,++cur;

方法:

1.a[cur] < key , 交换left 和 cur 数据, left++, cur++;

2.a[cur] == key, cur++;

3.a[cur] > key, 交换cur 和 right数据, right++;

4.cur > right 结束

核心思想:

1.与key相等的值,往后推

2.比key小的值,往前甩

3.比key大的值,往后甩

4.与key相等的就在中间

1.2.2 具体步骤

  1. 选择列表中的一个元素作为基准值(pivot),一般选择列表的第一个元素。
  2. 遍历列表,将比基准值小的元素放到基准值的左边,将比基准值大的元素放到基准值的右边。
  3. 将基准值放到它最终的位置上,即左边的元素都比它小,右边的元素都比它大。
  4. 对基准值左边和右边的子列表,分别进行递归快速排序。

1.2.3 算法特性

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

  2. 时间复杂度:O(N*logN)(经过优化后),在最坏情况下,快速排序的时间复杂度为O(n^2),但是通过一些优化技巧,可以将其降低到O(nlogn)。快速排序是一种原地排序算法,即不需要额外的空间来存储排序结果。

  3. 空间复杂度:O(logN)

  4. 稳定性:不稳定

1.2.4 算法实现

在算法整体实现之前我们需要考虑一下快速排序算法的特殊性:

这个算法利用了分治的思想,理想的状态下,从中间分开,时间复杂度大大降低,如果每次选key都正好选到中间值,那么完全可以达到\log (n)

但是要是key选的不好,假如说选到最小值或最大值,那么这个排序的效率就会大打折扣,不如直接使用插入排序。

所以,不能简单的直接取左边的数或者右边的数,这样很容易造成key的选择不好,降低效率。

好用的方法有两个:

        1. 随机选key

                这种方式使用随机在数中选key的方式,来选择key,降低了选到最大或者最小值的概率。

srand((unsigned int)time(NULL));


void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

int randi = left + (rand() % (right - left));
swap(&a[left], &a[randi]);

        2.三数取中法

        这种方法使用的较为普遍,通过选择三个数中中间的一个数来定key,在数据没有大量重复的情况下,对提高算法效率有很大帮助。

这里其实留下了一个坑,当数据大量重复时,快速排序的效率会降到Nlog(N),如何解决这个问题,可以等等下一篇博客,会介绍三路划分优化快速排序算法。

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//三数取中,有序情况下最快
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] > a[left])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else //a[mid] <= a[left]
	{
		if (a[right] < a[mid])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

int midi = GetMidNumi(a, left, right);
Swap(&a[left], &a[midi]);

 1)hoare版本(左右指针法)

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//三数取中,有序情况下最快
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] > a[left])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else //a[mid] <= a[left]
	{
		if (a[right] < a[mid])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//hoare版本
int PartSort1(int* a, int left, int right)
{
	//优化:三数取中
	int midi = GetMidNumi(a, left, right);
	Swap(&a[left], &a[midi]);

	int keyi = left;
	//这地方left 不能先+1,否则最后一步交换会出问题
	while (left < right)
	{
		//右边先找小
		//注意内部没有判断left< right
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		//左边找大
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}
	//相遇位置一定比key小
	Swap(&a[keyi], &a[left]);
	keyi = left;

	return keyi;
}

2)挖坑版本

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//三数取中,有序情况下最快
int GetMidNumi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[mid] > a[left])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else //a[mid] <= a[left]
	{
		if (a[right] < a[mid])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}
//挖坑版
int PartSort2(int* a, int left, int right)
{
	//优化:三数取中
	int midi = GetMidNumi(a, left, right);
	Swap(&a[left], &a[midi]);

	int key = a[left];
	int pit = left;
	//这地方left 不能先+1,否则最后一步交换会出问题
	while (left < right)
	{
		//右边先找小
		//注意内部没有判断left< right
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[pit] = a[right];
		pit = right;
		//左边找大
		while (left < right && a[left] <= key)
		{
			left++;
		}

		a[pit] = a[left];
		pit = left;
	}
	//相遇位置一定比key小
	a[pit] = key;

	return pit;
}

3)前后指针版本

//前后指针法
int PartSort3(int* a, int left, int right)
{
	int midi = GetMidNumi(a, left, right);
	if (midi != left)
	{
		Swap(&a[left], &a[midi]);
	}

	int keyi = left;

	int prev = left;
	int cur = left + 1;
			
	while (cur <= right)
	{
		if (a[cur] < a[keyi] && ++prev != cur)
		{
			Swap(&a[prev], &a[cur]);
		}
		
		++cur;
	}
	//这里prev直接换就好了,不用交换
	Swap(&a[prev], &a[keyi]);
	keyi = prev;
	
	return keyi;
}

4)快速排序调用程序 

void QuickSort(int* a, int left, int right)
{
	//递归结束条件
	if (left >= right)
	{
		return;
	}

	//小区间优化 -- 小区间使用插入排序
	//这个数字不能太大,否则没有意义
	if ((right - left + 1) > 10)
	{
		int keyi = PartSort3(a, left, right);//只需改变1,2,3就可以改用三个快速排序版本

		//[begin, keyi-1] keyi [keyi+1, end]

		QuickSort(a, left, keyi - 1);
		QuickSort(a, keyi+1, right);
	}
	else
	{
		InsertSort(a + left, right - left +1);
	}
}

 优化:

在区间长度小于10时,改用插入排序,然后再递归,增加了算法效率。但是一定要注意区间长度一定要设置的恰到好处,否则区间过长就起不到相应的作用了。

1.2.5 非递归实现(重点)

非递归的实现一般有两种方式:   

  1. 1.直接改,适用于简单的递归,比如斐波那契数;
  2. 2.使用栈辅助改为非递归,适用于复杂的结构,利用栈先进先出的特点,对数据进行排序。
void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	STInit(&st);
    //入栈,先入后出,所以先入右边的值
	STPush(&st, right);
	STPush(&st, left);
    //栈非空时,进行操作
	while (!STEmpty(&st))
	{
        //出栈
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		int keyi = PartSort3(a, begin, end);
		if (end > keyi + 1)
		{
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
	STDestroy(&st);
}

二、归并排序

归并排序是一种分治算法,它将列表递归地拆分成两个子列表,然后将这两个子列表合并成一个有序的列表。

2.1 基本思想

它的基本思想是将待排序列表分解成若干个子列表,然后将这些子列表逐个合并,最终得到一个有序的列表。

动画演示:

2.2 具体步骤

  1. 将列表不断地二分,直到每个子列表只包含一个元素。
  2. 将两个有序的子列表合并成一个有序的列表。合并时,比较两个子列表的第一个元素,将较小的元素放到结果列表中,并将指针向后移动一位,依此类推,直到其中一个子列表为空。然后将另一个非空子列表的剩余部分直接放到结果列表中。
  3. 重复步骤2,直到所有的子列表都合并为一个有序的列表。

2.3 算法特性

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN),其中n是列表的长度。
3. 空间复杂度:归并排序是一种稳定的排序算法,它需要额外的空间来存储临时的结果,所以空间复杂度为O(n)。
4. 稳定性:稳定

2.4 算法实现

//归并排序,O(NlogN)
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//递归结束条件
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	//mid控制递归
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	//[begin, mid] [mid+1, end]归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	//i为每个栈帧中的独立变量,控制每一次的归并
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		//begin1<=begin2,可以做到稳定
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
		
	}
	//没排到的直接接到后面
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//覆盖到原数组
	memcpy(a+begin, tmp+begin, sizeof(int)*(end-begin+1));
}

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

	free(tmp);
	tmp = NULL;
}

 归并在实现时使用了递归所以一定要注意栈帧的开辟,防止爆栈。

事实上,归并的递归写法并不难,但是在大量数据归并时对栈帧的开销很大,很容易造成栈溢出,所以作为一名合格的程序员,一定要学会将递归改为非递归。

修正思路:

归并排序改非递归:数据多不好调试,可以打印出来看边界

复杂问题分解为简单问题:分类处理

        1.end1越界?不归并了

        2.end1没有越界,begin2越界了?跟1一样处理

        3.end1、begin没有越界,end2越界了? 继续归并,修正

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;//gap是归并过程中,每组数据的个数
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2*gap)
		{			
			//归并一次,拷贝一次(推荐)
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			//归并
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				//begin1<=begin2时可以做到稳定
				if (a[begin1] <= a[begin2])
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}
			//没排到的直接接到后面
			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
			//数据覆盖到原数组
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}

		gap *= 2;//gap控制遍历
	}
	free(tmp);
	tmp = NULL;
}

三、算法复杂度及稳定性分析

 

 总结

  • 冒泡排序是简单的排序算法,通过交换相邻元素使得较大(小)的元素逐渐向右(左)移动,时间复杂度为O(n^2)。
  • 快速排序是高效的排序算法,采用分治思想,通过选择基准元素将数组分割成左右子数组,进一步递归排序,平均时间复杂度为O(nlogn)。
  • 归并排序也是高效的排序算法,采用分治思想,将数组分割成左右子数组,递归排序后再合并,时间复杂度为O(nlogn)。

快速排序和归并排序相比,快速排序更常用,但快速排序在最坏情况下可能达到O(n^2)的时间复杂度。在实际应用中,需要根据具体情况选择合适的排序算法。

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

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

相关文章

Recyclerview Item 高度不同 统一最大高度

参考这篇&#xff1a; android - How to set recycler height to highest item in recyclerView? - Stack Overflowhttps://stackoverflow.com/a/67403957/13583023

解读三国历史中的配角们:探索未被书写的故事 - 《三国配角演义》读后感

在传统的三国叙事中&#xff0c;英雄主角们的事迹往往被无限放大&#xff0c;而那些默默无闻的小人物则被忽视。然而&#xff0c;《三国配角演义》通过挖掘历史细节&#xff0c;赋予这些小角色新的生命。书中用微小的史料合理推断&#xff0c;构建了他们不为人知的精彩故事。 …

嵌入式 24081开班典礼:与梦同行,同筑未来

2024 年 9 月 3 日&#xff0c;华清远见西安中心嵌入式 24081期开班典礼在班主任熊燕老师的主持中成功举行。此次开班典礼为学员们开启了嵌入式学习之旅的精彩序幕。 1.开班致辞 典礼伊始&#xff0c;校企合作经理针对行业现状深入分析了嵌入式前景&#xff0c;为学员们清晰地…

JVM合集

序言: 1.什么是JVM? JVM就是将javac编译后的.class字节码文件翻译为操作系统能执行的机器指令翻译过程: 前端编译:生成.class文件就是前端编译后端编译:通过jvm解释(或即时编译或AOT)执行.class文件时跨平台的,jvm并不是跨平台的通过javap进行反编译2.java文件是怎么变…

AI 与大模型:物流行业的变革力量

一、物流行业的现状与挑战 物流行业在现代经济中扮演着至关重要的角色&#xff0c;但目前也面临着诸多挑战。 在效率方面&#xff0c;交通拥堵是一个突出问题。许多城市道路容量不足&#xff0c;无法满足日益增长的货物运输需求&#xff0c;导致运输时间延长。例如&#xff0…

【H2O2|全栈】关于HTML(4)HTML基础(三)

HTML相关知识 目录 HTML相关知识 前言 准备工作 标签的具体分类&#xff08;三&#xff09; 本文中的标签在什么位置中使用&#xff1f; 列表 ​编辑​编辑 有序列表 无序列表 自定义列表 表格 拓展案例 预告和回顾 后话 前言 本系列博客将分享HTML相关知识点…

【NLP自然语言处理】文本的数据分析------迅速掌握常用的文本数据分析方法~

目录 &#x1f354; 文件数据分析介绍 &#x1f354; 数据集说明 &#x1f354; 获取标签数量分布 &#x1f354; 获取句子长度分布 &#x1f354; 获取正负样本长度散点分布 &#x1f354; 获取不同词汇总数统计 &#x1f354; 获取训练集高频形容词词云 &#x1f354;…

【docker】通过云服务器安转Docker

一、前言 这里关于Docker的安转&#xff0c;大家可以采用本地使用虚拟机来运行和安转Docker,我这里呢就采用云服务器来安装Docker,之所以用云服务器安转docker是因为比较简单&#xff0c;只是需要花一点money,而且自己没有用过云服务器所以这里就用这种方式来安转Docker了&…

Nginx跨域运行案例:云台控制http请求,通过 http server 代理转发功能,实现跨域运行。(基于大华摄像头WEB无插件开发包)

文章目录 引言I 跨域运行案例开发资源测试/生产环境,Nginx代理转发,实现跨域运行本机开发运行II nginx的location指令Nginx配置中, 获取自定义请求header头Nginx 配置中,获取URL参数引言 背景:全景监控 需求:感知站点由于云台相关操作为 http 请求,http 请求受浏览器…

【Canvas与钟表】“社会主义核心价值观“表盘

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>387.干支表盘</title><style type"text/css">…

最大时间

题目描述 给定一个数组&#xff0c;里面有6个整数&#xff0c;求这个数组能够表示的最大 24 进制的时间是多少&#xff0c;输出这个时间&#xff0c;无法表示输出 invalid. 输入描述 输入为一个整数数组&#xff0c;数组内有六个整数。 输入整数数组长度为6&#xff0c;不需…

火语言RPA流程组件介绍--浏览网页

&#x1f6a9;【组件功能】&#xff1a;浏览器打开指定网址或本地html文件 配置预览 配置说明 网址URL 支持T或# 默认FLOW输入项 输入需要打开的网址URL 超时时间 支持T或# 打开网页超时时间 执行后后等待时间(ms) 支持T或# 当前组件执行完成后继续等待的时间 UserAgen…

微积分-积分应用5.4(功)

术语“功”在日常语言中用来表示完成一项任务所需的总努力量。在物理学中&#xff0c;它有一个依赖于“力”概念的技术含义。直观上&#xff0c;你可以将力理解为对物体的推或拉——例如&#xff0c;一个书本在桌面上的水平推动&#xff0c;或者地球对球的向下拉力。一般来说&a…

【iOS】——渲染原理与离屏渲染

图像渲染流水线&#xff08;图像渲染流程&#xff09; 图像渲染流程大致分为四个部分&#xff1a; Application 应用处理阶段&#xff1a;得到图元Geometry 几何处理阶段&#xff1a;处理图元Rasterization 光栅化阶段&#xff1a;图元转换为像素Pixel 像素处理阶段&#xff1…

图像去噪算法性能比较与分析

在数字图像处理领域&#xff0c;去噪是一个重要且常见的任务。本文将介绍一种实验&#xff0c;通过MATLAB实现多种去噪算法&#xff0c;并比较它们的性能。实验中使用了包括中值滤波&#xff08;MF&#xff09;、自适应加权中值滤波&#xff08;ACWMF&#xff09;、差分同态算法…

Clion不识别C代码或者无法跳转C语言项目怎么办?

如果是中文会显示&#xff1a; 此时只需要右击项目&#xff0c;或者你的源代码目录&#xff0c;将这个项目或者源码目录标记为项目源和头文件即可。 英文如下&#xff1a;

什么是数字化人才?数字化人才画像是怎么样的?(附数字化知识能力框架体系)

什么是数字化人才&#xff1f; 数字化人才是指具备较高信息素养&#xff0c;有效掌握数字化相关能力&#xff0c;并将这种能力不可或缺地应用于工作场景的相关人才。随着数字技术的快速发展和应用&#xff0c;数字化人才的需求日益增加&#xff0c;他们在大数据、“互联网”、…

编程效率飙升的秘密武器:Cursor编辑器的AI革命

有没有想过,写代码这件事其实可以更加轻松、高效?尤其是对于那些需要频繁修正、调试和优化的开发者们,Cursor编辑器带来的AI赋能,简直让人眼前一亮。相信很多人一提到AI,第一反应就是:“这真的靠谱吗?”今天,我就带你来揭开Cursor这款AI编辑器的神秘面纱,看看它是如何…

借助ChatGPT高效撰写优质论文的7大要素

大家好,感谢关注。我是七哥,一个在高校里不务正业,折腾学术科研AI实操的学术人。关于使用ChatGPT等AI学术科研的相关问题可以和作者七哥(yida985)交流,多多交流,相互成就,共同进步,为大家带来最酷最有效的智能AI学术科研写作攻略。 撰写一篇优秀的学术论文不仅需要深入…

onnxruntime——CUDA Execution Provider学习记录

ONNX Runtime&#xff08;简称 ORT&#xff09;是一个高性能的推理引擎&#xff0c;支持多种硬件加速器。CUDA Execution Provider 是 ONNX Runtime 提供的一个执行提供者&#xff08;Execution Provider&#xff09;&#xff0c;专门用于在 NVIDIA GPU 上加速推理。以下是详细…