数据结构——排序算法第二幕(交换排序:冒泡排序、快速排序(三种版本) 归并排序:归并排序(分治))超详细!!!!

在这里插入图片描述

文章目录

  • 前言
  • 一、交换排序
    • 1.1 冒泡排序
    • 1.2 快速排序
      • 1.2.1 hoare版本 快排
      • 1.2.2 挖坑法 快排
      • 1.2.3 lomuto前后指针 快排
  • 二、归并排序
  • 总结

前言

继上篇学习了排序的前面两个部分:直接插入排序选择排序
今天我们来学习排序中常用的交换排序以及非常稳定的归并排序
快排可是有多种方法的,高速列车,即将发车,fellow me

一、交换排序

交换排序基本思想:
所谓交换,就是根据序列中两个记录键值的比较结果对换这两个记录在序列中的位置
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

1.1 冒泡排序

冒泡排序是一种最基础的交换排序。之所以叫做冒泡排序,因为每一个元素都可以像小气泡一样,根据自身大小一点一点向数组的一侧移动。这个算法在平常算法题中基本不用(因为太慢了),只能说具有教学意义。
就简单实现一下代码啦

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

冒泡排序的特性总结
时间复杂度: O(N^2)
空间复杂度: O(1)

1.2 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

快速排序实现主框架:
其实快排主要就是递归,把一个大区间不断划分成子区间

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	//_QuickSort用于按照基准值将区间[left,right)中的元素进行划分
	int meet = _QuickSort(a, left, right);
	QuickSort(a, left, meet - 1);
	QuickSort(a, meet + 1, right);
}

1.2.1 hoare版本 快排

算法思路 :
创建左右指针,确定基准值
从右向左找出比基准值小的数据,从左向右找出比基准值大的数据,左右指针数据交换,进入下次循环
其实就是确定一个数为基准值,然后根据基准值,把当前区域的数据分成两部分,左边小于基准值,右边大于基准值
然后再递归到分好的区域,继续重复操作,一个大问题划分成无数个一样的子问题。

讲到这里大概都知道怎么写啦,上代码

int _QuickSort(int* a, int left, int right)
{
	int keyi = left;   //  先定义区间第一个数为基准值   
	left++;  // left++  对除基准值以外的数据进行判断操作   
	while (left <= right)   //   只要left<right  就继续循环   
	{					//   我们这里right是找比基准值小的数据   left是找比基准值大的数据   然后进行调换  
		while (left <= right && a[right] > a[keyi])//  当右边的值大于基准值时  right--  直到找到小于基准值的再跳出循环
		{
			right--;
		}
		while (left <= right && a[left] < a[keyi])//  当左边的数据小于基准值时  left++  直到找到大于基准值的再跳出循环
		{
			left--;
		}    //   两个循环跳出后  left对应的数据大于基准值  right对应的数据小于基准值  
		//   对数据进行调换   这样就把小的放在左边  大的放在右边   
		if (left <= right)   
		{
			swap(a[right--], a[left++]);
		}
	}   //   当left>right的时候  交换最开始的基准值的位置 这个时候新的基准值就取right  
	swap(a[right], a[keyi]);
	return right;    //   到此  新的区间划分就处理好了   新的基准值返回就好啦  
}

第一个版本实现完毕了,可能会有些疑问

为什么跳出循环后right位置的值一定不大于key?
当left > right 时,即right走到left的左侧,而left扫描过的数据均不大于key,因此right此时指向的数据一定不大于key

可以试着自己模拟一下下面的流程图
在这里插入图片描述

问题2:为什么left 和 right指定的数据和key值相等时也要交换?
相等的值参与交换确实有一些额外消耗。实际还有各种复杂的场景,-假设数组中的数据大量重复时,相等也交换能进行有效的分割排序。

在这里插入图片描述

如果不相等才交换的话,假设数据全是相同的数据,那每次基准值只能找到初始基准值的下一个,时间复杂度会变成O(N^2)

快排是挺快的,但好东西总有缺陷

快排 :hoare版本的时间复杂度
划分区间递归的时间复杂度为 logn
每次区间内找新的基准值时间复杂度为 n
时间复杂度为 N*logN
但是在数据有序的时候,时间复杂度还是O(N^2),新的基准值只能找到key的下一个数字,划分区间的效率很低

1.2.2 挖坑法 快排

思路:
创建左右指针。首先从右向左找出比基准小的数据找到后立即放入左边坑中当前位置变为新的"坑"然后从左向右找出比基准大的数据找到后立即放入右边坑中当前位置变为新的"坑",结束循环后将最开始存储的分界值放入当前的"坑"中,返回当前"坑"下标(即分界值下标)。
就是先从右往左找,再从左往右找,不断循环,直到left>right,过程中数值一直在迭代交换,这个时候最后一个坑刚好放最开始挖的值。

相比hoare还是有差别的
在这里插入图片描述

int _QuickSort1(int* a, int left, int right)
{
	int hole = left;   //  找到第一个坑  
	int key = a[left];  //   把第一个坑保存起来   
	while (left < right)   //  left==right时跳出循环  最后一个坑
	{
		while (left < right && a[right] >= key)   //  从右开始往左找
		{
			right--;
		}
		a[hole] = a[right];   //  当right--的循环跳出后  这个时候right对应的值小于key  把当前right的值换到坑里
		hole = right;  				// right变成新的坑  
		while (left < right && a[left] <= key)  //  从左开始往右找  
		{
			left++;     
		}
		a[hole] = a[left];   //  当left++的循环跳出后  这个时候left对应的值大于key  把当前left的值换到坑里  
		hole = left;		//  left变成新坑  
	}
	a[hole] = key;   //  大循环结束后  left=right
	return hole;    //  这个新坑留给一开始的key值   返回新的基准值下边  
}

挖坑法完毕

挖坑法和hoare版本的时间复杂度一样 n*logn
但是在特殊情况也会有不好的地方 在数据有序的时候 时间复杂的还是会变成 O(N^2)

1.2.3 lomuto前后指针 快排

创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。
前后指针是我认为最好理解,也是代码最简单的一个
就是定义一个cur指针向前走,一个prev指针在后面跟着,cur找比基准值小的数据
在这里插入图片描述

在这里插入图片描述
话不多说,上代码

int _QuickSort1(int* a, int left, int right)
{
	int prev = left;   //  定义prev  cur  指针  
	int key = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[key] && ++prev != cur)   //  当cur对应的值小于key时,可以考虑将prev与cur对应的值交换
		{										//  但如果这个时候,cur刚好是prev的下一个值,时没有必要交换的
			swap(a[prev], a[cur]);				//  所以要判断  prev++与cur是否相等  
		}
		++cur;   //  每次循环  cur++一次   
	}
	swap(a[key], a[prev]); // 循环结束之后,prev对应的值时小于key的prev的下一个就是大于key的  这个时候调换key和prev的值
	return prev;			//		找到新的基准值下标返回
}								

仔细了解前后指针的流程,想必也会感觉到,当数据有序或者是全部相同时
前后指针也是O(N^2)的时间复杂度,比起hoare和挖坑法 缺陷又多了一个数据全部相同时
想想数据全部相同或者有序,其实也没有排序的需要了,除非是算法题卡了数据相同的样例
所以快排的三种方法还是可行的

快速排序特性总结:
时间复杂度: O(nlogn)
空间复杂度: O(logn)

快排的基本内容就到这里啦


二、归并排序

归并排序算法思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
在这里插入图片描述
底层核心还是递归,把一个大区间逐渐分成无数个小区间,(一个大问题分成无数个相同的子问题),快排和归并都是用到了递归,想想递归真的好用。

博客链接:合并两个有序数组

还有一个问题就是在递归到最后一层之后,怎么合并两个子区间让他们有序,这里我想到前面我们做过的习题,上面的链接供参考。

话不多说上代码

void _MergeSort(int* arr, int left, int right, int* tmp)  //  把大区间分配数个小空间  
{														// 两个小空间  排序成一个空间  用tmp接受  返回赋值给原数组 		
	if (left >= right)  //  递归出口   
	{
		return;
	}
	int mid = left + (right - left) / 2;//  分成两个区间  采用二分
	_MergeSort(arr, left, mid, tmp);     // 左区间
	_MergeSort(arr, mid + 1, right, tmp);// 右区间  
	//  递归处理之后  现在就是合并两个子区间  使他们有序  
	int begin1 = left, end1 = mid;       //  第一个区间的begin和end
	int begin2 = mid + 1, end2 = right;  // 第二个区间的  begin和end
	int index = begin1;    //  新的下标  对应tmp数组  
	while (begin1 <= end1 && begin2 <= end2)   //  合并两个数组的流程  不多赘述啦
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[index++] = arr[begin1++];
		}
		else
		{
			tmp[index++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)      //  有数组没有全部传给tmp的情况  
	{
		tmp[index++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = arr[begin2++];
	}
	for (int i = left; i <= right; i++)  //  赋值返回原数组 
	{
		arr[i] = tmp[i];
	}
}
void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);   //  我们这里传一个新开的tmp数组空间进去,辅助合并两个子区间
	_MergeSort(arr, 0, n - 1, tmp);
	free(tmp);
}

仔细回看,归并其实也不难,就是一个递归的处理,然后再合并两个区间而已,洒洒水啦

实话实说,归并稳定,时间复杂度一直是O(nlogn) 不管数据是否有序是否相同
归并排序特性总结:
时间复杂度: O(nlogn)
空间复杂度: O(n)

总结

都说快排是个大家伙,现在学完来看,也就一般般嘛
回顾今天学习的内容,从快排的三种方式,到递归合并的归并排序
差不多都是围绕递归在展开排序,虽然快排有些许缺陷,但影响不大
现在想想,归并排序,又稳又好,就是代码有点多 哈哈哈哈
今天的学习就到这里啦,下一篇将深究一下快排以及非递归实现快排,不要走开,小编持续更新中~~~~

有差错的地方还请各位指出,小编必然马不停蹄来修改~~~~

在这里插入图片描述

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

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

相关文章

Android基本概念及控件

Android是Google公司基于Linux平台开发的主要应用于智能手机及平板电脑的操作系统。 ART模式与Dalvik模式最大的不同在于:在启用ART模式后&#xff0c;系统在安装应用程序的时候会进行一次预编译&#xff0c;并先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都…

【JavaEE初阶 — 网络编程】Socket 套接字 & UDP数据报套接字编程

1. Socket套接字 1.1 概念 Socket 套接字&#xff0c;是由系统提供用于网络通信的技术&#xff0c;是基于TCP / IP协议的网络通信的基本操作单元。基于 Socket 套接字的网络程序开发就是网络编程。 1.2 分类 Socket套接字主要针对传输层协议划分为如下三类&#x…

Leecode刷题C语言之交替组②

执行结果:通过 执行用时和内存消耗如下&#xff1a; 代码如下&#xff1a; int numberOfAlternatingGroups(int* colors, int colorsSize, int k) {int res 0, cnt 1;for (int i -k 2; i < colorsSize; i) {if (colors[(i colorsSize) % colorsSize] ! colors[(i - …

科技惊艳:RFID技术引领被装物联网信息化革新

被装物联网信息化监控系统是一项错综复杂却成效斐然的解决方案&#xff0c;它巧妙地将物联网技术的先进性与装设备资源管理的实际需求相融合&#xff0c;实现了对被装设备资源的即时追踪、智能化调控以及资源的最优化配置。以下是对被装物联网的深度剖析与高端解读&#xff1a;…

360推出全新的生成式 AI 搜索产品:纳米搜索,要重塑搜索产品

【大力财经】直击互联网最前线&#xff1a;360 集团在 2024 年 11 月 27 日开发布会&#xff0c;重磅推出了一款全新的生成式 AI 搜索产品——纳米搜索&#xff0c;并且已经上架到苹果 App Store 以及应用宝等安卓应用商店&#xff0c;直接与百度、阿里夸克、秘塔 AI、Perplexi…

Android Deep Links 深度链接解析

在实现 Android 应用链接之前&#xff0c;请务必了解您可以在 Android 应用中创建的不同类型的链接&#xff1a;深层链接、网页链接和 Android 应用链接。 Android Deep Links 深度链接解析 一、什么是Deep Links&#xff1f;二、Deep Links的优势三、Deep Links的实现方式1. …

setter方法注入(Java EE 学习笔记07)

属性setter方法注入是Spring最主流的注入方法&#xff0c;这种注入方法简单、直观&#xff0c;它是在被注入的类中声明一个setter方法&#xff0c;通过setter方法的参数注入对应的值。 案例&#xff1a; ① 创建User2实体&#xff0c;配置setter方法 package com.lq.entities…

英语知识网站:Spring Boot技术构建

6系统测试 6.1概念和意义 测试的定义&#xff1a;程序测试是为了发现错误而执行程序的过程。测试(Testing)的任务与目的可以描述为&#xff1a; 目的&#xff1a;发现程序的错误&#xff1b; 任务&#xff1a;通过在计算机上执行程序&#xff0c;暴露程序中潜在的错误。 另一个…

2025蓝桥杯(单片机)备赛--扩展外设之UART1的原理与应用(十二)

一、串口1的实现原理 a.查看STC15F2K60S2数据手册: 串口一在590页&#xff0c;此款单片机有两个串口。 串口1相关寄存器&#xff1a; SCON:串行控制寄存器&#xff08;可位寻址&#xff09; SCON寄存器说明&#xff1a; 需要PCON寄存器的SMOD0/PCON.6为0&#xff0c;使SM0和SM…

利用Python爬取12306网站车次信息

前言 随着互联网技术的发展,网络爬虫成为了获取公开数据的强大工具之一。对于经常需要查询火车票信息的人来说,能够自己编写一个爬虫程序来自动获取并整理这些信息,无疑是一个非常实用的技能。本文将详细介绍如何使用Python爬取12306网站上的车次信息,包括获取站点对应城市…

React Hooks中use的细节

文档 useState useState如果是以函数作为参数&#xff0c;那要求是一个纯函数&#xff0c;不接受任何参数&#xff0c;同时需要一个任意类型的返回值作为初始值。 useState可以传入任何类型的参数作为初始值&#xff0c;当以一个函数作为参数进行传入的时候需要注意&#xff…

2024 TIP 论文 robust-ref-seg 复现过程

本篇是 2024 年 TIP 论文 Toward Robust Referring Image Segmentation 的复现过程。 特点是对不存在的目标不会进行错误分割&#xff0c;鲁棒性较高&#xff0c;其结果如图&#xff1a; 配置环境 根据论文给出的链接 robust-ref-seg 配置环境。 下载数据集 按照 README 指…

数据结构(初阶6)---二叉树(遍历——递归的艺术)(详解)

二叉树的遍历与练习 一.二叉树的基本遍历形式1.前序遍历(深度优先遍历)2.中序遍历(深度优先遍历)3.后序遍历(深度优先遍历)4.层序遍历&#xff01;&#xff01;(广度优先遍历) 二.二叉树的leetcode小练习1.判断平衡二叉树1&#xff09;正常解法2&#xff09;优化解法 2.对称二叉…

k8s集群增加nfs-subdir-external-provisioner存储类

文章目录 前言一、版本信息二、本机安装nfs组件包三、下载nfs-subdir-external-provisioner配置文件并进行配置1.下载文件2.修改配置 三、进行部署备注&#xff1a;关于镜像无法拉取问题的处理 前言 手里的一台服务器搭建一个单点的k8s集群&#xff0c;然后在本机上使用nfs-su…

C++ For Hot100

数组&#xff1a;数组是存放在连续内存空间上的相同类型数据的集合。 1. 两数之和 - 力扣&#xff08;LeetCode&#xff09; class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {vector<int> v;for(int i 0;i<nums.size…

高校宿舍节能用电现状及智慧监管平台构建

0 引言 在节能减排的大背景下&#xff0c;高校通过精细化宿舍用电管理&#xff0c;提升师生的节能节电意识等举措&#xff0c;能够显著提高电能资源的使用效率&#xff0c;并有效预防火灾等安全事故&#xff0c;确保师生的人身安全。因此&#xff0c;当前亟需加强对智慧监管平…

Spring Boot英语知识网站:开发策略

5系统详细实现 5.1 管理员模块的实现 5.1.1 用户信息管理 英语知识应用网站的系统管理员可以对用户信息添加修改删除以及查询操作。具体界面的展示如图5.1所示。 图5.1 用户信息管理界面 5.1.2 在线学习管理 系统管理员可以对在线学习信息进行添加&#xff0c;修改&#xff0…

Jmeter中的前置处理器

5&#xff09;前置处理器 1--JSR223 PreProcessor 功能特点 自定义数据处理&#xff1a;使用脚本语言处理请求数据&#xff0c;实现高度定制化的数据处理和生成。动态数据生成&#xff1a;在请求发送前生成动态数据&#xff0c;如随机数、时间戳等。变量设置&#xff1a;设置…

华为鸿蒙内核成为HarmonyOS NEXT流畅安全新基座

HDC2024华为重磅发布全自研操作系统内核—鸿蒙内核&#xff0c;鸿蒙内核替换Linux内核成为HarmonyOS NEXT稳定流畅新基座。鸿蒙内核具备更弹性、更流畅、更安全三大特征&#xff0c;性能超越Linux内核10.7%。 鸿蒙内核更弹性&#xff1a;元OS架构&#xff0c;性能安全双收益 万…

EG3D: Efficient Geometry-aware 3D Generative Adversarial Networks 学习笔记

1 Contributions 混合显式-隐式网络架构&#xff1a;提出了一种 Tri-plane 的3D表征方法&#xff0c;结合显式体素网格与隐式解码器的优点 速度快&#xff0c;内存效率高&#xff1b; 支持高分辨率生成&#xff0c;保持3D表征的灵活性和表达能力。与纯显式或隐式方法相比&#…