【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

news2024/11/26 4:26:04

 前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值

基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。

快速排序实现主框架:

//快速排序 
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
    //keyi即是基准值
	int keyi = _QuickSort1(arr, left, right);//实现找基准值的方法

	QuickSort(arr, left, keyi - 1);

	QuickSort(arr, keyi + 1, right);

}

快速排序找基准值三种方法

Swap方法的实现 ,即交换两个数的值

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

hoare版本

算法思路:

1)创建左右指针,确定基准值

2)从右向左找出比基准值小的数据,从左向右找比基准值大的数据,左右指针数据交换,进入下次循环

问题1:为什么跳出循环后right位置的值⼀定不大于key? 

当 left > right 时,即right⾛到left的左侧,而left扫描过的数据均不大于key,因此right此时指向的数据⼀定不大于key

问题2:为什么left 和 right指定的数据和key值相等时也要交换?

 相等的值参与交换确实有⼀些额外消耗。实际还有各种复杂的场景,假设数组中的数据⼤量   重复时, 无法进行有效的分割排序。

int _QuickSort1(int* arr, int left, int right)
{
	int keyi = left;
	++left;
	while (left <= right)
	{
		while (left <= right && arr[right] > arr[keyi])
		{
			right--;
		}
		while (left <= right && arr[left] < arr[keyi])
		{
			left++;
		}
		if (left <= right)
		{
			Swap(&arr[left++], &arr[right--]);
		}
	}
	Swap(&arr[keyi], &arr[right]);

	return right;
}

挖坑法

思路: 创建左右指针。首先从右向左找出比基准小的数据,找到后立即放入左边坑中,当前位置变为新的"坑",然后从左向右找出比基准大的数据,找到后立即放入右边坑中,当前位置变为新的"坑",结束循环后将最开始存储的分界值放入当前的"坑"中,返回当前"坑"下标(即分界值下标)

int _QuickSort2(int* arr, int left, int right)
{
	int hole = left;//坑
	int key = arr[hole];//坑位数据

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

		while (left < right && arr[left] < key)
		{
			++left;
		}
		arr[hole] = arr[left];
		hole = left;
	}
	arr[hole] = arr[left];
	return hole;
}

lomuto前后指针

创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。

int _QuickSort3(int* arr, int left, int right)
{
	int prev = left, cur = left + 1;
	int keyi = left;
	while (cur <= right)
	{
		if (arr[cur] < arr[keyi] && ++prev != cur)
		{
			Swap(&arr[cur], &arr[prev]);
		}
		++cur;
	}
	Swap(&arr[keyi], &arr[prev]);
	return prev;
}

以上就是快速排序当中查找基准值的三种方法 

 快速排序特性总结:

1. 时间复杂度:O(nlogn) 

2. 空间复杂度:O(logn)

快速排序非递归版本,借助数据结构栈 

void QuickSortNonR(int* arr, 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);
		//找基准值[begin,end]

		int prev = begin;
		int cur = begin + 1;
		int keyi = begin;

		while (cur <= end)
		{
			if (arr[cur] < arr[keyi] && ++prev != cur)
			{
				Swap(&arr[cur], &arr[prev]);
			}
			cur++;
		}
		Swap(&arr[keyi], &arr[prev]);

		keyi = prev;
		//根据基准值划分左右区间
		//左区间:[begin,keyi-1]
		//右区间:[keyi+1,end]

		if (keyi + 1 < end)
		{
			STPush(&st, end);
			STPush(&st,keyi + 1);
		}
		if (keyi - 1 > begin)
		{
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}

	STDestroy(&st);//销毁
}

冒泡排序 

冒泡排序核心思想是通过反复遍历待排序的序列,比较相邻的元素并交换它们的位置,使得每一趟遍历后,最大的元素逐渐"冒泡"到序列的末尾。

核心步骤如下:

  1. 比较相邻的元素:从第一个元素开始,依次比较相邻的两个元素。

    • 如果前一个元素大于后一个元素,则交换它们的位置。
  2. 继续遍历序列:一趟遍历后,最大的元素会被“冒泡”到序列的末尾。

  3. 重复遍历:从头开始再进行遍历,对剩下的元素重复比较和交换操作,直到所有元素都按顺序排列。

  4. 优化(可选):如果在某一趟遍历中没有发生任何交换,说明序列已经有序,可以提前终止排序。

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

 其时间复杂度为 O(n²),由于每次都要遍历未排序的部分,并且重复多次比较操作,因此效率较低。

直接插入排序 

直接插入排序(Direct Insertion Sort)的原理是通过逐步将待排序数组中的元素插入到已排序部分的正确位置,最终实现排序。它是一种简单且直观的排序算法,尤其适用于小规模或近乎有序的数据集。

原理步骤:

  1. 初始化已排序序列:假设数组的第一个元素已经是有序的,直接跳过。

  2. 逐个插入元素:从第二个元素开始,逐个将每个元素插入到前面已经排好序的部分中。

    • 将当前元素与前面已经排序好的元素依次比较,找到它的正确位置。
    • 如果当前元素比已排序部分的某个元素小,则将已排序部分的元素向后移动,空出位置给当前元素。
  3. 插入完成:重复这个过程,直到所有元素都被插入正确位置,排序完成。

举例说明:

假设有一个数组 [5, 2, 9, 1, 5, 6] 需要排序,步骤如下:

  • 初始数组:[5, 2, 9, 1, 5, 6]
  • 第一轮(从第二个元素开始,即 2):比较 2 和 5,2 小于 5,将 5 向后移,插入 2。得到:[2, 5, 9, 1, 5, 6]
  • 第二轮(9):9 比 5 大,直接进入下一个。得到:[2, 5, 9, 1, 5, 6]
  • 第三轮(1):1 小于 9、5 和 2,将它们都向后移,插入 1。得到:[1, 2, 5, 9, 5, 6]
  • 第四轮(5):5 小于 9,移位插入 5。得到:[1, 2, 5, 5, 9, 6]
  • 第五轮(6):6 小于 9,移位插入 6。得到:[1, 2, 5, 5, 6, 9]
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)
		{
			if (arr[end] > tmp)
			{
				arr[end + 1] = arr[end];
				end--;
			}
			else
			{
				break;
			}
		}
		arr[end + 1] = tmp;
	}
}

希尔排序 

希尔排序(Shell Sort)是插入排序的改进版,它通过比较和交换不相邻的元素来减少数据的移动次数,以加速排序过程。希尔排序的核心思想是将待排序的序列按一定间隔分组,分别对每一组进行插入排序,逐步缩小间隔,直到间隔为 1 时,完成最终的插入排序。

原理步骤:

  1. 确定间隔序列:首先选择一个间隔(也称“增量”),将整个序列按照这个间隔进行分组。例如,间隔为 5 时,第 1 个元素与第 6 个元素、第二个元素与第 7 个元素形成一组,依次类推。

  2. 组内排序:对每一组进行插入排序。在每个组内的元素之间的距离由间隔确定,插入排序的操作类似于直接插入排序,但由于间隔较大,能使元素迅速向正确的位置移动,减少了总的移动次数。

  3. 缩小间隔:缩小间隔并重复步骤 2。常见的缩小方式是将间隔减半,直到间隔为 1。

  4. 完成排序:当间隔为 1 时,整个序列已经近乎有序,最后一次插入排序将序列排好。

举例说明:

假设对数组 [23, 29, 15, 19, 31, 7, 9, 5, 2] 进行希尔排序,初始数组如下:

  • 原数组:[23, 29, 15, 19, 31, 7, 9, 5, 2]

第一步:假设初始间隔为 4,将数组分组进行插入排序:

  • 对于第 1, 5, 9 号元素:[23, 31, 2] 进行排序,结果为 [2, 23, 31]。
  • 对于第 2, 6 号元素:[29, 7] 进行排序,结果为 [7, 29]。
  • 对于第 3, 7 号元素:[15, 9] 进行排序,结果为 [9, 15]。
  • 对于第 4, 8 号元素:[19, 5] 进行排序,结果为 [5, 19]。

结果数组:[2, 7, 9, 5, 23, 29, 15, 19, 31]

第二步:缩小间隔到 2,继续分组排序:

  • 对于第 1, 3, 5, 7 号元素:[2, 9, 23, 15] 进行排序,结果为 [2, 9, 15, 23]。
  • 对于第 2, 4, 6, 8 号元素:[7, 5, 29, 19] 进行排序,结果为 [5, 7, 19, 29]。

结果数组:[2, 5, 9, 7, 15, 19, 23, 29, 31]

第三步:间隔为 1 时,进行最后一次插入排序,得到最终排序结果:[2, 5, 7, 9, 15, 19, 23, 29, 31]

void ShellSort(int* arr, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (arr[end] > tmp)
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = tmp;

		}
	}
}

归并排序

归并排序算法思想: 归并排序(MERGE-SORT)是建立在归并操作上的⼀种有效的排序算法,该算法是采⽤分治法(Divide andConquer)的⼀个非常典型的应⽤。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成⼀个有序表,称为二路归并。归并排序核心步骤:

void _MergeSort(int* arr, int left, int right, int* tmp)
{
	//分开
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) / 2;

	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid + 1, right, tmp);

	//合并
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = begin1;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		}
	}

	//要么begin1越界,要么begin2越界
	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = arr[begin2++];
	}

	//将tmp数据拷贝回arr当中
	for (int i = left; i <= right; i++)
	{
		arr[i] = tmp[i];
	}
}

void MergeSort(int* arr, int n) 
{
	int* tmp = (int*)malloc(sizeof(int) * n);

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

	free(tmp);
}

1. 时间复杂度: O(nlogn)

2. 空间复杂度: O(n)

计数排序

计数排序⼜称为鸽巢原理,是对哈希直接定址法的变形应⽤。

操作步骤:

1)统计相同元素出现次数

2)根据统计的结果将序列回收到原来的序列中

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

	for (int i = 1; i < n; i++)
	{
		if (arr[i] > max)
		{
			max = arr[i];
		}
		if(arr[i] < min)
		{
			min = arr[i];
		}
	}
	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);

	if (count == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	//初始化range中的数据为0
	memset(count, 0, range * sizeof(int));

	//统计数组中每个数据出现的个数
	for (int i = 0; i < n; i++)
	{
		count[arr[i] - min]++;
	}
	int index = 0;
	//取count中的数据往arr中放
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			arr[index++] = i + min;
		}
	}
}

这五种排序的优缺点

1. 快速排序:

  • 优点:
    • 平均时间复杂度为 O(nlog⁡n)O(n \log n)O(nlogn),非常高效。
    • 原地排序,不需要额外的存储空间(递归调用栈除外)。
    • 在实际应用中表现出色,尤其对大数据集。
  • 缺点:
    • 最坏情况下时间复杂度为 O(n2)O(n^2)O(n2),特别是在数据有序时。
    • 不稳定,可能改变相同元素的相对顺序。
    • 对递归栈空间有要求,可能导致栈溢出。

2. 希尔排序:

  • 优点:
    • 改进了插入排序,通过使用间隔的分组排序减少了移动次数。
    • 时间复杂度一般为 O(n1.3−n2)O(n^{1.3} - n^{2})O(n1.3−n2),对于中等规模的数据表现良好。
    • 原地排序,不需要额外空间。
  • 缺点:
    • 不是稳定排序,相同元素可能打乱顺序。
    • 性能依赖于选取的增量序列,难以分析其最优时间复杂度。
    • 实现相对复杂。

3. 直接插入排序:

  • 优点:
    • 实现简单,适合少量元素时的排序。
    • 稳定排序,保持相同元素的相对顺序。
    • 对于几乎有序的数组非常高效(接近 O(n)O(n)O(n))。
  • 缺点:
    • 时间复杂度为 O(n2)O(n^2)O(n2),对大规模数据效率低。
    • 需要频繁的元素移动,尤其是当数据无序时。

4. 归并排序:

  • 优点:
    • 时间复杂度为 O(nlog⁡n)O(n \log n)O(nlogn),在最坏情况下仍能保持高效。
    • 稳定排序,保持相同元素的相对顺序。
    • 可用于链表等不连续存储的数据结构。
    • 非原地排序,但可以用外部排序实现超大数据集的排序。
  • 缺点:
    • 需要额外的空间 O(n)O(n)O(n),对于内存敏感的应用不是很理想。
    • 实现相对复杂。

5. 计数排序:

  • 优点:
    • 时间复杂度为 O(n+k)O(n+k)O(n+k),对数据范围 kkk 相对较小的整数数据集非常高效。
    • 稳定排序,保持相同元素的相对顺序。
    • 不涉及比较,适用于一些特殊的场景,如成绩排名等。
  • 缺点:
    • 需要额外的存储空间 O(k)O(k)O(k),当数据范围大时,空间消耗可能过高。
    • 只能处理整数或离散类型数据,无法处理浮点数或复杂类型数据。
    • 对于数据范围远大于数据量的情况,效率不高。

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

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

相关文章

滑动窗口——优选算法

个人主页&#xff1a;敲上瘾-CSDN博客 个人专栏&#xff1a;游戏、数据结构、c语言基础、c学习、算法 目录 一.滑动窗口算法原理&#xff1a; 二.无重复字符的最长子串 1.题目解析​编辑 2.算法原理 3.代码编写 三.长度最小的子数组 1.题目解析 2.算法原理 3.代码编…

小米红米系列机型 机型代码查询总目录 adb指令查询步骤

小米机型型号与代码 小米系列机型 型号众多。有时候我们在刷机或者下载固件的时候对一些 同型号分版本的机型不太注意下错固件刷机会导致系统故障。手机设备代码虽然在一般情况下用处不大&#xff0c;不过真正到你需要它的时候&#xff0c;又苦于不知道它是什么&#xff0c;以…

Acrobat Pro DC 2023 for Mac/Win:全能型PDF编辑器深度解析

Adobe Acrobat Pro DC 2023作为一款跨平台的PDF编辑器&#xff0c;无论是对于Mac还是Windows用户&#xff0c;都提供了极为全面且强大的PDF处理功能。该软件凭借其卓越的性能和丰富的特性&#xff0c;成为了全球范围内用户处理PDF文档的首选工具。 一、强大的编辑功能 Acroba…

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题&#xff0c;本文将一起从代码层去分析为什么没有建立索引&#xff1f; 开源ERP项目地址&#xff1a;…

Windows系统引入全新 Android 体验?快来尝鲜!

听说微软 Windows 11 操作系统引入全新体验 &#xff1a;实时访问 Android 设备图片。 意思就是在Android 设备上捕获了新照片或屏幕截图时&#xff0c;Windows 上立刻收到通知&#xff0c;且可以不用插数据线就能访问。 用Windows连接手机的功能其实早在Windows10就已经有的了…

【进阶篇】应届毕业生必备:机器学习面试题指南【2】

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发…

总线概述

CPU能通过地址总线给主存、硬盘、打印机通过地址总线发送地址&#xff0c;CPU可以通过数据总线和其他的部件进行信息传输&#xff0c;地址总线和数据总线可以并行传输很多位信息&#xff0c;为什么呢&#xff1f;因为每个总线可能由很多跟信号线组成的。CPU可以通过控制总线给其…

锐捷交换机常用命令

文章目录 1. 基本操作命令2. 接口配置3. VLAN配置4. 链路聚合5. 生成树协议6. 端口安全7. 常用查看命令8. 系统管理9. 配置端口镜像10. 配置生成树协议 1. 基本操作命令 进入特权模式&#xff1a;enable 进入全局配置模式&#xff1a;configure terminal 保存配置&#xff1a;…

在线plotly绘制动态旭日图,展示复杂数据层次结构

探索数据的层次之美&#xff1a;旭日图&#xff0c;以环环相扣的视觉效果&#xff0c;清晰展现数据的层级关系。搭配Plotly的动态可视化技术&#xff0c;不仅让数据层次一目了然&#xff0c;更通过交互式操作&#xff0c;让用户轻松探索每个层级的详细信息&#xff0c;享受数据…

国内web组态推荐

万维组态是一款功能强大的基于Web的可视化组态编辑器&#xff0c;采用标准HTML5技术&#xff0c;基于B/S架构进行开发&#xff0c;支持WEB端呈现&#xff1b; 支持在浏览器端完成便捷的人机交互&#xff0c;简单的拖拽即可完成可视化页面的设计;可快速构建和部署可扩展的SCADA…

关于百度翻译以及这三款好用的翻译推荐!!

今天咱来聊聊在线翻译工具&#xff0c;尤其是百度翻译&#xff0c;以及我超爱的其他几款翻译工具。如果你跟我一样&#xff0c;经常要处理多语言文件&#xff0c;或者想快速了解外国文化&#xff0c;那么这些工具绝对是你的好帮手&#xff1a; 关于百度翻译 先说说我日常用的…

低温烧结银AS9378火爆的六大原因

低温烧结银AS9378火爆的六大原因 低温烧结银AS9378近年来在电子材料领域迅速崛起&#xff0c;其火爆程度令人瞩目。这款采用纳米技术和低温烧结工艺的高性能材料&#xff0c;凭借其独特的优势在众多应用中脱颖而出。以下&#xff0c;我们将深入探讨低温烧结银AS9378火爆的六大原…

纷享销客生态伙伴大会北京站成功举办,共谋数智新未来

9月5日&#xff0c;主题为“智享未来 领创CRM新纪元”的纷享销客生态伙伴大会北京站圆满落幕&#xff0c;此次盛会吸引了超过600位来自不同行业的精英代表、企业领袖、技术专家等汇聚一堂&#xff0c;共同探讨CRM领域的最新趋势、创新实践与未来机遇。 01、智享未来&#xff0…

idea修改内存设置后,启动没反应 Error opening zip file or JAR manifest missing :

Error opening zip file or JAR manifest missing :一个路径 解决办法 删除环境变量中的路径 重装idea

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始&#xff0c;按照书籍的划分&#xff0c;第10章开始就进入保护模式&#xff08;Protected Mode&#xff09;部分了&#xff0c;感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断&#xff08;Interrupt&#xff09;的设计&#…

idea开发Java程序的步骤及设置

project中可以创建多个module&#xff0c;module中可以创建多个package。package中可以创建多个class。 idea中的Java程序是自动编译和执行的&#xff0c;编译后的class文件在工程路径下的一个out文件夹里。 IDEA中设置主题、字体 IDEA常用快捷键

Ubuntu20如何设置网络

如图设置静态地址 第1步&#xff1a; 查看当前主机的网卡名&#xff0c;当前ip, 子网掩码&#xff0c;网关地址 ifconfig route -n 如果ifconfig命令无法使用, 请运行以下命令安装net-tools sudo apt update -y sudo apt install net-tools -y 如上图所示&#xff1a;网卡名为 …

Cesium 展示——实现雾的天气效果模拟

文章目录 需求分析1. 添加2. 移除需求 Cesium 完成雾的天气效果模拟 分析 1. 添加 源码case

C语言-qosrt函数—秩序大师

1、qsort()的作用 在我们的日常生活中&#xff0c;排序无处不在。想象一下&#xff0c;当你整理书架时&#xff0c;会按照书籍的类别、作者或者大小进行排列&#xff0c;让你的阅读空间更加整洁有序。又比如&#xff0c;在超市的货架上&#xff0c;商品通常也是按照一定的规则进…

启动与登录Mysql

1.启动与停止MYSQL服务 启动MySQL 服务的命令 以管理员身份打开Windows 的命令行窗口&#xff0c;在命令提示符后输入以下命令启动MySQL 服务&#xff1a; net start[ 服务名称] 也可以直接输入以下命令&#xff1a; net start 按【Enter】键执行该命令&#xff0c;默认启…