数据结构:手撕图解七大排序(含动图演示)

news2024/11/14 2:11:23

文章目录

  • 插入排序
    • 直接插入排序
    • 希尔排序
  • 选择排序
    • 选择排序
    • 堆排序
  • 交换排序
    • 冒泡排序
    • 快速排序
      • hoare版
      • 挖坑法
      • 前后指针法
      • 快速排序的递归展开图
      • 快速排序的优化
        • 三数取中法
      • 快速排序的非递归实现
  • 归并排序

在这里插入图片描述

插入排序

插入排序分为直接插入排序和希尔排序,其中希尔排序是很值得学习的算法

希尔排序的基础是直接插入排序,先学习直接插入排序

直接插入排序

直接插入排序类似于打扑克牌前的整牌的过程,假设我们现在有2 4 5 3四张牌,那么应该怎么整牌?
方法很简单,把3插到2和4中间,这样就完成了整牌的过程,而插入排序的算法就是这样的过程

插入排序的基本原理图如下所示

在这里插入图片描述
我们在这里定义end为已经排查结束的,排好序的一段数据的最后一个元素,tmp作为存储要移动的元素,那么具体实现方法如下:
在这里插入图片描述
这里找到了tmp确实比end要小,于是下一步是要让tmp移动到end前面这段有序的数据中的合适的位置

算法实现的思想是:tmp如果比end的值小,那么就让end的值向后移动,end再指向前一个,再比较覆盖移动…直到tmp的值不比end小或者end直接走出数组,如果走出数组就让tmp成为新的首元素

在这里插入图片描述

这样就完成了一次插入,那么接着进行一次排序:

在这里插入图片描述
从中可以看出,插入排序整体的思想并不算复杂,代码实现相对也更简单,直接插入排序的价值在于给希尔排序做准备

插入排序的实现如下:

void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		int end = i;      // 找到有序数组的最后一个元素
		int tmp = a[i + 1];  // 要参与插入排序的元素
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				// 进行覆盖
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}

		a[end + 1] = tmp;
	}
}

直接插入排序的时间复杂度也不难分析,是O(N^2),和冒泡排序在同一个水平,并不算高效
直接插入排序更多是给希尔排序做铺垫,希尔排序是很重要的排序,处理数据的效率可以和快速排序看齐

希尔排序

上面学完了插入排序,那么就必须总结一下插入排序的弊端

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

于是,希尔排序就是基于上面这两个问题进行解决的:
首先,插入排序对于已经排序差不多的序列有很强的效率,但与此同时它一次只能调整一个元素的位置,因此希尔就发明了希尔排序,它具体的操作几乎和插入排序相同,只是在插入排序的基础上,前面多了预排序的步骤,预排序是相当重要的,可以把一段数据的大小排序基本相同

那预排序的实现是如何实现的?

首先把数据进行分组,假设分成3组,下面的图片演示了这个过程

在这里插入图片描述

分好组后,对每一组元素单独进行插入排序

在这里插入图片描述
此时,序列里的数据就已经很接近有序了,再对这里的数据进行插入排序,可以完美适应插入排序的优点

在这里插入图片描述

这里只是写了希尔排序的基本思想是如何工作的,具体的代码实现较为复杂

那么下一个问题就有了,为什么gap是3?如果数据量特别大还是3吗?gap的选择应该如何选择?
这里对gap要进行理解,gap到底是用来做什么的,它的大小会对最终结果造成什么影响

gap是对数据进行预处理阶段选择的大小,通过gap可以把数据变得相对有序一点,而gap越大,说明分的组越多,每一组的数据就越少,gap越小,分的就越细,就越接近真正的有序,当gap为1的时候,此时序列只有一组,那么就排成了真正的有序

代码实现如下:

void ShellSort(int* a, int n)
{
	int gap = n;
	while (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 = end - gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

这里重点理解两点

gap = gap / 3 + 1;  // 这句的目的是什么?
gap==1  // 是什么

gap=gap/3+1会让gap的最终结果一定为1
而gap为1的时候,此时就是插入排序,而序列也接近有序,插入排序的优点可以很好的被利用

希尔排序的时间复杂度相当难算,需要建立复杂的数学模型,这里直接说结论,希尔排序的时间复杂度大体上是接近于 O(N^1.3) 整体看效率是不低的,值得深入钻研学习

选择排序

选择排序

基础版的选择排序实现是很简单的,算法思路如下

在这里插入图片描述

在这里插入图片描述

这里需要注意一点是,maxi可能会和begin重叠,导致交换begin和min的时候产生bug,因此只需要在前面补充一下条件即可

void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;
	while (begin < end)
	{
		int maxi = begin, mini = begin;
		for (int i = begin; i <= end; i++)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}

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

		Swap(&a[begin], &a[mini]);
		// 如果maxi和begin重叠,修正一下即可
		if (begin == maxi)
		{
			maxi = mini;
		}

		Swap(&a[end], &a[maxi]);

		++begin;
		--end;
	}
}

堆排序

堆排序前面文章有过详细讲解,这里就不多赘述了

数据结构—手撕图解堆

直接上代码实现

void Swap(int* p, int* c)
{
	int tmp = *p;
	*p = *c;
	*c = tmp;
}

void AdjustDown(int* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			child++;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	// 建堆
	for (int i = (n - 2) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	// 堆排序
	int end = n - 1;
	while (end)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

交换排序

冒泡排序

在这里插入图片描述

入门出学的第一个排序,效率很低

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

下面重点是对快速排序进行学习,快速排序正常来说是泛用性最广的排序算法

快速排序

快速排序是所有排序算法中速度最快的一个排序算法(在数据量很庞大的前提下),因此,很多库中自带的sort都是用快速排序做底层实现的,例如qsort和STL中的sort,因此,学习好它是很有必要的

首先说明它的基本思想

基本思路是,选定一个元素为key,经过一系列算法让原本数组中比key小的数据在key的左边,比key大的数据在key的右边,然后递归进入key的左边,在递归函数中重复这个操作,最后就能完成排序,那么第一个关键点就是如何实现让以key为分界点,左右分别是比它大和比它小的?

关于这个算法有很多版本,我们一一介绍

hoare版

快速排序的创始人就是hoare,作为快速排序的祖师爷,hoare在研究快速排序自然写出了对应的算法,那么我们当然要先学习最经典的算法

下面展示hoare算法的示意图

在这里插入图片描述
在这里插入图片描述
看完演绎图和上面的流程图,大概可以理解hoare算法的基本思路,但其实还有一些问题,比如最后交换的元素(上图中为3) 一定比key小吗?比key大难道不会让大的元素到key的左边吗?

解释上述问题的原因

其实问题的原因就在于left和right谁先走的问题,在上面算法中是让right先走,这是为什么?
我们假设中间的元素不是3,而是8 (大于key的都可以) 那么,当right继续向前走的时候就会跳过8,继续向前找,最后最坏的结果会找到left,而left对应的是和前面交换后的比key小的元素,因此这里只要是right先走,最终和right和left相遇的位置一定比key小!

这个算法其实并不好写,需要控制的变量和问题很多,实现过程如下

int PartSort1(int* a, int left, int right)
{
	int keyi = left;
	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]);
	
	return left;
}

注意点

  1. keyi的选取是left而不是0,因为后面递归的时候最左边的元素下标不一定是0
  2. while循环向前/向后寻找时要随时判断left有没有小于right,防止越界
  3. 返回的是左值,这个左值就是下一次的左边界或右边界、

快速排序的实现

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

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

后续的三种写法只需要替换掉PartSort1即可

挖坑法

在这里插入图片描述
在这里插入图片描述
代码实现如下:

int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while (left<right && a[right]>= key)
		{
			right--;
		}

		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= key)
		{
			left++;
		}

		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;

	return hole;
}

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

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

这个实现很简单,没有需要额外注意,相较第一个算法来说更容易理解一些

前后指针法

实现原理如下图所示

在这里插入图片描述
在这里插入图片描述
代码实现逻辑如下

int PartSort3(int* a, int left, int right)
{
	int cur = left+1;
	int prev = left;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			++prev;
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}

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

	return prev;
}

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

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

实际上是prev找cur,如果cur指针对应的值小于key,那么就++prev再交换,否则cur就继续前进,这样就能使得cur和prev之间的数据全部为比key大的数据

快速排序的递归展开图

了解了快速排序的工作原理,独立画出它的递归展开图有助于了解它的工作原理

在这里插入图片描述

快速排序的优化

快速排序确实是在很多方面都很优秀的排序,但是仅仅用上述的代码并不能完全解决问题,假设现在给的序列是一个按升序排列的序列,那么此时我们选取的key是最小的数据,时间复杂度是O(N^2),但如果每次选取的数据恰好是中位数,那么就是整个数据最高效的方式,时间复杂度是O(NlogN),因此如何优化?

常见的优化有三数取中法和递归到小的子区间选取插入排序法

三数取中法

顾名思义,就是取开头,末尾和中间的三个数,选这三个数中最中间的一个,让这个数作为key

int GetMid(int* a, int left, int right)
{
	int midi = (left + right) / 2;
	if (a[left] < a[midi])
	{
		if (a[midi] < a[right])
		{
			return midi;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else  // a[left] > a[midi]
	{
		if (a[midi] > a[right])
		{
			return midi;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

int PartSort1(int* a, int left, int right)
{
	int midi = GetMid(a, left, right);
	Swap(&a[midi], &a[left]);
	int keyi = left;
	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]);

	return left;
}

int PartSort2(int* a, int left, int right)
{
	int midi = GetMid(a, left, right);
	Swap(&a[midi], &a[left]);
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= key)
		{
			right--;
		}

		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= key)
		{
			left++;
		}

		a[hole] = a[left];
		hole = left;
	}

	a[hole] = key;

	return hole;
}

int PartSort3(int* a, int left, int right)
{
	int midi = GetMid(a, left, right);
	Swap(&a[midi], &a[left]);
	int cur = left+1;
	int prev = left;
	int keyi = left;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			++prev;
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}

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

	return prev;
}

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

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

快速排序的非递归实现

快速排序是利用递归实现的,而凡是递归就有可能爆栈的情况出现,因此这里要准备快速排序的非递归实现方法

非递归实现是借助栈实现的,栈是在堆上malloc实现的,栈区一般在几十Mb左右,而堆区有几G左右的空间,在堆上完成操作是没有问题的

在这里插入图片描述

当left<keyi-1才会入栈,当keyi+1<right才会入栈

随着不断入栈出栈,区间划分越来越小,left最终会等于keyi-1,这样就不会入栈,右边同理,不入栈只出栈,最终栈会为空,当栈为空时,排序完成

归并排序

归并排序的排序原理如下:

在这里插入图片描述

从中可以看出,归并排序的原理就是把一整个大的,无序的数组分解成小数组,直到分到数组中只有一个数,再把数组组装成有序的数组,再把组装成有序的两个数组合并成有序数组,再让这个有序数组和另外一个合并…依次递归,这样就和二叉树一样递归成了一个合适的数组

在这里插入图片描述
代码实现如下:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin == end)
		return;
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	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, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}

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

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

相关文章

python面试题(一)

如果无聊&#xff0c;可以查看这篇文章~ 使用python做一些奇奇怪怪的事情&#xff1f;_什么时候才能变强的博客-CSDN博客https://blog.csdn.net/qq_17496235/article/details/131906360 问&#xff1a;什么是Python的GIL&#xff08;全局解释器锁&#xff09;&#xff1f;它对…

如何以毫秒精度,查看系统时间以及文件的创建时间

用 cmd 查看系统的时间&#xff1a; powershell -command "(Get-Date -UFormat %Y-%m-%d %H:%M:%S).toString() . ((Get-Date).millisecond)" 用 XYplorer 查看文件的精确创建时间&#xff08;含30天试用&#xff09;&#xff1a; XYplorer - File Manager for …

基于拉格朗日-遗传算法的最优分布式能源DG选址与定容(Matlab代码实现)

目录 1 概述 2 数学模型 2.1 问题表述 2.2 DG的最佳位置和容量&#xff08;解析法&#xff09; 2.3 使用 GA 进行最佳功率因数确定和 DG 分配 3 仿真结果与讨论 3.1 33 节点测试配电系统的仿真 3.2 69 节点测试配电系统仿真 4 结论 1 概述 为了使系统网损达到最低值&a…

系统学习Linux-MySQL用户权限管理(三)

一、用户权限管理概述 数据库用户权限管理是数据库系统中非常重要的一个方面&#xff0c;它用于控制不同用户访问和操作数据库的权限范围。数据库用户权限管理可以保护敏感数据和数据库结构&#xff0c;确保只有被授权的用户才可以操作和使用数据库&#xff0c;防止数据被修改…

Qt之切换语言的方法(传统数组法与Qt语言家)

http://t.csdn.cn/BVigB 传统数组法&#xff1a; 定义一个字符串二维数组&#xff0c; QString weekStr[2][7] {"星期一","星期二","星期三","星期四","星期五","星期六","星期日",\ "Monday&…

2023级中国社科院美国杜兰大学中外合作办学双证能源管理硕士

2023级中国社科院美国杜兰大学中外合作办学双证能源管理硕士 作为国内首个且唯一侧重能源金融交易的硕士项目&#xff0c;中国社会科学院大学与美国杜兰大学合作举办的能源管理硕士&#xff08;Master of Management in Energy&#xff09;项目旨在培养具备国际视野的高级能源…

基于XDMA 中断模式的 PCIE3.0 QT上位机与FPGA数据交互架构 提供工程源码和QT上位机源码

目录 1、前言2、我已有的PCIE方案3、PCIE理论4、总体设计思路和方案图像产生、发送、缓存数据处理XDMA简介XDMA中断模式图像读取、输出、显示QT上位机及其源码 5、vivado工程详解6、上板调试验证7、福利&#xff1a;工程代码的获取 1、前言 PCIE&#xff08;PCI Express&#…

权威认可丨九州未来获科学技术成果登记证书

近日&#xff0c;由九州未来自主研发的工业项目——“5G”边缘计算云平台的产品研究及其在工业视觉AI设计中的应用&#xff0c;经评审、公示获准登记&#xff0c;取得浙江省科技厅颁发的科学技术成果登记证书。 伴随我国在新基建领域不断推进深入&#xff0c;5G以其特有的大带宽…

Vue教程(三):计算属性

1、姓名案例—插值语法版 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>姓名案例-插值语法版</title><script type"text/javascript" src"../js/vue.js"><…

IDEA中文UT方法执行报错问题、wps默认保存格式

wps默认保存格式、IDEA中文UT方法执行报错问题 背景 1、wps修改文件后&#xff0c;编码格式从UTF-8-bom变成UTF-8&#xff08;notepad可以查看&#xff09;&#xff1b; 2、IDEA中文UT执行报错&#xff1a; 解决方案 1、语言设置中不要勾选 “Beta版。。。。” 2、cmd中执…

Vue-cli3项目之Vue.config.js配置文件—取代vue-cli2中build与config

我们在做vue项目的时候&#xff0c;在根目录中肯定都看到过一个vue.config.js文件&#xff0c;那么这个文件在整个项目中到底有什么作用呢&#xff1f;本文就来说说使用Vue-cli3 创建的vue项目中&#xff0c;Vue.config.js文件的配置问题。 说点题外话&#xff0c;先来看看vue-…

隐藏文件夹怎么显示?3种方法轻松解决!

“我有些文件不知道为什么就看不到了&#xff0c;不知道是因为我不小心将它们隐藏了还是删除了。有家人知道隐藏文件怎么显示的吗&#xff1f;非常着急&#xff0c;感谢回答&#xff01;” 为了保护我们的隐私&#xff0c;有些重要的文件我们不想将其被人看到&#xff0c;一个…

2017年全国硕士研究生入学统一考试管理类专业学位联考写作试题——解析版

2017年1月真题 四、写作&#xff1a;第56~57小题&#xff0c;共65 分。其中论证有效性分析30 分&#xff0c;论说文35分。 56、论证有效性分析&#xff1a; 分析下述论证中存在的缺陷和漏洞&#xff0c;选择若干要点&#xff0c;写一篇600字左右的文章&#xff0c;对论证的有…

【启扬方案】嵌入式核心板在全自动生化仪设备中的应用

随着科技的不断进步和医疗技术的发展&#xff0c;全自动生化分析仪作为体外诊断领域的重要工具之一&#xff0c;逐渐受到广泛的关注。全自动生化分析仪作为一种能够实时监测和分析体液中生化指标的设备&#xff0c;在临床医学应用中的用途广泛&#xff0c;可用于检测血液中的血…

Nim游戏博弈论

【模板】nim 游戏 题目描述 https://www.luogu.com.cn/problem/P2197 甲&#xff0c;乙两个人玩 nim 取石子游戏。 nim 游戏的规则是这样的&#xff1a;地上有 n n n 堆石子&#xff08;每堆石子数量小于 1 0 4 10^4 104&#xff09;&#xff0c;每人每次可从任意一堆石子…

管理后台低代码PaaS平台源码:点击鼠标,就能编程

低代码平台源码10大核心功能:1建模引擎 、2 移动引擎 、3,流程引擎 5.报表引擎、6安全引擎、 7 API引擎 、8.应用集成引擎、 9.代码引擎、 10.公式引擎。 一、低代码开发特色 1.低代码开发&#xff1a;管理后台提供了一系列易于使用的低代码开发工具&#xff0c;使企业可以快速…

CSPM难度大吗?对比pmp怎么样?

CSPM证书是刚出来的&#xff0c;难度不会很大&#xff0c;大家都知道 PMP 证书是从国外引进的&#xff0c;近几年很热门&#xff0c;持证人数已经高达 90 余万了&#xff0c;但是目前我们和老美关系大家有目共睹&#xff0c;一直推国际标准和美国标准感觉有点奇怪。 现在新出台…

React Flow

// 创建项目 npm create vitelatest my-react-flow-app -- --template react // 安装插件 npm install reactflow // 运行项目 npm run dev 1、App.jsx import { useCallback, useState } from react; import ReactFlow, {addEdge,ReactFlowProvider,MiniMap,Controls,useNode…

适用于虚拟环境的免费企业备份软件

多年来&#xff0c;许多行业严重依赖物理服务器提供计算资源——你可以想象到巨大的服务器机房和笨重的服务器的场景。 然而&#xff0c;随着业务快速增长&#xff0c;许多组织发现物理服务器已经无法有效利用计算资源。因此&#xff0c;为了节省成本&#xff0c;引入了虚拟服…

STL中 vector常见函数用法和迭代器失效的解决方案【C++】

文章目录 size && capacityreserveresizeempty迭代器begin和end push_back &&pop_backinsert && erasefindswap[ ]范围for遍历vector迭代器失效问题 size && capacity #include <iostream> #include <vector> using namespace st…