从零实现数据结构:一文搞定所有排序!(下集)

news2024/10/27 22:44:59

1.快速排序

思路框架:

在有了前面冒泡选择插入希尔排序之后,人们就在想能不能再快一点,我们知道排序算法说人话就是把大的往后放小的往前放,问题就在于如何更快的把大的挪到数组队尾小的挪到数组前面。这里我们先总结一下上集前面几种排序算法,冒泡排序一轮单趟排序相当于只完成了把一个最大的挪到后面;选择排序可以做到一轮单趟把一个最小的往前放一个最大的往后放;插入排序是从队头开始重新建立一个有序数组,一轮单趟完成了插入一个元素;希尔排序是一轮完成了一个组的排序,在前面排序的基础上完成了优化,大的元素只需要两到三步就可以跳到队伍的后端,小的元素也是同理,因此比前面那些排序时间复杂度要快出一个量级。

在有了前面这些排序的基础上,我们开始想单趟排序能不能一步到位,也不要像希尔排序那样分两三步,直接定义一个基准线,找到比key大的和比key小的同时交换,这样既完成了一次性既把大的往后放了也把小的往前面挪了,又只需要交换这一步就完成了元素挪动,这也是交换排序种类的魅力之一。接下来基于这个思想,我们介绍三种常见版本的写法

版本一:发明者hoare版本

我们先如上图定义最左边的元素为key也就是基准线(图中的红色),定义一个左一个右两个变量来同时往中间走,right找比基准线小的left找比基准线大的,找到之后就交换left和right,重复此动作知道left和right相遇停止。最后再交换key和left,完成一轮单趟排序。

下面我们一步一步从最基础的想法开始,跳过经典的坑逐渐丰富修正细节完成实现:

void PartSort(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		while (a[right] > a[key])
		{
			--right;
		}
		while (a[left] < a[key])
		{
			++left;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[left], &a[key]);
}

按照上面的思想我们完成了第一步最基础的实现,但是这里有一个潜在的问题,也就是死循环和等号问题。如果我们给出如下数组:

[6 1 6 7 9 6 10 8]

当left和right同时走到中间两个6的时候,就会出现死循环走不下去的情况,所以我们要加上等于号

修改后版本如下:

void PartSort(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		while (a[right] >= a[key])
		{
			--right;
		}
		while (a[left] <= a[key])
		{
			++left;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[left], &a[key]);
}

这里又出现了新的潜在问题,那就是由于key值的选择越界问题,例如如果我们修改一下刚刚的数组例子,令第一个元素等于0,也就是key=0

[0 1 6 7 9 6 10 8]

这个时候right从右边一直走会一直找不到比0小的数,从而走出了数组导致越界访问的问题,所以我们还要增加一个控制越界的判断条件。

修改版本如下:

void PartSort(int* a, int left, int right)
{
	int key = left;
	while (left < right)
	{
		while (left<right && a[right] >= a[key])
		{
			--right;
		}
		while (left<right && a[left] <= a[key])
		{
			++left;
		}
		swap(&a[left], &a[right]);
	}
	swap(&a[left], &a[key]);
}

这里我们需要注意判断顺序,一定是先判断是否越界再去访问,c语言对于越界访问是一种类似于抽查的行为,不一定能够检测的到,但是一旦我们这里换成vector类的话,检测就变得很严格,无论是访问还是干啥都能检测到。至此我们的单趟排序就已经完成了,下面我们思考两个问题:

1.为什么两个指针相遇的位子的数一定比key小

这里其实我们在问的是最后和left和key交换这一动作的合理性,我们分两种情况来讨论这个问题:

情况一:如果最后是right在移动和left相遇了,这里又分为两种情况

1.left一次没有动过,right直接从数组尾部一直找到left,这个时候left的位置就是key的位置,所以满足left位置一定比key小

2.前面已经经过一些轮次的交换了,这时候right再往左找比key小的时候遇到left,由于前面已经交换过了,所以left所在的位置已经是前面轮次right找到的比key小的数交换过来了,所以也满足left这个位置一定比key小

情况二:如果最后是left在移动和left相遇了

由于是先走的right,而right已经停下来了,所以肯定满足比key小,这个时候和left相遇也满足一定比key小

2.左右两边哪个先动有关系吗

根据上面的逻辑我们不难看出下面的规律:

1.如果左边作为key,那就要右边先走,保障了相遇位置的值比key小

2.如果右边作为key,那就要左边先走,保障了相遇位置的值比key大

至此我们完成了单趟排序的所有细节问题,我们来考虑整体的排序之前先想想单趟排序完成了啥:

一次单趟排序所完成的任务是:

1.key就已经排好了,就在它现在的位子以后都不用动了

2.如果左区间有序,右区间有序那整个数组就是有序了(递归分治思想)

所以我们根据这个思路给出整体排序的实现:

void QuickSort(int* a, int begin, int end)//这里传入区间的开始和结束的位置,方便递归
{
	if (begin >= end)//两种情况1.只有一个值2.区间不存在
		return;
	int key = PartSort(a, begin, end);
	QuickSort(a, begin, key - 1);
	QuickSort(a, key + 1, end);
}

下面引用一下网上的动图演示:
在这里插入图片描述

版本二:挖坑法

在完成了初始hoare版本的实现以后,不难看出这种实现坑有点多,后来又出现一种相对理解实现简单一点的挖坑法。我们先给出引用的动图,大家看图就能理解了,就不再作过多文字阐述:

在这里插入图片描述

下面先给出实现:

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;
}

需要注意的是这种挖坑法和第一种hoare版本的单趟排序结果可能不同,但其本质都是交换前后元素,时间复杂度是一样的。由于基本逻辑和前面基本相似所以不做过多解释。

版本三:前后指针法

在这里插入图片描述

这种方法本质上有别于前面两种,也是相对来说不好理解的一种方式,我们来具体解释一下。

这里cur指针是无论什么情况都往前走一格,用来找到比key小的数,找到之后和prev交换,如果cur和prev重合则无事发生,只有当cur走过了比key大的数的时候,再遇到比key小的数的时候发生交换,此时他们之间的数都是比key大的数,这个时候的交换就相当于把大的数一路翻滚到后面

前后指针法与前后交换法的区别:

特性前后指针法前后交换法
扫描方式单向(左到右)双向(两端向中间)
基准值位置通常选择最后一个元素通常选择第一个元素
交换方式只交换小于基准值的元素与大于基准值的元素直接交换找到的不符合基准值的两个元素
操作次数O(n)O(n)
适用场景通常适用于快速分区且实现简洁常用于理解双向扫描,适合手动模拟操作
稳定性不稳定不稳定
int PartSort3(int* a, int left, int right)
{
	int prev = left;
	int cur = left+1;
	int key = left;
	while (left <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)
		{
			++prev;
			swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	swap(&a[prev], &a[key]);
	key = prev;
	return key;
}

非递归栈版本

例如还是这个例子,在我们实现非递归版本之前先搞清楚递归版本的顺序,再模拟成栈的顺序即可。下面是递归的区间顺序:区间 0-9->区间 0-6->区间 8-9->区间 0-3->区间 5-6->区间 0-2->区间 0-1。因此模拟栈的思路就是每次从栈里面拿出来栈顶的一个区间,然后再让该区间的左右区间入栈。下面给出实现:

void QuickSortIterative(int* a, int n) {
    std::stack<std::pair<int, int>> stack;
    stack.push({0, n - 1}); // 初始区间

    while (!stack.empty()) {
        int left = stack.top().first;
        int right = stack.top().second;
        stack.pop();

        if (left < right) {
            int pivotIndex = Partition(a, left, right);

            // 将右区间压入栈中
            stack.push({pivotIndex + 1, right});
            // 将左区间压入栈中
            stack.push({left, pivotIndex - 1});
        }
    }
}
1. 初始化栈
  • 创建一个 std::stack 来模拟递归过程。
  • 初始时,将整个数组的区间 [0, n-1] 压入栈中。栈中存储的每个区间用一对整数表示,其中第一个整数是区间的起始索引 left,第二个整数是区间的结束索引 right
2. 循环处理栈中的区间
  • 进入 while 循环,只要栈不为空,就表示还有未排序的区间需要处理。

    • 弹出栈顶区间

      • 使用 stack.top() 获取栈顶元素,分别取出当前区间的起始和结束位置 leftright
      • stack.pop() 将这个区间从栈中移除,因为我们即将处理它。
    • 检查区间是否有效

      • 检查 left < right,确保当前区间至少包含两个元素。如果 left >= right,则说明这个区间已经排序好(一个元素或空区间),不需要进一步处理。
    • 将子区间压入栈

      • 将左右两个子区间压入栈中,继续处理。注意先将右子区间 [pivotIndex + 1, right] 压入栈,再将左子区间 [left, pivotIndex - 1] 压入栈。这样栈顶的元素总是左子区间,确保以“先处理左侧,再处理右侧”的顺序执行,和递归版本一致。

极端情况与优化版本:

其实我们现在这种版本的快排在绝大多数情况下是比其他排序算法要快的,因此各大语言的sort算法也基本是按照快排来写的,但是唯一的缺陷就是怕遇到极端情况,例如数组在接近有序的情况下就显的非常慢甚至不如插入排序。在有序情况下就会出现如下情况,每次选key选是最小(最大)的数,就造成了每层只能排好一个数O(N),需要排N层,很明显这是一个等差数列,通过公式得出最坏的情况时间复杂度是

因此我们给出解决方案有两种:1随机数取key        2三数取中作为key

随机数只需要添加一行即可:

int pivotIndex = left + rand() % (right - left + 1);

三数取中这里我们添加一个取中函数:

int MedianOfThree(int* a, int left, int right) {
    int mid = left + (right - left) / 2;

    // 对 left, mid, right 三个元素排序,选择中间值作为基准
    if (a[left] > a[mid]) std::swap(a[left], a[mid]);
    if (a[left] > a[right]) std::swap(a[left], a[right]);
    if (a[mid] > a[right]) std::swap(a[mid], a[right]);

    // 将中间值移到最右边,作为基准
    std::swap(a[mid], a[right]);
    return a[right];
}

小区间插入优化:

我们还可以做进一步优化,我们知道二叉树最后一层叶子结点占了整个树的结点个数大部分,这里递归调用也是如此,因此会造成递归深度太深的问题,因此为了解决这个问题我们可以设置一个阈值,如果小于该阈值的时候直接调用插入排序即可,插入排序在接近有序的情况下还是很好用的。

  if (right - left <= threshold) 
{
    InsertionSort(a, left, right);
    continue;
}

至此快速排序的所有内容结束!

2.归并排序

还是先给出引用的动图方便理解:

在这里插入图片描述

可以看到归并排序的思想也是分治,想要整体有序,先让左区间右区间有序,以此递归下去。和快排不同的是,快排是自顶向下的,而归并是自下而上的,先分到不能再分为止然后再一层一层往上合并。下面给出具体的实现:

void PartMergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;
	int mid = (begin + end) / 2;
	PartMergeSort(a, begin, mid, tmp);
	PartMergeSort(a, mid + 1, end, tmp);
	int beginl = begin, endl = mid;//左区间
	int beginr = mid + 1, endr = end;//右区间

	//合并两个有序数组
	int index = begin;
	while (beginl <= endl && beginr <= endr)
	{
		if (a[beginl] < a[beginr])
		{
			tmp[index++] = a[beginl++];
		}
		else
		{
			tmp[index++] = a[beginr++];
		}
	}
	//解决某一个已经结束的情况,处理剩下的数据
	//由于不知道哪个先结束所以只能两个都判断一遍
	while (beginl <= endl)
		tmp[index++] = a[beginl++];
	while (beginr <= endr)
		tmp[index++] = a[beginr++];
	//最后拷贝回原数组
	memmove(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}


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

需要注意的是,这里由于我们要申请临时的拷贝空间,所以定义了一个子函数。

至此常见的排序算法结束力,感谢您看到这里!

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

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

相关文章

jenkins 自动化部署Springboot 项目

一、安装docker 1.更新yum命令 yum -y update2.查看机器有残留的docker服务&#xff0c;有就卸载干净 查看docker 服务 rpm -qa |grep docker卸载docker sudo yum remove docker-ce docker-ce-cli containerd.io sudo rm -rf /var/lib/docker sudo rm -rf /var/lib/contai…

算法的学习笔记—二叉树的深度(牛客JZ55)

&#x1f600;前言 在二叉树的相关操作中&#xff0c;计算树的深度是一个非常基础但十分重要的操作。本文将详细解析如何计算一棵二叉树的深度&#xff0c;并通过代码实现来展示具体的解决方案。 &#x1f3e0;个人主页&#xff1a;尘觉主页 文章目录 &#x1f49d;二叉树的深度…

了解 .NET 8 中的定时任务或后台服务:IHostedService 和 BackgroundService

IHostedService.NET 8 引入了使用和管理后台任务的强大功能BackgroundService。这些服务使长时间运行的操作&#xff08;例如计划任务、后台处理和定期维护任务&#xff09;可以无缝集成到您的应用程序中。本文探讨了这些新功能&#xff0c;并提供了实际示例来帮助您入门。您可…

HarmonyOS开发 - 本地持久化之实现LocalStorage实例

用户首选项为应用提供Key-Value键值型的数据处理能力&#xff0c;支持应用持久化轻量级数据&#xff0c;并对其修改和查询。数据存储形式为键值对&#xff0c;键的类型为字符串型&#xff0c;值的存储数据类型包括数字型、字符型、布尔型以及这3种类型的数组类型。 说明&#x…

同步电机不同电流参考方向下的功率计算

同步电机的功率计算有时候会看见两种表达方式&#xff1a; 当以发电机惯例&#xff0c;即电流方向输出时&#xff0c;功率计算式为&#xff1a; { P s 3 2 ( u s d i s d u s q i s q ) Q s 3 2 ( u s q i s d − u s d i s q ) \left\{\begin{array}{l} P_{\mathrm{s}}\fr…

PostgreSQL(十三)pgcrypto 扩展实现 AES、PGP 加密,并自定义存储过程

目录 一、pgcrypto 简介1.1 安装 pgcrypto 扩展1.2 pgcrypto 包含的函数 二、用法①&#xff1a;对称加密&#xff08;使用 AES、Blowfish 算法&#xff09;2.1 密钥2.2 密钥偏移量 三、用法②&#xff1a;PGP加解密3.1 什么是PGP算法&#xff1f;3.2 使用 GPG 生成密钥对3.3 列…

【AI大模型】深入解析 存储和展示地理数据(.kmz)文件格式:结构、应用与项目实战

文章目录 1. 引言2. 什么是 .kmz 文件&#xff1f;2.1 .kmz 文件的定义与用途2.2 .kmz 与 .kml 的关系2.3 常见的 .kmz 文件使用场景 3. .kmz 文件的内部结构3.1 .kmz 文件的压缩格式3.2 解压缩 .kmz 文件的方法3.3 .kmz 文件的典型内容3.4 .kml 文件的结构与主要元素介绍 4. 深…

豆包MarsCode Agent 登顶 SWE-bench Lite 评测集

大语言模型&#xff08;LLM&#xff09;能力正在迅速提升&#xff0c;对包括软件工程在内的诸多行业产生了深远影响。GPT-4o、Claude3.5 等 LLM 已经逐步展现出胜任复杂任务的能力&#xff0c;例如文本总结、智能客服、代码生成&#xff0c;甚至能够分析和解决数学问题。在这一…

为什么在网络中不能直接传输数据

为什么在网络中不能直接传输数据 原因 在网络中不能直接传输原始数据形式&#xff0c;主要有以下几方面原因&#xff1a; 数据表示的多样性&#xff1a;不同的计算机系统、编程语言和应用程序对数据的表示方式可能各不相同。例如&#xff0c;整数在不同的编程语言中可能有不同…

了解Java开发中的会话层

在现代Web应用开发中&#xff0c;会话管理是一个至关重要的概念。它涉及到如何在客户端和服务器之间保持用户状态信息&#xff0c;从而提供个性化、连续的用户体验。Java作为一种广泛使用的编程语言&#xff0c;在Web开发中扮演着重要角色&#xff0c;特别是在企业级应用中。了…

基于neo4j的课程资源生成性知识图谱

你是不是还在为毕业设计苦恼&#xff1f;又或者想在课堂中进行知识的高效管理&#xff1f;今天给大家分享一个你一定会感兴趣的技术项目——基于Neo4j的课程资源生成性知识图谱&#xff01;&#x1f4a1; 这套系统通过知识图谱的形式&#xff0c;将课程资源、知识点和学习路径…

一文掌握异步web框架FastAPI(五)-- 中间件(测试环境、访问速率限制、请求体解析、自定义认证、重试机制、请求频率统计、路径重写)

接上篇:一文掌握异步web框架FastAPI(四)-CSDN博客 目录 七、中间件 15、测试环境中间件 16、访问速率限制中间件,即限制每个IP特定时间内的请求数(基于内存,生产上要使用数据库) 1)限制单ip访问速率 2)增加限制单ip并发(跟上面的一样,也是限制每个IP特定时间内的请…

vue2结合echarts实现数据排名列表——前端柱状进度条排行榜

写在前面&#xff0c;博主是个在北京打拼的码农&#xff0c;工作多年做过各类项目&#xff0c;最近心血来潮在这儿写点东西&#xff0c;欢迎大家多多指教。 数据排名列表——图表开发&#xff0c;动态柱状图表&#xff0c;排名图 UI 直接搜到类似在线代码&#xff08;数据列表…

事务的原理、MVCC的原理

事务特性 数据库事务具有以下四个基本特性&#xff0c;通常被称为 ACID 特性&#xff1a; 原子性&#xff08;Atomicity&#xff09;&#xff1a;事务被视为不可分割的最小工作单元&#xff0c;要么全部执行成功&#xff0c;要么全部失败回滚。这意味着如果事务执行过程中发生…

交换机:端口安全与访问控制指南

为了实现端口安全和访问控制&#xff0c;交换机通常通过以下几种机制和配置来保护网络&#xff0c;防止未经授权的访问和恶意攻击。 01-端口安全 定义及功能 端口安全功能允许管理员限制每个交换机端口可以学习的MAC地址数量。 通过绑定特定的MAC地址到交换机的某一端口上&a…

二十二、Python基础语法(模块)

模块(module)&#xff1a;在python中&#xff0c;每个代码文件就是一个模块&#xff0c;在模块中定义的变量、函数、类别人都可以直接使用&#xff0c;如果想要使用别人写好的模块&#xff0c;就必须先导入别人的模块&#xff0c;模块名须满足标识符规则&#xff08;由字母、数…

MFC七段码显示实例

在MFC中添加iSenvenSegmentAnalogX控件&#xff0c;添加编辑框和按钮实现在编辑框中输入数字点击按钮后数字用七段码显示 1、在对话框中点击右键如下图添加控件和变量 2、在sevenDlg.h中添加代码 public: void ShowInd(int,double);3、在sevenDlg.cpp中添加代码 void CSe…

将 el-date-picker获取的时间数据转换成时间戳

在Vue.js中使用Element UI的el-date-picker组件时&#xff0c;你可以获取用户选择的日期并将其转换为时间戳。el-date-picker通常返回的是一个Date对象或一个格式化后的字符串&#xff08;取决于你如何配置它&#xff09;。下面是一个示例&#xff0c;展示了如何将el-date-pick…

攻防世界的新手web题解

攻防世界引导模式 1、disabled_button 好&#xff0c;给了一个按钮&#xff0c;第一道题目就不会做 看的wp<input disabled class"btn btn-default" style"height:50px;width:200px;" type"submit" value"flag" name"auth&q…

来源爬虫程序调研报告

来源爬虫程序调研报告 一、什么是爬虫 爬虫&#xff1a;就是抓取网页数据的程序。从网站某一个页面&#xff08;通常是首页&#xff09;开始&#xff0c;读取网页的内容&#xff0c;找到在网页中的其它链接地址&#xff0c;然后通过这些链接地址寻找下一个网页&#xff0c;这…