暴力数据结构之排序大杂烩

1. 冒泡排序:O(N^2)

逻辑解析: 冒泡排序并没有什么实际意义,但是有教学意义,相信大部分小白在学习的初期第一个接触的排序就是冒泡排序。那么接下来我们了解一下他的底层逻辑:

冒泡排序顾名思义就是将最大(小)的值像一个一个小气泡一样逐渐运动到最上层,也就是将一串数字从头开始两两比较,逐步将最值移动到最尾端,最终实现排序。其时间复杂度为O(N^2)。

 

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


//冒泡排序O(N^2)
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		int flag = 0;
		for (int j = 1; j < n - i; j++)
		{
			if (a[i] > a[j])
			{
				Swap(&a[i], &a[j]);
				flag = 1;
			}
		}
		//已经排好序,所以第一次遍历没有调整,直接退出
		if (flag == 0)
		{
			break;
		}
	}
}

2. 插入排序:O(N^2)

逻辑解析: 插入排序就是设置一个end,假设区间[0,end]都是有序的,此时取出第end+1个数字插入到前面的有序数列,以此类推,最值end到尾端,此时整个数列就是一个有序数列。时间复杂度为:O(N^2)

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

//插入排序O(N^2)
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//默认[0,end]的区间是有序的,取a[end+1]插入前面的区间
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

3. 希尔排序:O(N^1.3)

逻辑解析:希尔排序与插入排序的共同点是,希尔排序共进行多次,最后一次时的gap=1,也就是插入排序,在前面的时候先将gap置为n,也就是数组元素个数,这时每次对gap/3后+1,使gap/3递减的同时始终满足gap最小值为1,即保证最后一趟一定是插入排序。在插入排序之前要保证数组分为不相邻的gap组,保证每个组都是有序的,然后在最后一次插入排序使整个数组有序即可。时间复杂度为:O(N^1.3)

//希尔排序O(N^1.3)
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		// +1保证最后一个gap一定是1
		// gap > 1时是预排序
		// gap == 1时是插入排序
		gap = gap / 3 + 1;

		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];

			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

4. 堆排序:O(N*logN)

逻辑解析:堆排序是基于堆的逻辑结构设计的,所以了解堆排序首先要知道堆的相关知识,详情移步堆的相关知识 ,关于堆排序实质就是对数组的一些操作,即首先根据升(降)序来决定创建大(小)堆,然后生成一个堆。这时就直接将根节点与最后的叶子结点交换,再向下调整即可。即将数组的首尾交换后进行操作。时间复杂度为:O(N*logN)

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

void AdjustDown(int* a, int n, int parent)
{
	// 先假设左孩子小
	int child = parent * 2 + 1;

	while (child < n)  // child >= n说明孩子不存在,调整到叶子了
	{
		// 找出小的那个孩子
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


//堆排序O(N*logN)
void HeapSort(int* a, int n)
{
	// 降序,建小堆
	// 升序,建大堆
	// 向上调整建堆 O(N*logN)

	// 向下调整建堆 O(N)
	//从离叶子结点最近的上一层根节点开始
    //n-1代表最末尾的叶子结点,那么((n - 1) - 1) / 2就是该叶子结点的根结点
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	// O(N*logN)
	int end = n - 1;
	while (end > 0)
	{
		//交换根叶结点后向下调整
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

5. 选择排序: O(N^2)

逻辑解析: 选择排序和冒泡排序一样都没有什么实践意义,同样也没有什么教学意义,所以一般只做了解即可。关于选择排序,大体思路十分简单,就是将最大值与最小值下标都初始置为第一个元素的下标,然后从数组首尾分别向中间遍历,将最大值放在末尾,最小值放在首位,然后缩小遍历区间,再寻找该区间的最大值与最小值,放在两端,以此类推,直到遍历完整个数组即可。

//选择排序
//在[begin,end]范围内选择最值放到两边
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;

	while (begin < end)
	{
		int min = begin;
		int max = begin;

		for (int i = begin + 1; i <= end; i++)
		{
			if (a[i] > a[max])
			{
				max = i;
			}

			if (a[i] < a[min])
			{
				min = i;
			}
		}

		if (begin == max)
		{
			max = min;
		}

		Swap(&a[begin], &a[min]);
		Swap(&a[end], &a[max]);
		begin++;
		end--;
	}
}

6. 快速排序(递归实现):O(N*logN)

  1.霍尔方法实现

逻辑解析: 首先讲解一下霍尔发明的排序方法,就是初始时刻将数组最左边的元素置为 key 元素,然后设置两个标识 begin 与 end ,起初 begin 在最左边,end 在最右边,然后向中间遍历,如果 a[begin] 遇到比 a[key] 小的值就向后遍历,同理 a[end] 遇到比 a[key] 大的值就向前遍历,直到遇见不符合的情况,然后将 a[begin] 与 a[end] 交换,直到 begin == end ,这时将 a[key] 与 a[begin] 交换(与 a[end] 也可以) ,目的就是保证 key 元素的左边元素均小于它本身,右边元素都大于它本身,然后递归实现第一次排序后 key 元素的左右部分即可。

//快排的注意一点就是以一个端点为 key 开始端,那么另一个端点就先开始动
//即上述以左边为 key ,就是右边先开始动,直到 begin 和 end 遇见,此时的
//相遇点一定要小于 a[key] 的值

//快速排序优化
void QuickSort(int* a, int left, int  right)
{
	//当只有一个数字就返回
	if (left >= right)
	{
		return;
	}

	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((right - left + 1) < 10)
	{
		//这里 a+left = a[left],即从a[left]开始插入排序
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		// 三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int begin = left;
		int end = right;
		int key = left;
		while (begin < end)
		{
			//右找小
			//从最右边开始找出比a[key]小的值
			while (begin < end && a[key] <= a[end])
			{
				end--;
			}

			//左找大
			//从最左边开始找出比a[key]大的值
			while (begin < end && a[key] >= a[begin])
			{
				begin++;
			}

			//将左边比a[key]大的与右边比a[key]小的交换
			//保证begin == end 的位置左边的数字全部比a[key]小
			//同理,右边的数字全部比a[key]大
			Swap(&a[begin], &a[end]);
		}

		//当begin == end ,即两个目标相遇时就交换相遇的位置与a[key]的位置
		//然后以新的 key 为割点分为左部分与右部分,再对两部分依次递归
		Swap(&a[begin], &a[key]);
		key = begin;

		//递归实现,使第一次遍历后 key 位置的左右部分有序即可
		QuickSort(a, left, key - 1);
		QuickSort(a, key + 1, right);
	}
}

 2. 双指针方法实现 

逻辑解析:双指针就是初始化两个指针 cur  与 prev ,初始时刻 prev 指向开头,cur 指向 prev 的下一个位置,设置 key 元素为开头元素。然后 cur 向后遍历,遇到比 key 小的就让 prev++,然后交换 cur 与 prev 元素,遍历到 cur 指向元素末端之后结束,然后交换 prev 与 key 元素,然后递归对 key 元素的左右部分进行排序即可。

//快速排序优化
void QuickSort(int* a, int left, int  right)
{
	//当只有一个数字就返回
	if (left >= right)
	{
		return;
	}

	// 小区间优化,不再递归分割排序,减少递归的次数
	if ((right - left + 1) < 10)
	{
		//这里 a+left = a[left],即从a[left]开始插入排序
		InsertSort(a + left, right - left + 1);
	}
	else
	{
		// 三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int begin = left;
		int end = right;
		int key = left;
		while (begin < end)
		{
			//右找小
			//从最右边开始找出比a[key]小的值
			while (begin < end && a[key] <= a[end])
			{
				end--;
			}

			//左找大
			//从最左边开始找出比a[key]大的值
			while (begin < end && a[key] >= a[begin])
			{
				begin++;
			}

			//将左边比a[key]大的与右边比a[key]小的交换
			//保证begin == end 的位置左边的数字全部比a[key]小
			//同理,右边的数字全部比a[key]大
			Swap(&a[begin], &a[end]);
		}

		//当begin == end ,即两个目标相遇时就交换相遇的位置与a[key]的位置
		//然后以新的 key 为割点分为左部分与右部分,再对两部分依次递归
		Swap(&a[begin], &a[key]);
		key = begin;

		//递归实现,使第一次遍历后 key 位置的左右部分有序即可
		QuickSort(a, left, key - 1);
		QuickSort(a, key + 1, right);
	}
}

//单趟排序
int QSort2(int* a, int left, int right)
{
	int mid = GetMid(a, left, right);
	Swap(&a[left], &a[mid]);

	int key = left;
	int prev = left;
	int cur = prev + 1;

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

//快速排序双指针实现形式
void QuickSortDouble(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	int key = QSort2(a, left, right);

	QuickSort(a, left, key - 1);
	QuickSort(a, key, right);
}

7. 快速排序(非递归实现):O(N*logN)

逻辑解析: 虽然是非递归实现,但是其思路还是递归的思路,我们在递归版本的代码实质就是在对待排序数组的某些区间进行操作,所以非递归版本就沿用这个思路,依旧是对待排序数组的区间进行一系列操作,这里是通过栈来实现,即在栈中依次存入 begin 与 end 两个值,这两个值就是初始时刻待排序数组的全部区间,然后进行一趟排序,找到 key 元素,此时要排序的区间就分为了左区间 [begin,key - 1] 与右区间 [key + 1,end],这时就依次先对右区间再对左区间进行入栈操作,主要是因为栈遵循先进后出的原则,相关文章:暴力数据结构之栈与队列(栈的相关操作)

然后利用 while 循环实现不断分区间的操作即可,大体思路与递归相似。

//快速排序非递归实现(借助栈实现)
//主要是对区间进行操作,逐步二分,相当于深度优先遍历
void QuickSortNR(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);
		//取完栈为空,跳出while循环

		//单次排序
		//其中包含取中操作
		int key = QSort2(a, begin, end);

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

	STDestroy(&st);
}

8. 归并排序(递归实现):O(N*logN)

 

逻辑解析:归并排序就是首先两两比较再四四比较以此类推,直到排序完成,代码实现的思路是,创建一个新的数组,然后将原数组分为两个区间,左区间与右区间,然后分别对左右区间不断细分直到左右区间都只有一个数据,这时就对他们比较,将较小的值放在新数组,以此类推。概括起来就是不断二分直到不可再分为止,然后利用新开的数组再进行归并,最终实现有序。递归版本是先实现单趟排序的逻辑,再进行递归实现整个数组排序的实现。

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin == end)
	{
		return;
	}
	//如果[begin,mid][mid+1,end]有序就直接合并即可

	int mid = (begin + end) / 2;
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	//归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, 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, (end - begin + 1) * sizeof(int));
}

//归并排序(递归实现)时间复杂度:O(N*logN)
//分为左右区间,分别排序再合并到新数组即可
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc error");
		return;
	}

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

9. 归并排序(非递归实现): O(N*logN)

逻辑解析: 非递归版本就是首先进行一一比较,即第一个与第二个比较,第二个与第三个比较,以此类推,直到遇见末尾,然后再两两比较,即第一与第二个为一组,第二与第三个为一组,两组之间比较,然后存入新数组中,以此类推直到原数组分组到不可再分,代码实现就是使用 gap 进行分组,然后循环进行比较,注意不能越界,所以在越界时就要进行改正操作,最后排序完毕将新数组中已排序好的数据存入原数组即可。

//归并排序(非递归实现)
void MergeSortNR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc error");
		return;
	}

	//gap表示每一组归并的数据个数
	int gap = 1;
	while (gap < n)
	{
		//i表示每次归并的起始位置
		for (int i = 0; i < n; i += gap * 2)
		{
			//[begin1,end1][begin2,end2]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;

			// 第二组都越界不存在,这一组就不需要归并
			if (begin2 >= n)
				break;

			// 第二的组begin2没越界,end2越界了,需要修正一下,继续归并
			if (end2 >= n)
				end2 = n - 1;

			//归并左右区间
			int j = i;
			while (begin1 < end1 && begin2 < end2)
			{
				//取出较小的值存入新建的数组
				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;
	}

	free(tmp);
	tmp = NULL;
}

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

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

相关文章

力扣 101. 对称二叉树

给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/ bool check(struct TreeNode* L,struct TreeNode* R){if(!L&…

匠心独运,B 端系统 UI 演绎华章之美

匠心独运&#xff0c;B 端系统 UI 演绎华章之美

kettle从入门到精通 第六十四课 ETL之kettle kettle中执行SQL脚本步骤,使用需当心

1、群里有不定时会有同学反馈执行SQL脚本步骤使用有问题&#xff0c;那么咱们今天一起来学习下该步骤。trans中的执行SQL脚本有两方面功能&#xff0c;使用时需小心&#xff0c;不然很容易踩坑。 官方定义&#xff1a; 翻译&#xff1a; 您可以使用此步骤执行 SQL 脚本&#…

kafka集群内外网分流方案——筑梦之路

前言 在现代分布式系统架构中&#xff0c;Kafka作为一款高性能的消息队列系统&#xff0c;广泛应用于大数据处理、实时流处理以及微服务间的异步通信场景。特别是往往企业级应用中&#xff0c;业务网段和内网通信网段不是同一个网段&#xff0c;内网的机器想要访问业务数据只能…

考研人注意了:六月份保底进度+复习规划!

六月开始准备考研确实有点晚了&#xff0c;因为大家基本上上都是3月份开始的 开始的比别人晚&#xff0c;学习的时间就会比较紧张&#xff0c;但是不要怕&#xff0c;考研并不是比谁学的时间长&#xff0c;而是比谁学的效果好&#xff0c;就算是六月份开始&#xff0c;只要学习…

三、基于图像分类预训练编码及图神经网络的预测模型 【框图+源码】

背景&#xff1a; 抽时间补充&#xff0c;先挖个坑。 一、模型结构 二、源码

python结构化模式匹配switch-case,Python 3.10中引入,Python的模式匹配(pattern matching)语法

增加了采用模式加上相应动作的 match 语句 和 case 语句 的形式的结构化模式匹配。 模式由序列、映射、基本数据类型以及类实例构成。 模式匹配使得程序能够从复杂的数据类型中提取信息、根据数据结构实现分支&#xff0c;并基于不同的数据形式应用特定的动作。 语法与操作 模…

系统安全及应用11

一个新的服务器到手之后&#xff0c;部署服务器的初始化 1、配置IP地址 网关 dns解析&#xff08;static&#xff09;内网和外网 2、安装源外网&#xff08;在线即可&#xff09;&#xff0c;内网&#xff08;只能用源码包编译安装&#xff09; 3、磁盘分区&#xff0c;lvm …

coze自定义插件调用3

1&#xff0c;打开我的空间&#xff1b; 2&#xff0c;编辑&#xff0c;选择快捷指令 3&#xff0c;编辑指令 4&#xff0c;实际测试【输入框多了一个按钮“查询基础信息”&#xff0c;点击查询基础信息&#xff0c;提示输入缴费卡号&#xff0c;提交后如下图】

插入排序(直接插入排序与希尔排序)----数据结构-排序①

1、插入排序 1.1 插入排序的基本思想 将待排序的元素按其数值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的元素插入完为止&#xff0c;就可以得到一个新的有序序列 。 实际上在我们的日常生活中&#xff0c;插入排序的应用是很广泛的&#xff0c;例如我…

从墙的功能出发 -分析欧特克Revit和广联达数维的差别

欧特克&#xff08;Autodesk&#xff09;在三维建模软件领域的影响力是有目共睹的&#xff0c;它是行业的头部产商&#xff0c;拥有众多的高质量的三维设计软件&#xff0c;涵盖了建筑设计、机械设计与制造和电影文娱行业。Revit是其发布的建筑三维建模软件&#xff0c;也是BIM…

老黄自己卷自己!GPU要一年更新一代!预告新动作:AI工厂将吞噬一切

站在 AI 时代风口浪尖的弄潮儿英伟达又为大家带来了一场科技饕餮盛宴&#xff01; 昨晚 7 点&#xff0c;坐标中国台湾大学体育场&#xff0c;英伟达 CEO 黄仁勋为世界带来了一场名为 The Dawn of a New Industrial Revolution &#xff08;揭开新工业革命序幕&#xff09;的演…

IDEA 常用技巧

1、代码块整体移动 选中&#xff0c;tab整体右移选中&#xff0c;shifttab整体左 移 2、统一修改变量 3.方法分割线 seting >> editor >> apperance >> show method separators 4、快捷键 构造器、set与get方法、方法重写、toString 等快捷操 鼠标停留在…

微信公众号开发(五):私信日志记录

之前的开发内容里&#xff0c;基本是基本配置和回复设置&#xff0c;为了之后看用户/粉丝什么样的功能使用的最多&#xff0c;需要增加私信的日志记录&#xff1a; 1、日志表 首先&#xff0c;要在mysql里建表 主要字段&#xff1a;用户id、公众号id、时间、私信类型、私信内…

SQL Developer 小贴士:备份和恢复连接信息

问题与概念 有时候SQL Developer需要重装&#xff0c;能备份和恢复连接信息就比较重要。 SQL Developer提供连接的导出和导入功能。 导出连接 第一步&#xff1a;选择连接。 第2步&#xff1a;指定输出文件&#xff0c;例如sqldconns.json 第3步&#xff1a;因为连接中可…

一文读懂数据库中的DB、DBMS、DBS、DBAS

目前数据库的应用非常广泛,几乎各行各业都在直接或间接地与数据库打交道,例如网上购物、银行业务、铁路购票和酒店住宿等。在实际应用中,数据库、数据库管理系统、数据库系统和数据库应用系统经常被统称为数据库,而实质上这4个概念是不一样的,它们具有不同的定义和含义。下…

16.FreeRTOS直接任务通知 Notification

FreeRTOS 直接任务通知 Notification 介绍 在嵌入式系统开发中&#xff0c;任务间的通信和同步是非常重要的一部分。而FreeRTOS就提供了多种机制来实现这些&#xff0c;比如队列、信号量和事件组。不过&#xff0c;使用这些机制都需要创建一个通信对象&#xff0c;不能直接把事…

【Unity Shader入门精要 第10章】高级纹理(一)

1. 立方体纹理原理 立方体纹理由6张图片组成&#xff0c;每张图片分别对应立方体的一个面。这6张图片代表沿世界空间下的轴线&#xff08;上下左右前后&#xff09;观察所得的图像 立方体的应用主要分为两类&#xff1a; 单纯利用6张图片的展示功能&#xff0c;为我们提供一…

怎么下载 jar 包

一、在Maven仓库里面下载 Maven仓库 网址&#xff1a;https://mvnrepository.com/ 二、搜索需要的 jar 包&#xff08;以 druid 为例&#xff09; 三、找到 druid jar包&#xff0c;点进去 四、找到自己需要的版本&#xff0c;点进去 五、 点 jar 下载

数字化前沿:Web3如何引领未来技术演进

在当今数字化时代&#xff0c;随着技术的不断发展和创新&#xff0c;Web3作为一种新兴的互联网范式&#xff0c;正逐渐成为数字化前沿的代表。Web3以其去中心化、加密安全的特性&#xff0c;正在引领着未来技术的演进&#xff0c;为全球范围内的科技创新带来了新的可能性和机遇…