排序算法(详解)

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

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

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)

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

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

相关文章

SQL进阶理论篇(四):索引的结构原理(B树与B+树)

文章目录 简介如何评价索引的数据结构设计好坏二叉树的局限性什么是B树什么是B树总结参考文献 简介 我们在上一节中说过&#xff0c;索引其实是一种数据结构&#xff0c;那它到底是一种什么样的数据结构呢&#xff1f;本节将简单介绍一下几个问题&#xff1a; 什么样的数据结…

2024 年,新程序员如何与AI共赢!!

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

C++笔记汇总(随时更新)

你好&#xff0c;这里是争做图书馆扫地僧的小白。 个人主页&#xff1a;争做图书馆扫地僧的小白_-CSDN博客 目标&#xff1a;希望通过学习技术&#xff0c;期待着改变世界。 目录 前言 一、C语言向C语言过度的知识点 二、C语言的相关知识 总结 前言 2023.12.13 之前撰写的笔…

RobotFramework自动化测试框架的基础关键字

1.1.1 如何搜索RobotFramework的关键字 有两种方式可以快速的打开RIDE的关键字搜索对话框 1、选择菜单栏Tools->Search Keywords&#xff0c;然后会出现如下的关键字搜索对话框&#xff0c;这个对话框就类似提供了一个关键字的API的功能&#xff0c;提供了关键字的…

决策曲线 DCA 学习

roc回顾ROC及曲线面积汇总学习-CSDN博客 原理 P&#xff1a;给真阳性患者施加干预的受益值&#xff08;比如用某生化指标预测某患者有癌症&#xff0c;实际也有&#xff0c;予活检&#xff0c;达到了确诊的目的&#xff09;&#xff1b; L&#xff1a;给假阳性患者施加干预的…

进程调度算法

进程调度算法 优先调度算法 先来先服务调度算法&#xff08;FCFS&#xff09; 当在作业调度中采用该算法时&#xff0c;每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业&#xff0c;将它们调入内存&#xff0c;为它们分配资源、创建进程&#xff0c;然后放…

使用qt实现四则运算计算机项目

这是我们要包含的头文件 #include <QWidget> #include<QStack> #include<string.h> #include<string> 这是我在ui界面创建的计算机基础框架。 接下来要实现按住每个按钮在白框内显示&#xff1b; 因此我们要定义一个QString 类型的变量 QString e…

react-router-dom v6中优雅处理404重定向问题

在基于React的单页面应用&#xff08;SPA&#xff09;中&#xff0c;使用 react-router-dom 库来管理路由是一项关键任务。当用户访问一个不存在的页面时&#xff0c;我们通常希望能够以优雅的方式处理404情况&#xff0c;从而提升用户体验。本文将探讨如何在React应用中使用re…

【算法Hot100系列】无重复字符的最长子串

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

python下使用Open3D

1.切记不要安装最新的python否则无法使用open3D &#xff0c;官网显示只支持python3.8-3.11 这是我安装的python版本 2.由于访问github很慢&#xff0c;所以我手动下载ply文件 https://github.com/isl-org/open3d_downloads/releases/download/20220201-data/fragment.ply 3…

Python占位符%详解:格式化字符串的利器

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python中&#xff0c;%占位符是一种强大的工具&#xff0c;用于格式化字符串。本文将深入解析Python中占位符的使用方法&#xff0c;包括字符串格式化、数字格式化、日期格式化等多个方面。通过丰富的示例代码…

设计模式(2)--对象创建(2)--生成器

1. 意图 将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 2. 四种角色 指挥(Director)、抽象生成器(Builder)、具体生成器(Concrete Builder)、产品(Product) 3. 优点 3.1 可以改变一个产品的内部表示(通过定义新的生成器)。 3.2 将构…

软件项目总结报告

1. 项目进度 1.1. 进度表 1.2. 总结偏差 2. 项目成本 2.1. 项目规模 2.2. 项目工作量 3. 项目质量 3.1. 评审 4. 计划偏差 5. 测试总结 5.1. 缺陷分析 5.2. 测试Bug分布统计 5.3. Bug分布图 5.4. 总结 6. 最佳实践 7. 经验教训 7.1. 项目过程管理 7.2. 合同完成度管理 7.3. 项目…

Apifox接口测试工具详细解析

最近发现一款接口测试工具--apifox&#xff0c;我我们很难将它描述为一款接口管理工具 或 接口自测试工具。 官方给了一个简单的公式&#xff0c;更能说明apifox可以做什么。 Apifox Postman Swagger Mock JMeter Apifox的特点&#xff1a; 接口文档定义&#xff1a; Api…

Kibana搜索数据利器:KQL与Lucene

文章目录 一、搜索数据二、KQL查询1、字段搜索2、逻辑运算符3、通配符4、存在性检查5、括号 三、Lucene查询1、字段搜索2、逻辑运算符3、通配符4、范围搜索5、存在性检查6、括号 四、总结 一、搜索数据 默认情况下&#xff0c;您可以使用 Kibana 的标准查询语言&#xff0c;该…

如何将从GitHub上弄下来的Three.js本地官网设为中文

我们辛辛苦苦从git上面弄下来的 Three.js 本地文档 启动之后 会发现 好家伙 这鬼东西是个英文的 我们可以找到根目录下的 docs下的 index.html 然后全局搜索 language 变量声明的地方 let language你能看到是英文 那说明 它用的肯定是en 我们改成zh 我们整个文档就变成中文…

PHP在线SEO文章伪原创同义词交换工具源码

源码介绍 PHP在线SEO文章伪原创同义词交换工具源码 支持关键词提交 独立后台 1.支持文章在线伪原创功能 2.支持关键字交换预览 3.有独立背景 4.支持访客提交关键词(后台可以审核用户提交的关键词) 5.完全开源&#xff0c;支持二次开发 使用php语言独立开发utf-8编码 适合工具…

二叉搜索树的简单理解

1. 二叉搜索树 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0c;或者是具有以下性质的二叉树&#xff1a; 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点的值 它…

接口测试总结及其用例设计方法整理

接口测试的总结文档 第一部分&#xff1a;主要从问题出发&#xff0c;引入接口测试的相关内容并与前端测试进行简单对比&#xff0c;总结两者之前的区别与联系。但该部分只交代了怎么做和如何做&#xff1f;并没有解释为什么要做&#xff1f; 第二部分&#xff1a;主要介绍为什…

A good teacher is patient and consistent(CVPR 2022)论文解读

paper&#xff1a;Knowledge distillation: A good teacher is patient and consistent official implementation&#xff1a;https://github.com/google-research/big_vision 本文的创新点 本文没有提出新的方法&#xff0c;而是提出了一些影响蒸馏性能的关键因素&#xff…