【数据结构】冒泡排序、快速排序(递归,非递归)、归并排序(递归,非递归),七大排序比较,

2023-11-13

冒泡排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
在代码中添加了exchange是为了提高一下数据在有序情况以下的效率。
请添加图片描述

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

冒泡排序的特性总结

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
这里一共有几个不同的写法:hoare版本、 挖坑法、前后指针版本
hoare版本
请添加图片描述

void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//递归结束
	{
		return;
	}
	int left = begin;
	int right = end;
	int keyi = begin;
	while (left < right)
	{
		while (left < right && a[right] >= a[keyi])
		{
			right--;
		}

		while (left < right && a[left] <= a[keyi])
		{
			left++;
		}
		swap(&a[right], &a[left]);
	}
	swap(&a[left],&a[keyi]);
	keyi = left;
	QuickSort(a,begin,keyi-1);
	QuickSort(a, keyi+1,end);
}

上面的代码,可以计算出在理想状态下时间复杂度是O(N*logN),和希尔、堆排是同一量级的,但是在不是理想状态下(数据处于有序,或者key处于在数据最大或者最小时)时间复杂度是O(N^2),在不理想状态下也就相当于冒泡排序。而且快排使用到了递归,使用了递归我们就必须要注意对栈帧的使用,防止栈溢出。
以上的问题使用两种方法来解决:三数取中、小区间优化。
三数取中:很好的解决了key处于最大值或者最小值的问题,也就避免了时间复杂度出现O(N^2)的情况。
小区间优化:因为在之前我们也学习过树的结构,最低层的数据就占全部数据的一半,所以我们在大区间使用递归,小区间直接使用插入排序,为了减少递归使用的次数,从而造成栈溢出的问题。

int GetMidIndex(int* a, int begin, int end)
{
    //三数取中
	int mid = (begin + end) / 2;
	if (a[end] > a[mid])
	{
		if (a[mid] > a[begin])
		{
			return mid;
		}
		else if(a[begin]>a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
	else
	{
		if (a[begin] > a[mid])
		{
			return mid;
		}
		else if(a[end]>a[begin])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//递归结束
	{
		return;
	}
	//小区间优化
	if ((end - begin + 1) < 15)
	{
		// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a+begin, end - begin + 1);
	}
	else
	{
		//三数取中,对快排进行优化
		int mid = GetMidIndex(a, begin, end);
		swap(&a[begin], &a[mid]);
		int left = begin;
		int right = end;
		int keyi = left;
		while (left < right)
		{
			while (left < right && a[right] >= a[keyi])
			{
				right--;
			}
			while (left < right && a[left] <= a[keyi])
			{
				left++;
			}
			swap(&a[right], &a[left]);
		}
		swap(&a[left], &a[keyi]);
		keyi = left;
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

挖坑法

挖坑法的思路是:同样的用三数取中求出keyi,也就是左边的第一个数据,再用一个变量将第一个设为坑,从右开始找出小于keyi的值,将该数放到坑中,再将此位置从新设为坑,然后再从左边开始遍历,找出大于keyi的值,以此类推,最后相遇在坑上,将keyi放在坑中,在依次遍历。
请添加图片描述

//挖坑法
int PartSort2(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	swap(&a[begin], &a[mid]);
	int left = begin;
	int right = end;
	int keyi = a[left];
	int hole = left;
	while (left < right)
	{
		while (left < right && a[right] >= keyi)
		{
			right--;
		}
		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= keyi)
		{
			left++;
		}
		a[hole] = a[left];
		hole = left;
	}
	a[hole] = keyi;
	return hole;
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//递归结束
	{
		return;
	}
	//小区间优化
	if ((end - begin + 1) < 15)
	{
		// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a+begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort2(a,begin,end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

前后指针法

前后指针法思路是:从最左边设置两个前后指针,curprev的前一个,cur先移动碰到比keyi小的数就将++prev进行交换,直至cur到达尾部。再将keyiprev位置的值进行交换再将prev位置设置为新的keyi
请添加图片描述

//前后双指针法
int PartSort3(int* a, int begin, int end)
{
	int mid = GetMidIndex(a, begin, end);
	swap(&a[begin], &a[mid]);
	int keyi = begin;
	int prev = begin;
	int cur = begin + 1;
	while (cur <= end)
	{
		if( ++prev!=cur&& a[cur]<a[keyi])
		{
			swap(&a[prev], &a[cur]);
		}
		
		cur++;
	}
	swap(&a[prev], &a[keyi]);
	keyi = prev;
	return keyi;
}


void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//递归结束
	{
		return;
	}
	//小区间优化
	if ((end - begin + 1) < 15)
	{
		// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a+begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a,begin,end);
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}

非递归写法

上面已经介绍完递归的写法,因为一提到递归的问题首先就应该想到的栈溢出的问题,所以就介绍了非递归的写法。
非递归思路:这里我们借用到了栈(当然不是只能使用栈,队列也是可以的,只是用栈更加符合递归的流程)将数据两头的数据进行入栈,还是接入上面的三种方法(hoare,挖坑法,前后指针)然后取到keyi分割为两部分继续进行入栈。

//快排非递归
void QuickSortNonR(int* a, int begin, int end)
{
	ST st;
	StackInit(&st);
	StackPush(&st,begin);
	StackPush(&st,end);
	while (!StackEmpty(&st))
	{
		int right=StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);
		int keyi = PartSort3(a, left,right);
		if (keyi + 1<right)
		{
			StackPush(&st, keyi+1);
			StackPush(&st, right);
		}
		if (keyi - 1 > begin)
		{
			StackPush(&st, begin);
			StackPush(&st, keyi-1);
		}
	}
	StackDestroy(&st);
}

解决数据当中有大量重复数据,或者全是相同数据效率的问题

OJ链接

我们上面提供的代码已经可以解决大部分的问题了,但是有时在写OJ的情况下会发现再碰到一些特殊用例并不能高效的解决,也就不能通过测试用例。上面的代码时间复杂度是O(N*logN)但是在特殊情况下时间复杂度是O(N^2),所以提出了解决的方法,在我们上面应用的方法可以把他们称为两路划分,而我们要解决的问题的方法称为三路划分
解题思路:将数据分为三部分,前面是小于keyi的而中间放和keyi相等的,后面放大于keyi的,这样如果再碰到数据全是相同的也能很好的解决。
在这里我们再继续使用三数取中来完成OJ是行不通的了,可能大量区间选keyi让你选到比较小的或者比较大的,导致性能下降,座椅解决方案就是:结合随机选keyi优化。

void swap(int* a, int* b)
{
	int temp = 0;
	temp = *a;
	*a = *b;
	*b = temp;
}

int GetMidIndex(int* a, int begin, int end)
{
	//int mid = (begin + end) / 2;
    int mid=begin+rand()%(end-begin);
	if (a[end] > a[mid])
	{
		if (a[mid] > a[begin])
		{
			return mid;
		}
		else if(a[begin]>a[end])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
	else
	{
		if (a[begin] > a[mid])
		{
			return mid;
		}
		else if(a[end]>a[begin])
		{
			return end;
		}
		else
		{
			return begin;
		}
	}
}
void QuickSort(int* a, int begin, int end)
{
	if (begin >= end)//递归结束
	{
		return;
	}
	//小区间优化
	if ((end - begin + 1) < 15)
    {
	// // 	// 小区间用直接插入替代,减少递归调用次数
		InsertSort(a+begin, end - begin + 1);
	}
	else
	{
		int mid=GetMidIndex(a,begin,end);
        swap(&a[mid],&a[begin]);
        int left=begin;
        int right=end;
        int keyi=a[begin];
        int cur=begin+1;
        while(cur<=right)
        {
            if(a[cur]>keyi)
            {
                swap(&a[cur],&a[right]);
                //cur++;
                right--;
            }
            else if(a[cur]<keyi)
            {
                swap(&a[cur],&a[left]);
                cur++;
                left++;
            }
            else
            {
                cur++;
            }
        }
		QuickSort(a, begin, left - 1);
		QuickSort(a, right + 1, end);
	}
}

快速排序的特性总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(logN)
  4. 稳定性:不稳定

归并排序

基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
请添加图片描述
递归写法:

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);
	if (tmp == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	_MergeSort(a,0,n-1,tmp);

	free(tmp);
	tmp = NULL;
}

非递归写法:

上面已经介绍完递归的写法,因为一提到递归的问题首先就应该想到的栈溢出的问题,所以就介绍了非递归的写法。
非递归思路:这里我们借用到了rangeN变量来控制每次归并的数量,这里还要解决的一个问题就是当数据的个数不是2的几次方的情况下就会出现随机数的问题,所以每次在归并前都要进行一下是否需要修正的判断。每次进行归并的间距是begin1end1begin2,end2进行归并,而出现随机数的情况只有三种,就是end1到最后,begin2到最后,end2到最后,只有这三种情况,将这三种情况控制住就可以实现非递归的归并了。
控制随机数的方法也是有两种:一是整体拷贝。二是部分拷贝

整体拷贝

//整体拷贝
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * (n));
	if (tmp == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	int rangeN = 1;

	while (rangeN < n)
	{
		for (int j = 0; j < n; j += rangeN * 2)
		{
			int begin1 = j, end1 = j + rangeN - 1;
			int begin2 = j + rangeN, end2 = j + 2 * rangeN - 1;
			int i = j;
			//修正
			if (end1 >= n)
			{
				end1 = n - 1;
				//不存在的区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			//归并
			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, tmp, sizeof(int) * n);

		rangeN *= 2;
	}
	free(tmp);
	tmp = NULL;
}

部分拷贝


//部分拷贝
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * (n));
	if (tmp == NULL)
	{
		perror("malloc");
		exit(-1);
	}
	int rangeN = 1;

	while (rangeN < n)
	{
		for (int j = 0; j < n; j += rangeN * 2)
		{
			int begin1 = j, end1 = j + rangeN - 1;
			int begin2 = j + rangeN, end2 = j + 2 * rangeN - 1;
			int i = j;
			//修正
			if (end1 >= n)
			{
				break;
			}
			else if (begin2 >= n)
			{
				break;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			//归并
			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+j, tmp+j, sizeof(int) * (end2-j+1));
		}
		rangeN *= 2;
	}
	free(tmp);
	tmp = NULL;
}

归并排序的特性总结

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

七大排序之间的对比

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。在这里插入图片描述

最后:文章有什么不对的地方或者有什么更好的写法欢迎大家在评论区指出
现在是2022年12月29日也是22年的最后一天,在这里也简单进行一下2022年的小总结。我是从暑假的时候开始学习的,虽说是学习到了东西,但是大部份时间还是在进行摆烂,在学校的日子还算可以身边人也能带动我学习但是一回到家还是没有自控力,说白了还是太懒了。在新的一年里希望更加的自律,坚持完成每一天的任务,少想多做,要让自己看到行动,要动起来!!!!! 在22年的最后一天,对23年的自己说一声加油!!!!
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【数据结构】冒泡排序、快速排序(递归,非递归)、归并排序(递归,非递归),七大排序比较, 的相关文章

随机推荐

  • 2023年网络安全趋势【网安人必看】

    数据安全越来越重要 我国 数据安全法 提出 建立健全数据安全治理体系 各地区部门均在探索和简历数据分类分级 重要数据识别与重点保护制度 数据安全治理不仅是一系列技术应用或产品 更是包括组织构建 规范制定 技术支撑等要素共同完成数据安全建设的
  • android Native和 Flutter 通信

    android 工程集成Flutter 集成方式主要是两种 1 工程的方式集成 首先创建android 和flutter工程 工程路径必须在同一路径下 1 在android 工程的settings gradle 文件中添加 rootProj
  • oracle的jdbc的版本与jdk对应关系

    连接类型 1 JDBC OCI oci是oracle call interface的缩写 此驱动类似于传统的ODBC 驱动 因为它需要Oracle Call Interface and Net8 所以它需要在运行使用此驱动的JAVA程序的机
  • 《软件测试》第六章 检查代码

    软件测试 第六章 检查代码 6 0 前言 6 1 静态白盒测试 检查设计和代码 6 2 正式审查 6 2 1 同事审查 6 2 2 走查 6 2 3 检验 6 3 编码标准和规范 6 4 通用代码审查清单 6 4 1 数据引用错误 6 4
  • MATLAB:画图

    目录 一 一个图上显示多个函数 二 在一个窗口显示多个图片 三 图形标注 四 线条属性 一 一个图上显示多个函数 1 相当于将上一个plot 保持住 plot B big angle x r hold on plot big angle x
  • Pandas中 DataFrame中添加一行/一列

    概述 添加行df loc 以及df append 两种方法 添加列df 和df insert 两种方法 添加行例concat 和reindex 两种方法 一 添加行 1 采用loc 方法 loc方法和iloc方法一样 可以索引DataFra
  • 定义和初始化vector对象

    和任何一种类类型一样 vector模板控制着定义和初始化向量的方法 下面列出了定义vector对象的常用方法 默认初始化 vector对象从而创建一个指定类型的空vector vector
  • Android Studio SDK Manager license 失效问题

    Before building your project you need to accept the license agreements and complete the installation of the missing comp
  • 创客教育中常见的视觉识别摄像头介绍

    近年来 创客教育 人工智能教育在中小学日渐普及 从目前中小学教育的应用层面来说 主要包含了视觉和听觉等几个领域的人工智能教学 因此 摄像头模块或传感器 作为视觉领域必不可少的教具 也被应用的越来越多 市面上越来越多的厂家或机构 也开发了许多
  • java项目时间不够怎么办_时间总是不够用怎么办?

    我在最近两年逐渐体会到两件事 1 这个世界上最公平的事情 就是每人每天拥有24个小时 2 绝大数人都是普通人 一次只能做一件事 我打游戏有三个原则 第一不玩匹配 其二不开声音 最后基本不双排 简单的三条原则 可以大大缩减我每赛季登录王者的时
  • 【git】git命令和相关脚本

    目录 git clone git checkout git diff git diff 忽略文件权限被修改的文件 对比两个分支差异 git add git pull rebase git pull git fetch git reset g
  • 如何查看linux版本 如何查看LINUX是多少位

    一 如何得知自己正在使用的linux是什么版本呢 下面的几种方法将给你带来答案 1 查看内核版本命令 1 root q1test01 cat proc version Linux version 2 6 9 22 ELsmp bhcompi
  • 【数组指针】 仅此一篇 让你深刻理解数组指针

    作者 Mitu 本帖内容著作权归作者所有 转载请务必保留本文链接 数组指针 数组指针 顾名思义 就是指向数组的指针 我们是这样定义它的 int p n n为要定义的个数 按照优先级运算 与 优先级相同 根据结合律 就从左向右运算 里是 p
  • vue中input中v-model语法糖原理

    v model是双向数据绑定 其本质是一个语法糖 我们怎么理解v model在中的运行原理呢 我们将代码进行拆分分析一下 div div
  • mysql如何快速导入大sql文件

    phpmyadmin有内存等的限制 所以文件过大会导致数据导入不全问题 Navicat Premium工具运行sql数据是可以倒全但是效率低 无法快速实现数据恢复 博主亲测source导入12Gsql mysql命令行执行 use data
  • Animator is not playing an AnimatorController

    调用bodyAnimator SetTrigger时出现标题的警告 原因是Animator所在的GameObject是不可见的 可以打印出activeInHierarchy确认一下 把动画的播放方式改为bodyAnimator Play就会
  • ruoyi的springboot微信小程序登录实现方式

    文章目录 前言 一 微信小程序的登录接口 二 微信用户数据库设计 三 springboot登录接口实现 1 新建实体WxUser 2 修改LoginUser类 3 增加wxLogin接口 4 微信小程序登录接口 5 开放接口 总结 前言 主
  • vue考试系统后台管理项目-登录、记住密码功能

    考试系统后台管理项目介绍 技术选型 Vue2 0 Elemenu ui 项目功能介绍 账户信息模块 菜单权限 角色权限设置 角色权限分配 账号设置 公司分组 考试管理模块 新增 编辑 删除考试试题 成绩查看 阅卷评分 成绩记录 成绩导出 题
  • 毕业设计 STM32单片机智能WiFi天气助手 - 物联网 单片机

    文章目录 0 前言 1 设计内容 2 软件设计 3 关键代码 4 最后 0 前言 这两年开始毕业设计和毕业答辩的要求和难度不断提升 传统的毕设题目缺少创新和亮点 往往达不到毕业答辩的要求 这两年不断有学弟学妹告诉学长自己做的项目系统达不到老
  • 【数据结构】冒泡排序、快速排序(递归,非递归)、归并排序(递归,非递归),七大排序比较,

    文章目录 冒泡排序 快速排序 归并排序 七大排序之间的对比 冒泡排序 基本思想 所谓交换 就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置 交换排序的特点是 将键值较大的记录向序列的尾部移动 键值较小的记录向序列的前部移动