排序算法(详解)

news2024/11/16 10:32:01

排序在日常生活中十分重要,购物平台上商品的排序,各国高校等级的排序......可以说,现代生活中已经离不开排序了;因此学好排序算法至关重要,本篇文章就来讲讲常见的排序算法

排序的种类非常多,按照种类划分,有插入排序,选择排序,交换排序......,而每种排序中又分多种排序,下图是常见的排序算法

1.插入排序

1.1直接插入排序

算法思想:

假设数组中一个区间[0,end]中的数据有序了,插入end+1位置的数据,如何保持数据依然有序?

  • 将end+1位置的数据从后往前,依次与前面的数据比较,如果小于比较的数据,则将比较过的数据往后挪,直到找到小于它的数据或者找到头了;再在停下来的下一个位置插入数据
        //单趟排序
		int i = 0;
		int end;
		int tmp = a[end + 1];
		for (i = end + 1; i > 0; i--)
		{
			if (tmp < a[i - 1])
				a[i] = a[i - 1];
			else
				break;
		}
		a[i] = tmp;

注意:以后我们写有多趟逻辑的代码时,建议先写出单趟的逻辑,再加上整体的逻辑

上面是单趟排序,整体的排序,相当于依次对[0,0],[0,1]......,[0,n-1]每个区间都进行一次单趟排序

void InsertSort(int* a, int n)
{
	for (int j = 0; j < n - 1; j++)
	{
		//单趟排序
		int i = 0;
		int end = j;
		int tmp = a[end + 1];
		for (i = end + 1; i > 0; i--)
		{
			if (tmp < a[i - 1])
				a[i] = a[i - 1];
			else
				break;
		}
		a[i] = tmp;
	}
}

复杂度分析:

  • 最好的情况:原数据有序或接近于有序,时间复杂度为O(N)
    最坏的情况:元数据无序或接近于无序,时间复杂度为O(N^{2} )
  • 空间复杂度:O(1)

1.2希尔排序

直接在数据有序或接近于有序的情况下效率是非常高的;但我们是不知道数据到底是怎么排序的,那能不能让数据先变成有序或接近于有序,再使用直接插入排序?这就是希尔排序的核心思想

算法思想:

希尔排序中,定义了一个间距gap,假设一开始gap为3,从第一个数据开始,将间距为gap的分为一组,一共有gap组

对每组分别使用直接插入排序,将每组排成有序,这样整体接近有序,再降低gap的值,重复操作,让数据更接近于有序,直到最后一次gap为1,此时就相当于直接插入排序了

首先是排一组中单趟的数据:

int i = 0;
int end;
int tmp = a[end + gap];
for (i = end + gap; i > gap - 1; i -= gap)
{
	if (tmp < a[i - gap])
		a[i] = a[i - gap];
	else
		break;
}
a[i] = tmp;

再将一组排好

for (int j = 0; j < n - gap; j += gap)
{
	int i = 0;
	int end = j;
	int tmp = a[end + gap];
	for (i = end + gap; i > gap - 1; i -= gap)
	{
		if (tmp < a[i - gap])
			a[i] = a[i - gap];
		else
			break;
	}
	a[i] = tmp;
}

j<n-gap的原因同样是防止越界

排完第一组还要排后面的组,因此以gap为3的整体代码如下

	int gap = 3;

	for (int z = 0; z < gap; z++)
	{
		for (int j = z; j < n - gap; j += gap)
		{
			int i = 0;
			int end = j;
			int tmp = a[end + gap];
			for (i = end + gap; i > gap - 1; i -= gap)
			{
				if (tmp < a[i - gap])
					a[i] = a[i - gap];
				else
					break;
			}
			a[i] = tmp;
		}
	}

这个代码套了三层循环,其实可以优化一下

	int gap = 3;

	for (int j = 0; j < n - gap; j++)
	{
		int i = 0;
		int end = j;
		int tmp = a[end + gap];
		for (i = end + gap; i > gap - 1; i -= gap)
		{
			if (tmp < a[i - gap])
				a[i] = a[i - gap];
			else
				break;
		}
		a[i] = tmp;
	}

如何理解呢?该代码是先将每组的前两个数据排好,再排每组的前三个数据......直到排好每组的最后一个数据

现在,我们gap组都排好了,需要减小gap的值,重复操作,并且保证最后一次排序gap为1

void ShellSort(int* a, int n)
{
	int gap = n;

	while (gap > 0)
	{
		gap = gap / 2;

		for (int j = 0; j < n - gap; j++)
		{
			int i = 0;
			int end = j;
			int tmp = a[end + gap];
			for (i = end + gap; i > gap - 1; i -= gap)
			{
				if (tmp < a[i - gap])
					a[i] = a[i - gap];
				else
					break;
			}
			a[i] = tmp;
		}
	}
}

怎么来确定gap的值呢?

我们发现,gap的值越大,大的数据跳到后面越快,小的数据跳到前面越快;gap越小,大的数据跳到后面越慢,小的数据跳到前面越慢

怎么取gap的值才最合适呢?其实也没有一个标准的说法,最关键的是你得保证最后一次排序gap的值为1

复杂度分析:

时间复杂度:O(N^{1.25})\sim O(1.6*N^{1.25} )

空间复杂度:O(1)


2.选择排序

2.1选择排序

算法思想:

遍历数据,选出最大的和最小的,换到尾和头,再选出次大的和次小的......

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

void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;

	while (begin < end)
	{
		//单趟排序
		int mini = begin;
		int maxi = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[i] < a[mini])
				mini = i;
			if(a[i] > a[maxi])
				maxi = i;
		}
		Swap(&a[begin], &a[mini]);
		if (maxi == begin)
			maxi = mini;
		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}

复杂度分析:

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

2.2堆排序

堆排序的讲解在这篇文章中堆排序(详解)-CSDN博客

这里就不再花时间细说了

复杂度分析:

  • 时间复杂度:O(N*\log_{2}{N} )
  • 空间复杂度:O(1)

3.交换排序

3.1冒泡排序

算法思想:

每一趟将一个数放到它应该在的位置,N个数需要进行N-1趟;由于该排序较简单,这里也不细讲了

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

复杂度分析:

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

3.2快速排序

算法思想:

如果一个数的左边的数都比它小,右边的数都比它大,那么这个数是不是就在它应该在的位置;快速排序的单趟排序就是将一个数变成具有上述性质;再去递归该数的左区间和右区间

快速排序的单趟排序有三种版本

第一种:hoare版

  • 随便选择一个数作为要调的整数,记为key;right从右往左找比key小的数,left从左往右找比key大的数,一旦找到就停下来,交换left和right位置的数,直到left和right相遇
    //单趟排序
	int left = begin;
	int right = end;
	int keyi = left;//keyi是key的下标
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;
  • 为什么判断大小时要加等号?
    我们一开始的key在left的位置,如果不加等于,第一次交换会改变key的值
  • 为什么判断的时候要left<right?
    在找的过程中可能left超过了right,此时应当终止循环
  • 为什么是右边先走?
    右边先走停下来的情况有两种:1)遇到比key小的数;2)和begin相等了
    由于right先走了,所以下次right走时,left位置的数一定是比key小的
    也就是说right停下来的位置的数一定是比key要小的,交换key和left与right相遇的位置,key左边就都比它小,右边都比它大了
    如果是left先走,left和right相遇时,相遇的位置的数可能比key大,此时和key交换的话,就不符合我们的要求了,因此右边需要先走

此时数据被分成了三个区间[begin,keyi-1]keyi[keyi+1,end],我们再对[begin,keyi-1]和[keyi,end]区间递归,如果begin>=end就直接返回

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	int left = begin;
	int right = end;
	int keyi = left;//keyi是key的下标
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}
		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}
	Swap(&a[keyi], &a[left]);
	keyi = left;

	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);
}

第二种:挖坑法

将随便一个位置的数记录为key,这里就选开始位置的数,作为一个坑位;同样的右边先走,找比key小的数,不同的是,这时找到了就把那个数放到刚才的坑位,留下了另一个坑位;再左边找比key大的数,找到了交换到上一个坑位,留下一个坑位......直到left和right相遇,将key给到上一个坑位

int PartSort2(int* a, int begin, int end)
{
	int left = begin;
	int right = end;
	int keyi = left;
	int key = a[left];
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}
		a[left] = a[right];
		while (left < right && a[left] <= key)
		{
			left++;
		}
		a[right] = a[left];
	}
	a[left] = key;
	keyi = left;
	
	return keyi;
}

第三种:前后指针法

定义两个指针prev和cur和key;cur去遍历数据,如果cur位置的数大于key,cur++;如果cur位置的数小于key,prev++之后交换perv和cur位置的数,直到cur走到尾;再交换perv和key的值

该方法的本质是让prev和cur错开,让它们之间的数都是比key大的数,再将比key小的数与prev和cur中间的数交换,相当于把中间的数往后挪

int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;
	int key = a[begin];

	while (cur <= end)
	{
		if (a[cur] < key)
		{
			prev++;
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}

	Swap(&a[keyi], &a[prev]);
	keyi = prev;

	return prev;
}

3.2.1快排的优化

优化一:三数取中

由于快排是递归进行排序的,如果每次key是中间数,那么需要递归的层数是\log_{2}{N}层;如果数据有序或接近于有序,递归的层数就会接近N层,效率大大降低,还可能会导致栈溢出,因此我们添加一个三数取中算法,确保每次key取到的不是最大或最小数

int GetMidi(int* a, int begin, int end)
{
	int midi = (begin + end) / 2;

	if (a[begin] < a[end])
	{
		if (a[midi] < a[begin])
			return begin;
		else if (a[end] < a[midi])
			return end;
		else
			return midi;
	}
	else
	{
		if (a[midi] < a[end])
			return end;
		else if (a[begin] < a[midi])
			return begin;
		else
			return midi;
	}
}

优化二:部分递归换直接插入排序

如果只有10个数,用递归去排序是不是显得很繁琐,因为递归还要建立栈帧,这时可以考虑用其他排序,我们选择了直接插入排序

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)
		return;

	if (end - begin + 1 <= 10)
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		//int keyi = PartSort1(a, begin, end);
		//int keyi = PartSort2(a, begin, end);
		int keyi = PartSort3(a, begin, end);

		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

复杂度分析:

  • 时间复杂度:O(N*\log_{2}{N})
  • 空间复杂度:O(1)

3.3非递归的快速排序

非递归的快排本质是将要排序的区间存到一个栈中,选一种单趟排序,排完后数据分成了三部分,将右区间和左区间入栈,进行排序,直到栈为空

void QuickSortNonR(int* a, int begin, int end)
{
	Stack st;
	StackInit(&st);

	StackPush(&st, end);
	StackPush(&st, begin);

	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);
		int right = StackTop(&st);
		StackPop(&st);

		int keyi = PartSort3(a, left, right);
		//[left,keyi-1]keyi[keyi+1,right]
		if (keyi + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyi + 1);
		}
		if (left < keyi - 1)
		{
			StackPush(&st, keyi - 1);
			StackPush(&st, left);
		}
	}

	StackDestroy(&st);
}

4.归并排序

算法思想:

如果一串数据中左边一部分有序了,右边一部分也有序了,那么把整体弄成有序?这就是合并两个有序数组的问题了

那怎么让左边和右边有序呢?将左边数据也弄成两部分,只要这两部分有序,再对整体使用合并算法,整体就有序了,右边也是同理

像这样一直分,直到两部分都只有一个数据,此时每部分相当于有序,合并后返回;也就是说,归并排序也是用递归来实现的

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
		return;

	int midi = (begin + end) / 2;

	_MergeSort(a, tmp, begin, midi);
	_MergeSort(a, tmp, midi + 1, end);
	//归并
	int begin1 = begin, begin2 = midi + 1;
	int end1 = midi, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		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("MergeSort:malloc fail");
		exit(-1);
	}

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

复杂度分析:

  • 时间复杂度:O(N*\log_{2}{N} )
  • 空间复杂度:O(N)

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

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

相关文章

AI数字人克隆采集规范分享!

数字人直播的时代已经来临&#xff0c;使用青否数字人SaaS系统数字人源码&#xff1a;zhibo175&#xff09;去生成数字人&#xff0c;那如何能得到自己想要的效果呢&#xff1f;需要注意一下几点&#xff1a; 一.摄影棚灯光方案 中型(15m左右)摄影棚​ 适用于美妆/珠宝等直播&a…

Rust语言抓取在线考试平台的专业试题数据

不管你是学车也好&#xff0c;还是考各类证书&#xff0c;都离不开刷题&#xff0c;有些题库都是需要收费的&#xff0c;而且市面平台那么多&#xff0c;想要刷更多的题只能下载很多不同APP&#xff0c;因此&#xff0c;我写了一个Rust爬取试题的爬虫&#xff0c;将更多的分散的…

从 enable_if 了解模板元编程

前言 在阅读学习 ZLToolKit 源码时&#xff0c;从如下一段代码中了解到 enable_if 和 SFINAE 的概念&#xff0c;从而引入了对模板元编程的了解。 template<class R, class... ArgTypes> class TaskCancelableImp<R(ArgTypes...)> : public TaskCancelable { pub…

PHP基础 - 循环与条件语句

循环语句 1)for循环: 重复执行一个代码块指定的次数。 for ($i = 0; $i < 5; $i++) { // 初始化 $i 为 0,每次循环后将 $i 值增加 1,当 $i 小于 5 时执行循环echo "The number is: $i \n"; // 输出当前 $i 的值并换行 }// 循环输出结果为: // The number …

【一秒梵高】基于OpenCV4实现图像九种风格迁移

风格迁移 图像风格迁移、色彩填充与色彩变换等&#xff0c;严格意义上来说都属于计算机视觉任务中图像处理的分支。它们输入的是图像&#xff0c;输出的也是图像&#xff0c;过程实现图像到图像的内容与风格的转换&#xff0c;深度学习在这类图像处理任务上也取得了良好的效果…

改进了编排控制并增强了推理的可视性,Agents for Amazon Bedrock 现已上市

七月份的时候&#xff0c;我们推出了 Agents for Amazon Bedrock 预览版。如今&#xff0c;Agents for Amazon Bedrock 全面上市。 Agents for Amazon Bedrock 通过编排多步任务&#xff0c;有助于您加速生成人工智能 &#xff08;AI&#xff09; 应用程序的开发。代理使用基础…

【ARM Coresight 系列 2 文章 -- Trace32 对 APBIC 地址的配置 介绍】

请阅读【ARM Coresight SoC-400/SoC-600 专栏导读】 文章目录 APBIC RomtableTrace32 RESBREAKTrace32 ENRESETAPBIC Romtable 图 1 APBIC 网络图 如上图所示,如果想通过Trace32/DS-5 去访问 AP, 这个时候需要怎么做呢?可以看到 APBIC 中ROMTABLE 中 APB-AP 的偏移是0x002000…

2023 re:Invent|Amazon Q与Amazon CodeWhisperer面向企业开发者提效利器

本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道 2023年&#xff0c;以GPT为代表的生成式AI引爆了新一轮技术热潮&#xff0c;短短一年的时…

Jenkins离线安装部署教程简记

前言 在上一篇文章基于Gitee实现Jenkins自动化部署SpringBoot项目中&#xff0c;我们了解了如何完成基于Jenkins实现自动化部署。 对于某些公司服务器来说&#xff0c;是不可以连接外网的&#xff0c;所以笔者专门整理了一篇文章总结一下&#xff0c;如何基于内网直接部署Jen…

【数据结构】栈和队列超详解!(Stack Queue)

文章目录 前言一、栈1、栈的基本概念2、栈的实现&#xff08;数组实现&#xff09;3、栈的基本操作3.1 栈的结构设计3.2 栈常见的基本函数接口 4、栈的实现4.1 初始化栈4.2 栈的销毁4.3 入栈4.4 出栈4.5 判空4.6 长度4.7 获取栈顶元素 完整代码Stack.hStack.cTest.c 二、队列1、…

排序-归并排序与计数排序

文章目录 一、归并排序1、概念2、过程3、代码实现4、复杂度5、稳定性 二、 计数排序1、思路2、代码实现3、复杂度&#xff1a;4、稳定性 一、归并排序 1、概念 是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已…

车载导航系统UI界面,可视化大屏设计(PS源文件)

大屏组件可以让UI设计师的工作更加便捷&#xff0c;使其更高效快速的完成设计任务。现分享车载导航系统科技风蓝黑简约UI界面、车载系统UI主界面、车载系统科技风UI界面、首页车载系统科技感界面界面的大屏Photoshop源文件&#xff0c;开箱即用&#xff01; 若需 更多行业 相关…

数据库动态视图和存储过程报表数据管理功能设计

需求&#xff1a;需要将ERP的报表数据挪到OA中&#xff0c;但是OA表单设计不支持存储过程动态传参&#xff0c;所以需要设计一个系统&#xff0c;可以手动配置&#xff0c;动态显示原本ERP的报表数据&#xff0c;ERP报表是存在数据库的视图和存储过程中 思路&#xff1a;因为E…

算法复习——6种排序方法的简单回顾

算法复习——6种排序方法的简单回顾 常见排序方法&#xff1a;冒泡排序、选择排序、插入排序、堆排序、归并排序、快速排序的简单回顾 冒泡排序 重复“从序列右边开始比较相邻两个数字的大小,再根据结果交换两个数字的位置” 在冒泡排序中&#xff0c;第 1 轮需要比较 n - 1…

整理b站黑马程序员C++课程中对于计算机视觉学习有所帮助的知识点。(重点用*标出)

文章目录 1、注释2、变量3、常量4、标识符5、整型 浮点型 字符型 字符串 布尔6、输入 输出7、逻辑运算法8、 程序流程结构9、三目运算符10、switch语句11、循环语句12、跳转语句13、*数组13.1一维数组名 14、二维数组15、**函数15.1、函数的调用15.2、函数的声明15.3、函数份文…

Android camera的metadata

一、实现 先看一下metadata内部是什么样子&#xff1a; 可以看出&#xff0c;metadata 内部是一块连续的内存空间。 其内存分布大致可概括为&#xff1a; 区域一 &#xff1a;存 camera_metadata_t 结构体定义&#xff0c;占用内存 96 Byte 区域二 &#xff1a;保留区&#x…

HarmonyOS--基础组件TextInput

TextInput 官方文档 TextInput组件https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/ts-basic-components-textinput-0000001427584864-V3#ZH-CN_TOPIC_0000001523968610__%E5%AD%90%E7%BB%84%E4%BB%B6 文本输入框组件 接口 TextInput(value?:…

【Python】用Python发邮件

准备工作 以新浪邮箱为例&#xff0c;进入账号管理&#xff0c;打开授权码并保存下来 用到的包 import smtplib from email.header import Header from email.mime.text import MIMEText 账号授权码准备 这里用的是前面记录的授权码&#xff0c;不是登录密码哦 email_hostsm…

40G AOC线缆全系列产品知识详解

40G AOC&#xff08;Active Optical Cable&#xff09;线缆作为高速数据传输的重要组成部分&#xff0c;在现代通信和数据中心应用中扮演着重要角色。本期文章我们将从其基本原理、应用领域、优势特点等方面对ETU-LINK 40G AOC全系列产品进行解析。 一、40G AOC全系列产品解析…

Facebook广告投放常见错误

在进行Facebook广告投放时&#xff0c;很容易犯一些常见的错误。这些错误可能导致广告投资的浪费&#xff0c;影响广告效果并降低回报。本文小编讲一些常见的Facebook广告投放错误&#xff0c;以及如何避免它们。 1、不明确目标受众 广告的成功与否很大程度上取决于你选择的目…