数据结构之排序【归并排序和快排的顶级优化和快排的三种原理的实现及分析】 内含动态演示图

news2025/1/17 1:45:58

文章目录

  • 引言:
    • 1.归并排序(MergeSort)
    • 2.快速排序的优化(顶级优化)
    • 3.快速排序的三种思路的代码实现及分析
    • 4.归并排序和快排第3原理的测试

引言:

刚刚去回顾了一下递归实现的几个小代码,感觉递归真的是很神奇的一个东西,现在感觉递归确实也是比较不好理解的东西,所以这边我提供几个好的学习和理解递归的方法,递归无非就是两种,一种是求出递归的递归,另一种是起作用的递归函数的递归,两种是有一定的区别的。我们先讲一下要求出递归的递归,这种就类似于用递归求一个数的阶乘,必须要从头开始算,算出了头才可以算尾,这个就叫要求出递归的递归,学习这种递归首先就是要弄明白这个递归的原理,然后再在纸上把每一个递归的值给算出来,当然是要从最头上的那个开始算,另一种就是不需要算的递归,只要可以发挥出函数的作用就可以了,这种递归是比较的抽象的,因为这种作用型递归函数都是非常长的一串代码,所以当我们要理解这种递归的时候,我们可以画出它的递归展开图,就是一幅图一幅图的画,递归一次你就画一幅图,并且在画图理解的同时可以调试代码,按照图来调试代码,一遍不行再一遍,直到理解(这时想要理解这种递归的唯一方法),所以递归可以说是非常的不好搞。现在北京时间 2022/12/27/0:43 ,今天可以看出是超出了平常写博客的时间,但是我们还是要坚持写博客,并且我刚刚看了一下我最开始写的博客,我觉得当时的博客写的很有味道,现在的博客总感觉没什么味道了,以前写博客每一个字,都是肺腑之言一般,现在的博客除了引言中的内容,好像大部分都有一些不够活灵活现的感觉,以前的每一句话写的真的都很好玩的感觉,就算是那种死板的语言也可以被我们自己理解然后写的非常的有趣搞笑,不想现在的,有一些刻板,就像是以前都在写真心话,现在写的都是客套话的感觉,并且有一种以前写的博客是真真实实的写给自己看的,而现在的博客更不像是不是写给自己看的;总感觉现在的博客和以前区别很大,可能是我真的经历了吧!经历了改变了,以前刚开始写博客,写完之后睡觉好像都是想住别人在浏览你的博客,然后你的解释是如何如何的好,语句是多么多么的清楚明了,自己在句里行间写了多少多少的细节在里面,然后别人发现这些细节的时候是怎样的感觉,之类的想法,当时好像都会有,但是现在的我,好像……,可能是明白了CSDN的推荐和不推荐,关注和不关注之类的问题和当今时代文章好像确实是没有人看,更没有人会一个字一个字的仔细看,并且自己作为一个小白,写的内容也都是最基本的,好像确实是不需要看之类的这一系列额想法在里面,最终导致了我们博客风格的改变吧!行文来到这个位置,我接上一篇博客分享一下我的昨晚睡觉体验(如我意料之中,是我许久以来睡的最舒服的一次),缩小了位置,并不会影响我的睡觉体验,但是却可以让我睡的暖和,…… ,天大寒,码字不易,引言就这样吧!今天我们讲一下什么是归并排序和快速排序的另外两种思路。

1.归并排序(MergeSort)

时间复杂度:O(N*logN)

1.1 归并排序原理:我们可以假设一个数组它的左半区间有序 右半区间也有序 ,然后在这个前提之下,然后我们使用归并算法:依次比较左半区间和右半区间中的元素然后取小的元素放到新创建的临时数组中。

按照归并排序的原理,此时我需要有两个新的个区间,如何获得两个区间呢?就是将int mid = (left + right) / 2; 这样我们就可以利用这个中间值进行一个数组分成两个区间,有了区间我们此时就假设左半区间[ left , mid ] 和右半区间 [ mid+1 ,right ] 是有序的,那么我们就可以进行归并,让我的左半区间和右半区间有序(只有这样,才可以进行我的归并算法实现),分治递归实现。

图示如下:

在这里插入图片描述

问题:归并之前,左右区间没有序?怎么办?
此时这个问题就涉及到我们归并排序中的递归问题
所以想要解决这个问题就需要我们进行一个分治递归的思想,从而实现我们的代码
所以我们此时如何使我的左半区间和右半区间有序呢?
分治递归:

	_MergeSort(arr, left, mid, tmp);//假设我使用归并算法已经把左半区间排成有序了
	_MergeSort(arr, mid + 1, right, tmp);//假设我使用归并算法已经把半右区间排成有序了

这样我们就可以让我们的一个数组的左半区间和右半区间都有序了,此时就可以进行归并排序了

具体动图如下:

在这里插入图片描述
1.2 归并排序代码实现

#include<stdlib.h>
void _MergeSort(int* arr, int left, int right, int* tmp)
{
	if (left >= right)//递归条件
	{
		return;
	}
	int mid = (left + right) >> 1;
	//分治递归
	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid + 1, right, tmp);

	//归并算法的代码实现:
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		} 
	}
	while (begin1 <= end1)
	{
		tmp[index++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = arr[begin2++];
	}

	//将新数组中的元素拷贝回原数组
	int i;
	for (i = left; i <= right; i++)
	{
		arr[i] = tmp[i];
	}

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

	free(tmp);
}

注意点: 为了防止待会递归这个函数的时候每一次都还要重新malloc一块空间,所以我们就要把这个函数的作用外包到外面去,不然每次都要malloc一块空间效率下降,所以我就可以在外部重新写一个这个函数的子函数void _MergeSort(int* arr, int left, int right, int* tmp) 并且此时的这个函数其实还是MergeSort函数,只是多了一个 _ 而已,_ 表示的就是子函数的意思

2.快速排序的优化(顶级优化)

(注释详细)

学到这个位置我们意识到了,快排是有一定的缺陷的,当我转参的数组是一个有序的数组的时候(例:1 2 3 4 5 6 7 8 9 ),想要使用快排进行排序(因为快排的原理,每次都是取arr[0]或arr[end-1]),所以就会导致第一次是N,第二次是N-1,……,最后0,然后等差数列相加(就会导致快排的时间复杂度是:O(N^2))
所以快排是有一定的缺陷的,所以为了避免这个缺陷(我们就要对key的值进行优化),就会使用一个叫三数取中的方法来获取key的值:所以我就要对这个快排的代码就行改进

第一个优化:快排的三数取中法优化, 函数如下int GetMidIndex(int* arr, int left, int right)
第二个优化:小区间优化

int GetMidIndex(int* arr, int left, int right)
{
	//三数取中法代码
	int mid = (left + right) >> 1;//这句代码=> int mid = (left + right)/2; 只是移位的效率会高一点点

	//下面的这些if条件的判断就是为了取到三个数中不是最大也不是最小的那个数而已
	if (arr[left] < arr[mid])
	{
		if (arr[mid] < arr[right])
		{
			return mid;
		}
		else if(arr[left]>arr[right])//程序来到这个位置就是说明,此时我的mid是我的最大值,然而我的目的是为了找到中间的那个值,所以这边我就比较一下left和right就行,大的那个就是中间值
		{
			return left;
		}
		else
		{
			return right;//程序来到这里就说明right大,那么right就是中间值,是中间值就return
		}
	}
	else  //此时这个else的意思就是:arr[left] > arr[mid]
	{
		if (arr[left] < arr[right])//这边三数取中的写法很多,可以有不一样的写法,不怕
		{
			return left;
		}
		else if (arr[mid] < arr[right])
		{
			return right;
		}
		else
		{
			return mid;
		}
	}

}
void QuickSort(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);//下面的代码基本不变,就是这两步用来获取我的中间值就行了

	int begin = left;
	int end = right;
	int pivot = begin;
	int key = arr[begin];
	while (begin < end)
	{
		while (begin < end && arr[end] >= key)
		{
			end--;
		}
		arr[pivot] = arr[end];
		pivot = end;

		while (begin < end && arr[begin] <= key)
		{
			begin++;
		}
		arr[pivot] = arr[begin];
		pivot = begin;
	}

	pivot = begin;
	arr[pivot] = key;

	//此时代码来到这个位置就有一些不好的地方(比如刚刚我们使用了三数取中的函数,如果此时需要排序的数据非常大(1000000),有这么多个数据,此时如果要一直调用三数取中的函数就会导致效率问题)
	//并且当我们需要排序的数据剩下的不是非常多的时候(此时正是调用次数最多的时候因为快排是一个logN的排序 越到后面,执行次数越多(例:2^19 2^18)),所以此时我们可以来一个小区间优化
	//就是当需要排序的数据没有那么多时,我们就不使用快速排序,我们使用直接插入排序就行
	//原递归代码:
	//QuickSort(arr, left, pivot - 1);
	//QuickSort(arr, pivot + 1, right);
	//所以优化代码如下:
	if (pivot - 1 - left > 10)
	{
		QuickSort(arr, left, pivot - 1);
	}
	else
	{
		InsertSort(arr + left, pivot - 1 - left + 1);
	}

	if (right - (pivot + 1) > 10)
	{
		QuickSort(arr, pivot + 1, right);
	}
	else
	{
		InsertSort(arr + pivot + 1, right - (pivot + 1) + 1);
	}

}

3.快速排序的三种思路的代码实现及分析

3.1 快排的第一种思想挖坑法

int PartQuickSort1(int* arr, int left, int right)//此时这个表示的是一个单趟排序的挖坑法快速排序方法
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);//下面的代码基本不变,就是这两步用来获取我的中间值就行了

	int begin = left;
	int end = right;
	int pivot = begin;
	int key = arr[begin];
	while (begin < end)
	{
		while (begin < end && arr[end] >= key)
		{
			end--;
		}
		arr[pivot] = arr[end];
		pivot = end;

		while (begin < end && arr[begin] <= key)
		{
			begin++;
		}
		arr[pivot] = arr[begin];
		pivot = begin;
	}

	pivot = begin;
	arr[pivot] = key;

	return pivot;
}

3.2 快排的第二种思路的实现(前后指针法)

int PartQuickSort3(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);

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

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

	return prev;

}

3.3 快排的第三种方法的实现(挖坑法的变形(左右指针法))

理解:注释很详细


int PartQuickSort2(int* arr, int left, int right)
{
	int index = GetMidIndex(arr, left, right);
	Swap(&arr[left], &arr[index]);

	int begin = left;
	int end = right;
	int keyi = begin;

	//左右指针法的快排
	while (begin < end)//表示两指针找元素,直到相遇为止
	{
		while (begin < end && arr[end] >= arr[keyi])//就是在右边找比key小的数
		{
			end--;        //注意点:记得要有等号,因为如果没有等号就可能会死循环,因为如果左边和右边,两边同时找到一个和keyi相等的数(进不去循环,就导致end和begin不变),此时就会导致这两个数相等的数一直换过来换过去,就会导致死循环
		}
		while (begin < end && arr[begin] <= arr[keyi])
		{
			begin++;
		}
		//程序来到这个位置就说明我已经找到了比key大的数和比key小的数了,此时只要把这两个数给交换一下,然后我就完成了快排的单趟排序了(剩下的就交给递归去搞定就行了)

		Swap(&arr[begin], &arr[end]);//交换完,我就完成了快排的单趟排序了(剩下的就交给递归去搞定就行了)
	}

	Swap(&arr[begin], &arr[keyi]);//这步就是为了把我的keyi这个关键位置给放到中间的那个位置(同时也是正确的那个位置)

	return begin;//就是返回中间位置(虽然可能不是中间,但是就是要中间的意思)

}

2.1 快排的三种原理实现测试


void QuickSort2(int* arr, int left, int right)
{
	if (left >= right)
	{
		return;
	}
    //三种思路的测试(不可以同时调用)
	int keyIndex = PartQuickSort1(arr, left, right);
	int keyIndex = PartQuickSort2(arr, left, right);
	int keyIndex = PartQuickSort3(arr, left, right);

	if (keyIndex - 1 - left > 10)
	{
		QuickSort2(arr, left, keyIndex - 1);
	}
	else
	{
		InsertSort(arr + left, keyIndex - 1 - left + 1);
	}

	if (right - (keyIndex + 1) > 10)
	{
		QuickSort2(arr, keyIndex + 1, right);
	}
	else
	{
		InsertSort(arr + keyIndex + 1, right - (keyIndex + 1) + 1);
	}
}

4.归并排序和快排第3原理的测试

在这里插入图片描述

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

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

相关文章

C# StringBuilder

StringBuilder位于命名空间System.Text下&#xff0c;使用前需引入 using System.Text; StringBuilder的构造 new StringBuilder(string value) StringBuilder sb1 new StringBuilder("www.abc.com"); 利用构造函数创建一个值为“www.abc.com”的StringBuilder…

解决Ubuntu不能上网以及无法远程连接Ubuntu

本文环境 物理机OS&#xff1a; Windows10 专业版 虚拟机平台&#xff1a; VMware Workstation 16 Pro 虚拟机OS&#xff1a; Ubuntu 20.04 相信大家在使用Ubuntu中也有遇到不能上网&#xff0c;我也是尝试了很多的方法都不行&#xff0c;终于找到了一种可行的方法。 步骤…

测试开发应该具备的六大能力

前言 前几天一个前同事找我聊了个问题&#xff1a;一个好的测试开发同学需要具备哪些能力&#xff1f;我思考了一下&#xff0c;给了他如下答复&#xff1a; 从我工作中接触到的测试开发&#xff0c;以及面试测试开发候选人时问的问题&#xff0c;我将自己对测试开发这个岗位…

Chrome插件开发

1.什么是 Chrome 插件 谷歌浏览器插件是一种小型的定制浏览器体验的程序&#xff0c;通过插件可以自定义浏览器的一些行为来适合个人的需求&#xff0c;例如上面的查看服务器状态插件。 在应用商店中下载下来的插件基本上都是以.crx 为文件后缀&#xff0c;该文件其实就是一个…

实验一 课本第三章MongoDB数据库操作3.1-3.7

一、实验目的&#xff1a; 掌握MongoDb的部署 熟悉数据库和集合操作 二、实验环境&#xff1a; 一台运行的计算机 Linux平台 SecureCRT平台 三、实验内容&#xff1a; 3.1MongoDB部署 1.MongonDb部署&#xff08;windows平台&#xff09; &#xff08;1&#xff09;下载Mongo…

Node环境安装

Node的版本管理工具工具介绍gnvm官网指出特色的地方安装验证配置与使用配置文件内容命令使用nvm安装脚本命令下载请求文件下载验证配置文件使用n安装使用Fast Node Manager (fnm)安装使用工具介绍 本文介绍四款 Node 版本管理工具&#xff0c;用于下载和切换对应的 Node 与 Npm…

【金猿人物展】极盾科技CEO丁杨:让数据安全回归场景、业务和价值

‍丁杨本文由极盾科技CEO丁杨撰写并投递参与“数据猿年度金猿策划活动——2022大数据产业趋势人物榜单及奖项”评选。‍数据智能产业创新服务媒体——聚焦数智 改变商业我们的数据安全&#xff0c;还是以前的安全么&#xff1f;传统数据安全方案依赖网络和数据库安全能力进行围…

2022年安徽建筑八大员(标准员)考试试题及答案

百分百题库提供建筑八大员&#xff08;标准员&#xff09;考试试题、建筑八大员&#xff08;标准员&#xff09;考试真题、建筑八大员&#xff08;标准员&#xff09;证考试题库等,提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 22.某设计单位对承接…

Vue2.x + Echarts实现知识图谱数据渲染

代码案例数据写死了&#xff0c;后端Java可使用SpringBootNeo4j查询数据返回。 <template><div id"myChart"></div> </template><style> #myChart {width: 100%;height: 1000px; } </style> <script>export default {nam…

视频号小店是什么?如何开通视频号小店API?

微信视频号于2022年7月正式推出“视频号小店“服务&#xff0c;为商家提供商品信息服务、商品交易&#xff0c;支持商家在视频号运营电商。目前视频号小店有个体工商户或企业资质的商家进行开店&#xff0c;企业店需要企业营业执照认证、个体工商户则需要个体工商户营业执照认证…

Android设计模式详解之模板方法模式

前言 定义&#xff1a;定义一个操作中的算法的框架&#xff0c;而将一些步骤延迟到子类中&#xff0c;使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤&#xff1b; 使用场景&#xff1a; 多个子类有公有的方法&#xff0c;并且逻辑基本相同时&#xff1b;…

Linux环境下挂载外接硬盘

一. 前言 调试ARTIK过程中&#xff0c;首次外接硬盘不会自动挂载&#xff0c;需要将硬盘挂载到系统文件夹下方能读取硬盘内容&#xff0c;因此对于Ubuntu系统下挂载硬盘和开机自动挂载外接硬盘配置的方法进行总结。 二. 挂载外接硬盘步骤 通过命令 fdisk -l 查看硬盘资源信息…

Hive+Spark离线数仓工业项目--ODS层及DWD层构建(2)

ODS层构建&#xff1a;代码导入 目标&#xff1a;实现Python项目代码的导入及配置 实施 Oracle本地驱动目录**&#xff1a;将提供的**instantclient_12_2**目录放入D盘的根目录下 PyHive本地连接配置&#xff1a;将提供的CMU目录放入C盘的根目录下 auto_create_hive_table包…

Java中的Map集合体系

Map集合体系Map集合的概述Map集合体系特点Map集合常用APIMap集合的遍历方式&#xff1a;方式一&#xff1a;键找值方式二&#xff1a;键值对方式三&#xff1a;lambda表达式Map集合的实现类HashMapMap集合的实现类TreeMap集合嵌套Map集合的概述 Map集合概述和使用&#xff1a;…

Java 基础:变量、操作符、代码块和控制流

目录 一、变量&#xff1a;Variables 1、基本数据类型 2、数组 二、操作符/运算符 Operators 三、表达式、语句和代码块 四、程序控制流 一、变量&#xff1a;Variables Java 定义了以下几种变量&#xff1a; 实例变量/成员变量&#xff08;非静态字段&#xff09;&…

Docker+NETCore系列文章(五、推送自制镜像到Docker Hub、阿里云镜像仓库)

推送镜像到Docker Hub镜像仓库 1、访问Docker Hub&#xff1a;https://hub.docker.com/&#xff0c;注册并登陆Docker。 2、使用docker pull hello-world命令拉取hello-workld镜像。 [rootVM-0-6-centos ~]# docker pull hello-world Using default tag: latest latest: Pul…

微服务架构 VS 单体架构

在软件行业&#xff0c;微服务架构是一种重要的发展趋势。这一趋势&#xff0c;不仅仅是对企业内的IT信息系统建设&#xff0c;甚至在企业向数字化转型方面&#xff0c;都有着深远的影响。微服务架构与传统的单体软件架构代表着IT产业处理软件开发方式的一个根本性转变&#xf…

【C++11】异常

&#x1f308;1.C语言传统处理错误的方式 在讲解C的异常机制之前我们先来复习一下传统的处理错误的方式。 传统的错误处理机制&#xff1a; 1.终止程序、如assert , 缺陷&#xff1a;用户难以接受。如发生内存错误&#xff0c;除0错误时就会终止程序。2.返回错误码、缺陷&…

Apache POI导入导出excel文件实战

文章目录前言技术栈1、引入依赖2、导入代码实现3、导出代码实现3.1、准备导出文件模板3.2、导出代码实现4、代码实现解释5、常见问题前言 这两天公司项目业务提出需求&#xff0c;要求在前端上传excel文件然后解析展示&#xff0c;因此写篇文章记录一下实现。 技术栈 spring…

抖音小程序实践三:接口开发指南

通过官方文档可以更系统的学习到所有的接口&#xff0c;我这边罗列一下我自己用到测试过的接口供大家参考。 前端-小程序对接官方文档&#xff1a;https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/api/open-interface/user-information/tt-get-user-info服务端-小…