一文讲透快速排序和递归!!!

快速排序
该算法是一种对于数列进行排序的高级排序算法,在数据量较大的情况下,它的运行时间复杂度为O(nlogn),空间复杂度为O(1)。以下将介绍快速排序算法的基本步骤,并分析难点和注意事项。

1. 算法步骤
1.选定基准值,基准值的选取十分重要,这在很大程度上决定了该程序的运行效率。而当数据的随机性足够的时候,基准值的选取平均情形一致。文中代码选取基准值代号为base。
2.区间划分。根据基准值对数列进行进行划分,使数列种所有不大于base的数都在base的左边,而所有比base大的数都在base的右边。
3.重复步骤二,直到区间元素的值为1或者0,这时函数返回的结果即为有序数组。由于遵循步骤二的条件,所以此时的数列一定是有序的。

2. 递归算法思想描述及快速排序递归方法分析
很多同学想必对于递归思想也是一知半解,的确,如果不了解计算机的工作原理,我们的确很难理解递归为何就可以返回正确的结果。而且,很多时候,我们往往会陷入一种误区,即我们总是希望能够想明白递归执行的步骤,试图想明白它运行的所有情况。然而结果却是我们常常令自己头昏眼花,甚至怀疑人生,哈哈有点扯远了。
那么,我们该以什么样的思考方式来看待递归呢?
我觉得我们不应该关注递归的具体细节,而应该关注递归的运行终止约束条件和递归过程式。这个式子可能是一个方程式,也可能是一个可以使我们的递归朝着递归终止条件前进的表达式。嘿嘿,因此,对于递归,我认为我们可以着重关注它的递归进行式以及它的终止条件。嘿嘿,这两个是很重要的哈,对于递归进行式,如果我们的式子写错了,我们就无法达到我们的终止条件,即使能够达到,运行结果也将错误。另外,如果递归进行式写错,这将意味着我们可能会导致堆栈溢出等问题,啊这个就难受了!对于终止条件,如果我们考虑不周,写错了,或者没有给出正确的边界条件。结果是什么呢?嘿嘿,要么运行结果不正确,要么嘿嘿,陷入死循环(这也很难受啊啊啊)哈哈,所有小伙伴一定要注意这两点!
接下来让我们分析一下快速排序的递归表达式和递归终止条件吧!
让我们来看一下快速排序算法的步骤2,由于要将所有不大于base的数都移到base的左边,而将所有不小于base的数都移到base的右边,同时考虑到覆盖全部数据(注意我们这样做的目标是使我们的最终结果满足步骤2的条件,这点请注意一下!)因此我们可以选取数列最左边的数据的下标作为一个参数,同时选取数列的最后一个数的下标作为另外的参数,同时指定base所在的下标,借助这三个参数,我们就可以实现步骤2的操作目标。(注意,这里由于两个参数的选取的不同,以及base选取法的不同,你也许会看到很多不同的递归版本,这些版本的递归本质和终止条件都是一样的(终止条件其实就是算法步骤3)在我们选取参数后,我们应该如何构造递归式子呢?哦吼,注意算法步骤3,当我们的区间长度为1或者为0时,递归返回。这就是我们的终止条件!那么这意味着什么呢?由于我们选取了三个参数,那么我们能否根据这三个函数将我们的排序区间逐渐缩小呢?(因为只要保证递归式是缩小的,那么它就可以终止的条件前进!)明白了这点,(如果你没有明白,多看几遍,多思考一下一定可以明白!),我们可以这样做,即构造一个排序函数,同时它的区间的参数是逐渐缩小的,同时我们再一次递归中实现算法步骤2要求的目标,这个问题不就可以解决了吗?!
接下来,请结合以下代码理解上述过程!

3. 递归算法代码展示
递归算法代码展示如下,其中每个步骤的说明我已详细注释,请结合2中分析仔细体会!!!这里选取的实现步骤2的方法为填充法。该函数返回下一次递归的左边边界,具体划分方法见以下代码:

int partition1(vector<int>& v, int left, int right) { //划分序列(填坑法)
	int base = v[left]; //一次递归运算中基准值不变的
	while(left < right) {
		while (left < right && v[right] >= v[left]) { //从左到右寻找比base小的数
			right--;
		}
		if (left < right) {//找到了
			v[left] = v[right];  //同时v[right]也空出来了
			left++;
		}
		while (left < right && v[left] <= base) { //从左往右寻找比base大的数
			v[right] = v[left];//将左边比base小的数移到右边
			right--;
		}
	}
	v[left] = base;//将基准值填入坑中,此时left和right已经相等,直接填入即可
	return left; //返回边界值,作为下一次递归的起点
}

结合划分方法中返回的边界,我们可以利用递归解决更小子区间的排序问题,同时也要考虑一定要覆盖到全部数据!!!相关代码见下:

void qsort_withRec1(vector<int>& v, int left, int right) {
	if (left< right) {
		int lower = partition1(v, left, right);//返回划分新边界,即base所在的下标
		qsort_withRec1(v, left, lower);//对左子区间进行排序,
		qsort_withRec1(v, lower+1, right);//对右子区间进行排序
	}
	//以上我们可以看到再调用排序函数的时候,区间的确变小了,而且,该区间会重复调用,嘿嘿!
	//有感觉了吧?!同时第二个调用的时候写的是lower+1,这就保障了数据的全覆盖并节省
}

4. 非递归算法思想描述
在分析完递归编写的代码后,让我们来分析一下如何编写非递归代码。首先,我先问一个问题,在我们将一个递归代码改写为一个非递归代码时,我们到底是在完成什么?(这看起来有点傻,但可以先想想)
好啦!嘿嘿让我来说一下我的看法哈哈,对于递归的过程,根据之前的分析,我们已经知道如何编写快速排序的算法了,并着重了如何使递归前进,最后给出了相应的终止条件。在回答这个问题之前,我们先来看看以上递归程序在做什么,结合注释,我们知道,它实际上是在不断地对自身进行调用,然后不断地逼近边界条件,最后在我们无法看见的地方,这个程序的数据就将我们的数组排好序,并最终返回排序结果。在这段代码的驱动下,计算机神不知鬼不觉地将我们想要的结果返回了,仿佛之前的排序结果已经被保存好了。好啦!扯得有点远了,那么,我们来回答一下,我们到底在完成什么。在这段分析下,我们貌似只需要将这段递归代码完成的功能以一种非递归的方式完成即可。在这个过程中,我们需要完成的功能包括:
1.使参数所表示的区间不断缩小。
2.记录每次排序所得结果,并依次返回

为了实现排序结果的返回,我们可以借助于(不熟悉栈的同学可以先看我的博客《STL之栈和队列》熟悉其相关操作并理解其功能)这种数据结构来实现,因为栈的数据结构特点不就是后进先出吗?那么我们可以将表示参数的数据放入栈中,这样我么不就可以表达出2所要求的功能了吗?另外,为了使相应的参数表示的区间不断缩小,我们在还需要对参数不断进行更新,并且缩小其参数范围。因此我们还需要对栈中保存的参数进行取出操作,并依据取出的结果决定我们是否再进一步对参数进行更新,进行参数更新显然可以根据partition函数进行,于是,我们就可以据此写出相关代码了。详细说明如下:

5. 非递归算法代码展示

void qsort_withoutRec(vector<int>&v, int left, int right) {
	if (left >= right) return;
	stack<int>s;//执行这段代码需要引入头文件<stack>
	s.push(left);
	s.push(right);//将参数入栈
	while (!s.empty()) {  //程序终止条件,这意味着到达递归终止条件时,left==right,
		               //且代码参数不再更新,结合以下代码可知其结果最终为空
		int right = s.top();
		s.pop();
		int left = s.top();
		s.pop();
		if (left < right) {
			int lower= partition1(v, left, right); //更新边界
			//左区间
			s.push(left);
			s.push(lower);//等价于对左区间进行排序操作
			//右区间
			s.push(lower + 1); //等价于对右子区间进行相关的操作
			s.push(right);
		}
	}  
}

6.随机数生成的说明
本程序为了生成数据进行测试,使用了rand()函数来生成随机数。但是我们知道这其实并不是真的随机数,这意味着你下一次操作时,他取出的数将相同。因此本程序借助于srand()函数以及利用time()生成随时间变化的随机数种子,并最终成功生成验证数据。
相关代码如下所示:

    srand(time_t());//设置随机数种子
	for (int i = 0; i < test_num; i++) {
		int temp = rand();
		test.push_back(temp);
		//if (i % 3 == 0) {
		//	test.push_back(2 * i - 5);
		//}第二种生成随机数的方法
	}

7. 结果打印说明
结果打印时,传入了参数vector&v,这是vector的引用,对于引用,你只需理解它其实就是自己,只不过它可以避免传统的拷贝构造,从而节省计算资源和计算时间。打印结果的过程中,本程序提供了vector类的迭代式和一般遍历方式,对相应操作不是很熟悉的同学可以查看我的博客《STL之vector》熟悉相关操作。
结果打印代码如下:


```cpp
void print(vector<int>& v) { //打印排序结果
	vector<int>::iterator iter = v.begin();
	cout << "排序结果为:" << endl;
	//for (; iter != v.end(); iter++) {
	//	cout << *iter<< " ";
	//	if ((*iter) % 5== 0) {
	//		cout << endl; //每输出10个元素进行换行操作
	//	}
	//}
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
		if ((i + 1) % 5 == 0) {
			cout << endl;
		}
	}
	cout << endl;
}


 **8. 测试结果展示**
 通过测试,我们随机生成10个数并进行排序,结果展示如下:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210306212718352.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NjMyNTc3Mw==,size_16,color_FFFFFF,t_70)


 **9. 额外说明**
 本文中还要很多知识点讲得不是很透彻,比如函数递归调用等,以及递归存在的容易引起堆栈溢出等问题,还有下标的一些分析思想,这些东西我将在今后的文章中说明。大家可以关注公号获取最新信息,关注有惊喜哦!。一起冲!
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210306212801821.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NjMyNTc3Mw==,size_16,color_FFFFFF,t_70#pic_center)

 
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值