[数据结构] 归并排序快速排序 及非递归实现

news/2024/7/16 9:01:05 标签: 数据结构, 算法, 排序算法

()标题:[数据结构] 归并排序&&快速排序 及非递归实现

@水墨不写bug


(图片来源于网络)


目录

(一)快速排序

类比递归谋划非递归

快速排序的非递归实现:

(二)归并排序 

归并排序的递归实现:

归并排序的非递归

细节处理:

归并排序的非递归实现:


 

 正文开始:

(一)快速排序

        快速排序一般通过递归来实现,但是递归也有递归的劣势:当递归程度太深,会导致栈溢出的问题,我们在前面的分享中已经讲解了快速排序的递归实现,这里不再赘述,为了便于讲解,直接给出快速排序的递归实现:


int GetRandomKey(vector<int>& nums, int l, int r)
{
	return nums[rand() % (r - l + 1) + l];
}
void QuickSort(vector<int>& nums,int l,int r)
{
	//递归出口
	if (l >= r)
		return;
	int key = GetRandomKey(nums,l,r);
	int left = l - 1, right = r + 1, cur = l;
	while (cur < right)
	{
		if (nums[cur] < key)
			swap(nums[cur++], nums[++left]);
		else if (nums[cur] == key)
			cur++;
		else
			swap(nums[--right],nums[cur]);
	}
	QuickSort(nums, l, left);
	QuickSort(nums, right, r);
}

这里给出的快速排序的递归实现是比较完备的优化过的快排,它解决了:

        (1)、key选取不合适导致的分区不平衡的问题。

        (2)、key在数据中重复大量出现的问题。

递归的过程:

        其实通过观察快排的过程,我们会发现之所以在传入参数的时候必须传入左右区间,是因为我们在快排的内部过程中并不确定需要对哪一个区间的数据 进行排序。

        随着递归的进行,函数栈帧逐层开辟,每一层函数栈帧中都存有需要排序的区间的边界值。

每一个函数栈帧都有一个 

        左区间端点值 :l 

        右区间端点值 :r 

递归是在栈区进行的,我们既然需要避免计算机自己的栈区溢出,那么我们为什么不自己模拟一个栈呢?


递归原理:

        通过模拟一个栈,来协助存储左右区间端点值,以此来达到让快排正常进行的目的。

因此,重要的是需要对自己实现的栈精确的控制。


类比递归谋划非递归

        什么时候递归停止?

        当所有递归都返回的时候递归停止——当模拟实现的栈为空的时候停止迭代;

        递归出口的条件设置?

        当递归区间不存在的时候,递归通过return返回到上一层——当递归区间不存在的时候,直接进入下一次迭代,这里就用到了continue;

        如何准确的控制接收的左右区间的端点值?

        通过栈来模拟,需要注意栈的后进先出的特点,push的顺序和pop的顺序是相反的,比如:先push左端点,再push右端点;在top的时候,先取得的是右端点值,pop后,top再取得的是左端点值。

快速排序的非递归实现:

 


void QuickSort_NoR(vector<int>& nums,int l1,int r1)
{
	stack<int> st;
	st.push(l1);
	st.push(r1);
	while (!st.empty())
	{
		int r = st.top();
		st.pop();
		int l = st.top();
		st.pop();

		if (l >= r)
			continue;

		int left = l - 1, right = r + 1, cur = l;
		int key = GetRandomKey(nums, l, r);

		while (cur < right)
		{
			if (nums[cur] < key)
				swap(nums[cur++], nums[++left]);
			else if (nums[cur] == key)
				cur++;
			else
				swap(nums[--right], nums[cur]);
		}
		st.push(right);
		st.push(r);
		st.push(l);
		st.push(left);
	}
}

(二)归并排序 

         归并是一种算法,当归并应用在排序中,实际上的操作就是将两个有序数组合并为一个有序数组的过程。

        归并排序一般通过非递归实现,其核心思想是分治,但是递归的缺点明显,本文上半部分也说明了递归的缺点,因此非递归实现归并有很大意义。

 

 
        时间复杂度:O(N*logN)
        空间复杂度:O(N)
        稳定性:稳定

        归并的缺点:需要O(N)的空间复杂度

我们在实现归并排序的时候,需要注意的是:

(1)、需要一个N个空间的数组辅助进行排序,由于递归次数很多,在递归过程中创建数组代价太大,所以我们在全局来创建一个数组tem,作为辅助,不仅在每一层递归中都可使用,也节省了资源。

(2)、归并的主要过程通过三目运算符处的逻辑实现。

(3)、三目运算符之后,需要再将没有遍历到末尾的数据继续添加到tem末尾即可,此时归并结束。

(4)、最终不要忘了将tem内的数据拷贝回原数组。

归并排序的递归实现:

vector<int> tem(0);
void MergeSort(vector<int>& nums,int l,int r)
{
	if (l >= r)
		return;
	int mid = (r - l) / 2 + l;
	int cur1 = l, cur2 = mid + 1;
	MergeSort(nums, l, mid);
	MergeSort(nums, mid + 1, r);
	int i = 0;
	while (cur1 <= mid && cur2 <= r)
	{
		tem[i++] = nums[cur1] < nums[cur2] ?
			nums[cur1++] : nums[cur2++];
	}
	while (cur1 <= mid)tem[i++] = nums[cur1++];
	while (cur2 <= r)tem[i++] = nums[cur2++];
	for (int j = l; j <= r;++j)
	{
		nums[j] = tem[j - l];
	}
}
int main()
{
	vector<int> nums = { 99,0,7,5,44,3,78,653,90,81 };
	tem.resize(nums.size());
	Print(nums);
	MergeSort(nums,0,nums.size()-1);
	Print(nums);
	return 0;
}

归并排序的非递归

        想要实现归并的非递归,在整体上需要换一种思路。

        在局部上,归并的逻辑仍然是与递归是一致的;

我们在思考的时候要将问题逐渐拆成一个一个的小问题:

(1)、归并过程:

        将[begin1,end1],[begin2,end2]归并为一个有序的数组,算法本质和步骤和非递归的实现方法完全一致;

(2)、非递归省去了进入递归的过程,而是直接将数组分为多份,每一份有gap个:

        gap开始取1,表示一个数字就是一个区间,这个步骤是数组本身就满足的;

        gap每次*2,表示区间扩大的过程,这样一来gap逐渐扩大,就在思路上完成了归并;

        通过分析,你也知道了最重要的是对区间的左右端点的控制,也就是需要控制好区间的偏移和越界问题。

细节处理:

(1)区间的偏移:

        通过一个循环,循环变量为k,两个区间的开始位置是由k来决定的,用k来控制区间的偏移:由于每次是归并两个数组,所以每次归并完成后,k+=2*gap:

演示(以gap=2为例):

偏移后:(k+=2*gap

(2)区间的越界:

我们上图举的例子是一个特殊情况,数组元素个数刚好够归并需要的元素,如果元素有9个而不是8个,这就需要考虑区间的越界问题了。

当数组的长度更加一般时,会出现区间的越界问题,对于每一个区间端点:

        begin1:由k决定(k< n,所以不可能越界)

        end1:begin1+gap-1,有可能越界;如果越界,数组个数只有一个,则不再归并。

        begin2:begin1+gap,可能越界;如果越界,数组个数只有一个,则不再归并。

        end2:begin1+2*gap-1;可能越界;如果越界,数组个数有两个,修正end2的位置后再归并。

归并排序的非递归实现:


void MergeSort_NoR(vector<int>& nums, int l, int r)
{
	int n = nums.size();
	int gap = 1;
	while (gap < n)
	{
		for (int k = 0; k < n; k += 2*gap)
		{
			// 对两组进行归并  [beign1,end1]  [begin2,end2] 
			// 组内宽度gap 
			int begin1 = k, end1 = begin1 + gap - 1;
			int begin2 = end1 + 1, end2 = begin2 + gap - 1;
			if (end1 >= n || begin2 >= n)
				break;
			if (end2 >= n)
				end2 = n-1;
			int i = k;
			while (begin1 <= end1 && begin2 <= end2)
				tem[i++] = nums[begin1] < nums[begin2] ?nums[begin1++] : nums[begin2++];
			while (begin1 <= end1) tem[i++] = nums[begin1++];
			while (begin2 <= end2) tem[i++] = nums[begin2++];
			for (int j = k; j <= end2; ++j)
				nums[j] = tem[j];
		}
		gap *= 2;
	}
}

完~

未经作者同意禁止转载 


http://www.niftyadmin.cn/n/5543023.html

相关文章

一.7.(2)基本运算电路,包括比例运算电路、加减运算电路、积分运算电路、微分电路等常见电路的分析、计算及应用;(未完待续)

what id the 虚短虚断虚地? 虚短&#xff1a;运放的正相输入端和反相输入端貌似连在一起了&#xff0c;所以两端的电压相等&#xff0c;即UU- 虚断&#xff1a;输入端输入阻抗无穷大 虚地&#xff1a;运放正相输入端接地&#xff0c;导致U&#xff1d;U-&#xff1d;0。 虚…

vue3【提效】使用 VueUse 高效开发(工具库 @vueuse/core + 新增的组件库 @vueuse/components)

Vueuse 是一个功能强大的 Vue.js 生态系统工具库&#xff0c;提供了可重用的组件和函数&#xff0c;帮助开发者更轻松地构建复杂的应用程序。 官网 &#xff1a;https://vueuse.org/core/useWindowScroll/ 安装 VueUse npm i vueuse/core vueuse/components&#xff08;可选&a…

搭建论坛和mysql数据库安装和php安装

目录 概念 步骤 安装mysql8.0.30 安装php 安装Discuz 概念 搭建论坛的架构&#xff1a; lnmpDISCUZ l 表示linux操作系统 n 表示nginx前端页面的web服务 m 表示 mysql 数据库 用来保存用户和密码以及论坛的相关内容 p 表示php 动态请求转发的中间件 步骤 &#xff…

Matlab 中 fftshift 与 ifftshift

文章目录 【 1. fftshift、ifftshift 的区别】【 2. fftshift(fft(A)) 作图 】【 3. fftshift(fft(A)) 还原到 A 】Matlab 直接对信号进行 FFT 的结果中,前半部分是正频,后半部分是负频,为了更直观的表示,需要将 负频 部分移到 前面。【 1. fftshift、ifftshift 的区别】 M…

Upload-Labs靶场闯关

文章目录 Pass-01Pass-02Pass-03Pass-04Pass-05Pass-06Pass-07Pass-08Pass-09Pass-10Pass-11Pass-12Pass-13Pass-14Pass-15Pass-16Pass-17Pass-18Pass-19Pass-20 以下是文件上传绕过的各种思路&#xff0c;不过是鄙人做题记下来的一些思路笔记罢了。 GitHub靶场环境下载&#x…

【Android】自定义换肤框架01之皮肤包制作

前言 目前为止&#xff0c;市面上主流的安卓换肤方案&#xff0c;其实原理都是差不多的 虽然大多都号称一行代码集成&#xff0c;但其实想要做到完全适配&#xff0c;并不简单 这个系列&#xff0c;就是让大家从零开始&#xff0c;完全掌握这方面知识&#xff0c;这样才能对…

星光云VR全景系统源码

星光云VR全景系统源码 体验地址请查看

µCOS-III 任务同步机制-任务信号量

1. 什么是任务信号量 任务信号量是一种用于任务间同步和通信的计数器&#xff0c;通常用于解决任务间的竞争条件和资源共享问题。在C/OS-III中&#xff0c;任务信号量提供了二进制信号量和计数信号量两种类型&#xff1a; 二进制信号量&#xff1a;只能取值0或1&#xff0c;适…