【C/C++ 数据结构】-八大排序之 冒泡排序快速排序

news2024/11/22 11:41:06

作者:学Java的冬瓜
博客主页:☀冬瓜的主页🌙
专栏:【C/C++数据结构与算法】
分享:那我便像你一样,永远躲在水面之下,面具之后! ——《画江湖之不良人》
主要内容:八大排序选择排序中的冒泡排序、选择排序(递归+非递归),理解快速排序一趟排序的三种算法:挖坑法,左右指针法,前后指针法。还有关于快排优化:三数取中法,小区间优化

在这里插入图片描述

在这里插入图片描述

文章目录

  • 一、冒泡排序
    • 1. 思路
    • 2. 复杂度
    • 3. 代码
  • 二、快速排序
    • 1. 思路:
      • 方法:挖坑法、左右指针、前后指针
      • 优化操作:三数取中法、小区间优化
    • 2. 复杂度
    • 3. 代码
      • 3.1 挖坑法 左右指针 前后指针(一趟排序)
      • 3.2 三数取中 小区间优化 递归实现排序
    • 4. 补充:测试排序性能方法
    • 5. 补充:快排非递归

一、冒泡排序

1. 思路

理解:这是排序中几乎最简单的一个排序。比如要把一组数从小到大排序,就是依次比较两数大小,大的数往后挪,直到最大数放在最后面这就完成了一次冒泡。然后最后边界减一,和之前一样的操作,直到完成n-1次冒泡。最重要的是:注意边界下标的控制!

2. 复杂度

时间复杂度:O(N^2)
如果使用优化版(使用一个变量标记在一趟排序中是否发生了交换,不发生交换,则表示这组数据刚好符合排序要求),且这组数刚好是按照要求,从小到大排的(和示例代码一致),那么时间复杂度达到O(N)

3. 代码

// 冒泡排序1(优化版)
// 时间复杂度:O(N^2)
void BubbleSort(int* arr, int size)
{
	for (int i = 0; i < size - 1; i++) {
		int exchange = 0;
		for (int j = 1; j < size - i; j++) {
			if (arr[j - 1] > arr[j]) {
				Swap(&arr[j - 1], &arr[j]);
				exchange = 1;
			}
		}
		if (exchange == 0) {
			break;
		}
	}
}
// 冒泡排序2
void BubbleSort(int* arr, int size)
{
	// 利用end控制末尾边界
	int end = size;
	while (end > 0) {
		for (int j = 1; j < end; j++) {
			if (arr[j - 1] > arr[j]) {
				Swap(&arr[j - 1], &arr[j]);
			}
		}
		end--;
	}
}

二、快速排序

1. 思路:

方法:挖坑法、左右指针、前后指针

  1. 挖坑法:随机或者选择开头第一个数做key,把右边比key小的数挪到左边,把左边比key大的数挪到右边,这样就找到了key的位置(即把key放入了该放的位置),然后左右在分【left,key-1】 key 【key+1,right】的区间去递归找到并放入每一个数到排序中该放的数。
  2. 左右指针:begin下标从左找比key大的数,end下标从右找比key小的数,然后交换位置,直到begin遇到end,和挖坑法很相似。
  3. 前后指针:cur下标在前,prev下标紧跟其后从左到右搜索。cur下标找到比key小的数时,先prev++,然后 Swap(&a[prev],a[cur]) ;会有两种情况:其一:cur就在prev后一个,prev++后赋值,是自己给自己赋值。其二:cur和prev之间隔着大于key的数,交换就是把cur下标所在的这个比key小的数和prev与cur之间比key大的数交换。结局就是比key小的数放在了前面,大的数移到了后面。

优化操作:三数取中法、小区间优化

  • 三数取中法对有序的有大量数据的数组很有作用,可以大大加大快速排序的效率。
    在这里插入图片描述

  • 小区间优化目的是:减少函数递归,从而减少栈帧的创建和销毁,提高效率,但是一般不是很明显。比如对长度100w的数组排序,最后三层就占了87.5w次递归。
    在这里插入图片描述

2. 复杂度

  • 时间复杂度:O(NlogN)。每次挖坑后区间减半,分两边去操作,一共需要排序N个数据,所以,需要选择key共logN次,每个数找它的位置时,要遍历整个数组(长度为N)。所以复杂度为O(NlogN)。

3. 代码

3.1 挖坑法 左右指针 前后指针(一趟排序)

// 一趟排序
// 法一:挖坑法
int FuncPart1(int* a, int left, int right)
{
	// 三数取中法
	int mid = getMidIndex(a + left, right - left + 1);
	Swap(&a[left], &a[left + mid]);  // 注意范围理解

	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];

	// 1、排序一趟的操作
	while (begin < end)
	{	// 2、右边找小
		while (begin < end && key <= a[end])  // 要加上begin<end的条件,如果从外面的while进入后,
			end--;                                 // 内层的while不判断就操作,会导致begin和end错开,从而出现错误排序
		//Swap(&a[pivot], &a[end]);  //注意:不能直接交换,不然交换时消耗大量时间就达不到快速的效果
		// 2.1、把坑位赋值为这个右边比key小的这个数,再更新坑位pivot
		a[pivot] = a[end];
		pivot = end;

		// 3、左边找大
		while (begin < end && a[begin] <= key)
			begin++;
		//Swap(&a[begin], &a[pivot]);   //注意:不能直接交换,不然交换时消耗大量时间就达不到快速的效果
		// 3.1、把坑位赋值为这个左边比key大的这个数,再更新坑位pivot
		a[pivot] = a[begin];
		pivot = begin;
	}

	// 4、把key这个数放进它的位置
	pivot = begin;
	a[begin] = key;

	return pivot;
}

// 法二:左右指针法
int FuncPart2(int* a, int left, int right)
{

	int mid = getMidIndex(a, right - left + 1);
	Swap(&a[left], &a[left + mid]);

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

	while (begin < end) {
		// 找小
		while (begin < end && a[keyi] <= a[end]) {
			end--;
		}
		// 找大
		while (begin < end && a[begin] <= a[keyi]) {
			begin++;
		}
		Swap(&a[begin], &a[end]);
	}
	Swap(&a[begin], &a[keyi]);

	keyi = begin;
	return keyi;
}

// 法三:前后指针法
int FuncPart3(int* a, int left, int right)
{
	int mid = getMidIndex(a, right - left + 1);
	Swap(&a[left], &a[left + mid]);

	int prev = left, cur = left + 1;
	int keyi = left;

	while (cur <= right) {
		if (a[cur] < a[keyi] && ++prev != cur) { // 注解:1只要cur的值小于keyi的值,prev就自增,即prev要标记左边最后比keyi小的数。
			Swap(&a[prev], &a[cur]);           //      2当进入if时,prev刚好在cur的后一个时,就是cur自己赋值给自己。
		}
		++cur;
	}
	// 注意:退出循环时,prev下标的值是最后一个比keyi值小的数,所以交换二者后,keyi就找到它的位置。
	Swap(&a[keyi], &a[prev]);

	keyi = prev;
	return keyi;
}

3.2 三数取中 小区间优化 递归实现排序

// 三数取中
int getMidIndex(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	int mid = (left + right) / 2;
	if (a[left] < a[mid]) {
		if (a[mid] < a[right]) {
			return mid;
		}
		//a[mid]>a[right]
		else if (a[left] < a[right]) { 
			return right;
		} 
		// a[left]<a[mid] a[mid]>a[right] a[left]>=a[right]
		else {
			return left;
		}
	}
	// a[left] >= a[mid]
	else {
		if (a[mid] > a[right]) {
			return mid;
		}
		// a[mid] < a[right]
		else if (a[right] > a[left]) {
			return left;
		}
		//a[left]>=a[mid] a[mid]<a[right] a[right]<=a[left]
		else {
			return right;
		}
	}
}

void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}
	int pivot = FuncPart3(a, left, right);
	 注意0:在数据量在一定范围内,无序情况下快排比堆排和希尔都快,但有序时,快排要慢很多。
	// 
	 分成区间[left,pivot-1] pivot [pivot+1,right],递归去实现子区间有序
	//QuickSort(a , left, pivot - 1);
	//QuickSort(a , pivot + 1, right);


	// 小区间优化(n<=10,使用直接插入排序,但是这个方法优化效率不明显)
	if (pivot - 1 - left > 10) {
		QuickSort(a, left, pivot - 1);
	}
	else {
		InsertSort(a + left, pivot - 1 - left + 1);
	}
	if (right - (pivot + 1) > 10) {
		QuickSort(a, pivot + 1, right);
	}
	else {
		InsertSort(a + pivot + 1, right - (pivot + 1) + 1);
	}
}

4. 补充:测试排序性能方法

// 测试性能
void TestOP()
{
	// 使用malloc申请新的空间,那么第二个排序就不会收到第一个排序结果的影响。否则,如果只使用传入的数组,排完一次后就有序了,那么其它排序前,数组就已经有序了。
	srand(time(NULL));  // 产生随机数
	int N = 1000000;
	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);


	for (int i = 0; i < N; ++i)
	{
		a1[i] = rand();
		//a1[i] = i;
		//a1[i] = N-i;
		a2[i] = a1[i];
		a3[i] = a1[i];
		a4[i] = a1[i];
		a5[i] = a1[i];
		a6[i] = a1[i];
		a7[i] = a1[i];
		a8[i] = a1[i];

	}

	long int begin1 = clock();  // 获取毫秒数
	InsertSort(a1, N);
	long int end1 = clock();

	long int begin2 = clock();
	ShellSort(a2, N);
	long int end2 = clock();

	long int begin3 = clock();
	SelectSort(a3, N);
	long int end3 = clock();

	long int begin4 = clock();
	HeapSort(a4, N);
	long int end4 = clock();

	long int begin5 = clock();
	BubbleSort(a5, N);
	long int end5 = clock();

	long int begin6 = clock();
	QuickSort(a6, 0, N - 1);
	long int end6 = clock();

	long int begin7 = clock();
	MergeSort(a7, N);
	long int end7 = clock();


	printf("直接插入:%ld ms\n", end1 - begin1);
	printf("希尔排序:%ld ms\n", end2 - begin2);
	printf("选择排序:%ld ms\n", end3 - begin3);
	printf("堆排序  :%ld ms\n", end4 - begin4);
	printf("冒泡排序:%ld ms\n", end5 - begin5);
	printf("快速排序:%ld ms\n", end6 - begin6);
	printf("归并排序:%ld ms\n", end7 - begin7);
}

5. 补充:快排非递归

  • 注意:对快排来说(不分递归非递归),逆序会比随机或者顺序慢很多,因为即使使用三数取中法,左右数字全都是需要交换的,复杂度虽然没有到O(N^2),但比O(NlogN)大很多。
  • 快速排序有了非递归,为什么还要实现非递归呢?非递归最根本的原因就是为了解决栈溢出的问题。排序的数据量很大时(比如1000w个数),递归的深度会很深,栈帧开销过大,这会让只有十几兆的栈空间不够用,导致栈溢出。
  • 下面的例子是借用数据结构的栈,来模拟实现快排。要看栈可以看这篇博客:栈和队列。
  • 另外,快排非递归也可以利用队列来实现,利用先进先出的规则。比如4 2 9 5 1 3 8,忽略三数取中法,利用栈或队列如下:
    在这里插入图片描述
// 非递归:
// 快速排序:
int FuncPart(int* a, int left, int right)
{
	 三数取中法
	int mid = getMidIndex(a + left, right - left + 1);
	Swap(&a[left], &a[left + mid]);  // 注意理解范围

	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];

	// 1、排序一趟的操作
	while (begin < end)
	{	// 2、右边找小
		while (begin < end && key <= a[end])  // 要加上begin<end的条件,如果从外面的while进入后,
			end--;                                 // 内层的while不判断就操作,会导致begin和end错开,从而出现错误排序
		//Swap(&a[pivot], &a[end]);  //注意:不能直接交换,不然交换时消耗大量时间就达不到快速的效果
		// 2.1、把坑位赋值为这个右边比key小的这个数,再更新坑位pivot
		a[pivot] = a[end];
		pivot = end;

		// 3、左边找大
		while (begin < end && a[begin] <= key)
			begin++;
		//Swap(&a[begin], &a[pivot]);   //注意:不能直接交换,不然交换时消耗大量时间就达不到快速的效果
		// 3.1、把坑位赋值为这个左边比key大的这个数,再更新坑位pivot
		a[pivot] = a[begin];
		pivot = begin;
	}

	// 4、把key这个数放进它的位置
	pivot = begin;
	a[begin] = key;

	return pivot;
}
void QurickSortNonR(int* a, int n)
{
	Stack st;
	StackInit(&st);
	StackPush(&st, n - 1);
	StackPush(&st, 0);
	while (!StackEmpty(&st)) {
		int left = StackTop(&st);
		StackPop(&st);

		int right = StackTop(&st);
		StackPop(&st);

		int Index = FuncPart(a, left, right);  // 用挖坑法,一趟排序

		// [left, index-1] index [index+1, right]
		// Push先入右,后入左,那Pop先出左
		if (Index + 1 < right) {  // 代表Index右边至少还有两数没排,继续入栈
			StackPush(&st, right);
			StackPush(&st, Index + 1);
		}
		if (left < Index - 1) {
			StackPush(&st, Index - 1);
			StackPush(&st, left);
		}
	}
}

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

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

相关文章

前端构建工具大盘点:gulp、webpack、vite、rollup、esbuild、snowpack、babel、parcel、swc、tsc

文章目录背景分类转译器打包器对比gulp VS webpackBundle vs Bundleless&#xff08;代表就是webpack VS vite&#xff09;其他比较个人理解总结官网背景 做前端也有好多年了&#xff0c;从最早的 jQuery 时代到现在的三大框架&#xff0c;这过程中用到了很多构建工具&#xf…

婴幼儿常见八大疾病及护理方法

在1岁之前&#xff0c;婴儿的体质还没有完全发育&#xff0c;很容易生病&#xff0c;大多数婴儿在1岁之后都会更好。今天&#xff0c;新的稀有婴儿育儿专家组织了一些婴儿最容易患的疾病和护理方法。1、新生儿黄疸宝宝出生后&#xff0c;你可能会注意到他的皮肤发黄。别担心&am…

Netty学习(三):Netty线程模型和代码示例

〇、前言网络编程的基本线程模型&#xff0c;详见&#xff1a;Netty学习&#xff08;二&#xff09;&#xff1a;线程模型一、工作原理简图Netty主要基于主从 Reactors 多线程模型&#xff08;如下图&#xff09; 做了一定的改进&#xff0c;其中主从Reactor 多线程模型有多个R…

11_MySQL数据处理(增删改)

1. 插入数据1.1 实际问题解决方式&#xff1a;使用 INSERT 语句向表中插入数据。1.2 方式1&#xff1a;使用VALUES使用这种语法一次只能向表中插入一条数据。情况1&#xff1a;为表的所有字段按默认顺序插入数据INSERT INTO 表名 VALUES (value1,value2,....);值列表中需要为表…

Windows安装Liberica JAVA8

在浏览器打开下载&#xff1a;Liberica JAVA。 点击链接"下载MSI"相对应的Microsoft Windows版本。 下载后完成后&#xff0c;验证的文件&#xff0c;通过比较其大小对你的驱动器和下载的页。 一个更加先进的方法来验证检验和在PowerShell&#xff1a; (Get-FileHas…

软件测试之测试环境--XAMPP

1. 测试环境 Linux下的环境搭建 LNMP: LinuxNginxMysqlphp项目 Windows下的环境搭建 WAMP: WindowsapacheMysqlphp项目 2. Xampp安装及使用 XAMPP包括Apache、MySQL、PHP、PERL&#xff0c;直接解压缩&#xff0c;没有复杂的安装过程&#xff0c;强烈推荐初学者使用。 到目前…

【C/C++基础知识点】输出n位斐波那契数列

目录 前言什么是斐波那契数列兔子的故事小知识点收尾前言 在软件行业已经有快十年,技术虽然一般般,但是足够应付额解决编程入门的相关问题! 都说十年磨一剑,积累到一定经验,是时候发挥自己的价值,给予入门的同行些许的帮助! 为什么要写收费专栏,其实原因很简单,时间就…

使用VNC远程连接Ubuntu - 内网穿透实现公网远程办公

写在前面&#xff1a;博主是一只经过实战开发历练后投身培训事业的“小山猪”&#xff0c;昵称取自动画片《狮子王》中的“彭彭”&#xff0c;总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域&#xff0c;如今终有小成…

PLSQL创建新用户并导入导出.dmp文件

一、登录管理员账号 用户名密码登录身份说明systemmanagerSYSDBA 或 NORMAL不能以 SYSOPER 登录&#xff0c;可作为默认的系统管理员syschange_on_installSYSDBA 或 SYSOPER不能以 NORMAL 登录&#xff0c;可作为默认的系统管理员scotttigerNORMAL普通用户aqadmaqadmSYSDBA 或…

目标检测论文阅读:RepPoints算法笔记

标题&#xff1a;RepPoints: Point Set Representation for Object Detection 会议&#xff1a;ICCV2019 论文地址&#xff1a;https://ieeexplore.ieee.org/document/9009032/ 官方代码&#xff1a;https://github.com/microsoft/RepPoints 作者单位&#xff1a;北京大学、清华…

Jetpack Compose 深入探索系列六:Compose runtime 高级用例

Compose runtime vs Compose UI 在深入讨论之前&#xff0c;非常重要的一点是要区分 Compose UI 和 Compose runtime。Compose UI 是 Android 的新 UI 工具包&#xff0c;具有 LayoutNodes 的树形结构&#xff0c;它们稍后在画布上绘制其内容。Compose runtime 提供底层机制和…

qsort快速排序的实现以及模拟实现qsort的功能(狠狠的拿捏)

当你为错过太阳而哭泣的时候&#xff0c;你也要再错过群星了。 --泰戈尔 目录 一.qsort快速排序的实现 二.模拟实现一个qsort功能的函数 一.qsort快速排序的实现 下面是 qsort() 函数的声明&#xff1a; void qsort(void *base, size_t nitems, size_t size, int (…

Java——电话号码的字母组合

题目链接 leetcode在线oj题——电话号码的字母组合 题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 题目示例…

高压功率放大器在压电驱动器的研究中的应用

实验名称&#xff1a;压电驱动器的电致振动特性研究研究方向&#xff1a;压电驱动器测试目的&#xff1a;旨在分析压电驱动器的电激励振动特性。以双晶压电悬臂梁为对象&#xff0c;基于能量法和热力学平衡方程推导了压电悬臂梁在电压激励下的强迫振动微分方程。利用自行搭建的…

Spring的核心基础——IOC与DI

文章目录一、Spring简介1 Spring介绍1.1 为什么要学1.2 学什么2 初识Spring2.1 Spring家族2.2 Spring发展史3 Spring体系结构3.1 Spring Framework系统架构图4 Spring核心概念问题导入4.1 核心概念二、IOC和DI入门1 IOC入门问题导入1.1 门案例思路分析1.2 实现步骤1.3 实现代码…

【计算机网络】HTTP

一、基础概念 请求和响应报文 客户端发送一个请求报文给服务器&#xff0c;服务器根据请求报文中的信息进行处理&#xff0c;并将处理结果放入响应报文中返回给客户端。 请求报文结构&#xff1a; 第一行是包含了请求方法、URL、协议版本&#xff1b;接下来的多行都是请求首…

大数据开发的工作内容与流程

大数据开发的工作内容与流程离线数据仓库开发实时流处理开发离线数据仓库开发 我们之后在做开发的时候&#xff0c;可能是选择某几个组件来使用。比如做数仓开发&#xff0c;可能就是用sqoop把数据抽到hdfs里&#xff0c;用spark或者mapreduce对这部分数据做一个清洗。 清洗的…

嵌入式开发--STM32H750VBT6开发中,新版本CubeMX的时钟问题,不能设置到最高速度480MHZ

嵌入式开发–STM32H750VBT6开发中&#xff0c;新版本CubeMX的时钟问题&#xff0c;不能设置到最高速度480MHZ 问题描述 之前开发的项目&#xff0c;开发环境是CubeMX6.6.1&#xff0c;H7系列的支持包版本是1.10.0。跑得没问题&#xff0c;最近需要对项目做修改&#xff0c;同…

vue学习(7)vuex的辅助函数封装(基于vue3)

简介&#xff1a; 封装了 mapState&#xff0c;mapGetters&#xff0c;mapActions&#xff0c;mapMutations&#xff0c;用更灵活的方式来使用vuex&#xff0c;主要使用的是vuex的createNamespacedHelpers方法&#xff0c;此方法是帮助重写以特定模块为主的辅助函数 createNa…

Spring Cloud(微服务)学习篇(四)

Spring Cloud(微服务)学习篇(四) 1.nacos实现服务之间传参数 1.1 在dto包(shop-sms-api项目)中创建SmsDTO类 package com.zlz.shop.sms.api.dto;import lombok.Data;Data public class SmsDTO {private String tel; }1.2 复制SmsDTO类到shop-sms-server项目的dto包下面 1.3 …