快速排序

快速排序

算法思想

快速排序的基本思想:选择一个数字作为基准数字,比这个数字大的放在右边,小于等于这个数字的放在左边;然后就可以分成两个子序列,对于每个子序列进行同样的操作,直到整个序列是有序的。

算法步骤:

  1. 从序列中挑出一个数字作为基准(pivot);
  2. 将给定的序列按照选定的基准分成两个子序列,小于等于基准的放在左侧,大于基准的放在右侧;
  3. 对于每个子序列采用递归的操作,重复1~2步骤,直到子序列的长度为1.

这里给出一个动画来展示上述的算法步骤:
在这里插入图片描述
为了可以使的算法步骤更明了,举一个例子,看看具体是如何操作的。给定一个数组 arr=[6,1,2,7,9,3,4,5,10,8],快排是如何把他分成两个子数组的,经典的快排算法选择的基准一般是最左侧或者最右侧,这里选择最左侧为基准,即pivot=6,那么大于6的放在右边,小于等于6的放在左边,一次排序的结果就是 arr=[5,1,2,4,3,6,9,7,10,8],通过下面的动画可以更清晰的看出是怎么进行交换的
在这里插入图片描述
对于快速排序来说,它的时间复杂度为 O ( n × l o g n ) O(n \times logn) O(n×logn),是因为我们需要划分 l o g n logn logn次,在每一次划分里面需要循环 n n n次,所以时间复杂度为 O ( n × l o g n ) O(n \times logn) O(n×logn),空间复杂度也是 O ( l o g n ) O(logn) O(logn),因为,每次划分子数组都需要记录分界点的位置,需要partition的次数,就是产生的变量数,累计 l o g n logn logn 次划分,所以,空间复杂度也是 O ( l o g n ) O(logn) O(logn)。快速排序是一种不稳定的排序算法,这是因为在基准数字和边界地方交换的过程中,破坏了稳定性,所以是不稳定的算法。

代码实现

#include <iostream>
#include <vector>

using namespace std;

int partition(vector<int>& vec, int left, int right) {
	int pivot = vec[left]; // 选择基准数字

    // 当左侧的下标大于等于右侧时,才跳出循环
	while (left < right) {
        // 大于基准的放在右侧
		while (left < right && vec[right] > pivot) { 
			right--;
		}
		vec[left] = vec[right]; // 交换数字
        // 小于等于基准的放在左侧
		while (left < right && vec[left] <= pivot) {
			left++;
		}
		vec[right] = vec[left]; // 交换数字
	}
	vec[left] = pivot; // 把基准数字更新到分界点的位置

	return left;    // 返回分界点
}

void quickSort(vector<int>& vec, int left, int right) {
    // 如果左侧的下标小于右侧的下标,执行递归操作
	if (left < right) {
		int par = partition(vec, left, right); // 获取分界点
		quickSort(vec, left, par-1);           // 左侧子序列
		quickSort(vec, par + 1, right);        // 右侧子序列
	}
}

void quickSort(vector<int>& vec) {
    // 当vec的长度小于2时,直接返回
	if (vec.size() < 2) {
		return;
	}
    // 调用递归
	quickSort(vec, 0, vec.size() - 1);
}

int main() {
	vector<int> arr = { 6,1,2,7,9,3,4,5,10,8 };
	quickSort(arr, 0, arr.size()-1);
	for (int i = 0; i < arr.size(); i++) {
		cout << arr[i] << " ";
	}
	cout << endl;

	return 0;
}

对于子序列的划分,也就是partition的过程,即大于基准数字的放在右侧,小于等于基准数字的放在左侧,可以也可以采用下面的代码,也是比较简单,可以看一下

// 交换两个数字
void mySwap(int& x, int& y) {
	int temp = x;
	x = y;
	y = temp;
}
// 数组划分子序列
int partition1(vector<int>& vec, int left, int right) {

	int index = left;   // 定义左边子序列的下标,开始是定义left-1,表示没有元素在左侧子序列中
	int pivot = vec[left];  // 定义基准数字

    // 遍历vec,从left开始,一直到right
	for (int i = left+1; i <= right; i++) { 
        // 数字小于等于基准,那么左侧的子序列的就扩充一个位置,并且交换数字
		if (vec[i] <= pivot) {
			mySwap(vec[i], vec[index+1]);
			index++;
		}
	}
    // 把基准数字和最左侧的数字交换
	mySwap(vec[left], vec[index]);

	return index;
}

优化经典快速排序

经典的排序算法是把大于基准的数字放在右侧,小于等于基准的数字放在左侧。举个例子,如果 arr = [6,1,4,3,7,6,6,8,9,10],如果pivot=6,左侧arrL=[6,1,4,3,6],右侧arrR=[7,8,9,10]。这样的话,只是排好了一个数字6,但是还有两个数字6,这样的话,还需要两次排序才可以得到 arr =[3,1,4,6,6,6,7,8,9,10],那么如果我们可以把数组划分为三块,左侧的放小于基准数字,中间的放等于基准数字,右侧放大于基准数字,这样就可以加快partition的过程,与经典快排不同的是我们需要返回的是下图的两个下标,即中间子序列的左侧下标和右侧下标
在这里插入图片描述

代码实现

#include <iostream>
#include <vector>

using namespace std;

/*
* 交换两个数
*/
void mySwap(vector<int>& vec, int index1, int index2) {
	int temp = vec[index1];
	vec[index1] = vec[index2];
	vec[index2] = temp;
}
/*
* 升级的子数组划分函数
*/
vector<int> partitionImprove(vector<int>& vec, int left, int right) {
	vector<int> res;             // 存储中间子数组的左右下标

	int pivot = vec[left];       // 定义基准数字
	int less = left;             // 左侧子数组开始的下标
	int more = right+1;          // 右侧子数组开始的下标
	int index = left+1;          // 遍历数组开始的下标
	
 	while (index < more) {
		// 如果小于基准数字,左侧子数组扩展一个位置,并且原地交换,index前进一位
		if (vec[index] < pivot) {
			mySwap(vec, less+1, index);
			less++;
			index++;
		}
		else if (vec[index] > pivot) {   // 如果大于基准数字,右侧子数组扩展一个位置,原地交换
			mySwap(vec, more-1, index);
			more--;

		}
		else {           //  中间子数组的范围
			index++;
		}
		
	}
	mySwap(vec, less, left);  // 把临界位置的数字和基准数字交换
	res = { less, more-1 };   // 中间子数组的左右下标

	return res;
}

void quickSort(vector<int>& vec, int left, int right) {
	if (left < right) {
		vector<int> p = partitionImprove(vec, left, right); // 接收函数返回的下标
		// p的长度固定为2,p[0]为最左侧已经排好的,p[1]为最右侧
		quickSort(vec, left, p[0] - 1);  // 其中的p[0]-1表示的是左侧子数组最右边未排序的数字
		quickSort(vec, p[1]+1, right);   // 其中的p[0]+1表示的是右侧子数组最左边未排序的数字
	}
}

void quickSort(vector<int>& vec) {
	// 长度小于2,表示没有或者只有一个数字,那么直接返回,不需要排序
	if (vec.size() < 2) {
		return;
	}
	// 调用递归
	quickSort(vec, 0, vec.size() - 1);
}

int main() {
	vector<int> arr = { 12,42,78,54,23,123,34,34,5,12};

	quickSort(arr);

	for (int i = 0; i < arr.size(); i++) {
		cout << arr[i] << " ";
	}
	cout << endl;

	return 0;
}

随机快速排序

经典快速排序的最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2),这是弊端,分析一下这个时间复杂度是怎么来的。假如给定一个数arr=[1,2,3,4,5,6],现在选择最左侧为基准数字 pivot=1,那么左侧的子数组就没有,右侧的子数组就为arrR=[2,3,4,5,6],这样的话,下次递归就是[2,3,4,5,6],依然选择pivot=2,那么依然是左侧为空,右侧为[3,4,5,6],这样继续递归的话,每次这排序一个,每个产生一个子数组,就要分割n次,而不是 l o g n logn logn了,就差生了 O ( n 2 ) O(n^2) O(n2)的时间复杂度。之所以产生这样的情况,是因为选择的基准数字不合适,不能把原数组均匀的分成左右两个子数组。这里采用的是产生一个随机数,产生数的范围是[left, right],即原数组的最左和最右之间,然后和基准数字交换,这样就打乱了原始的数组,改善了左右子数组的划分情况。这样的话,分割的情况就成了一个概率事件,时间复杂度为 O ( n 2 ) O(n^2) O(n2)的情况就不受数据影响了。代码也是很简单,只是多了一个一句话,打乱原始数组,关键代码就是这一句 mySwap(vec, left + (rand() % (right - left + 1)), left);。整体代码如下:

#include <iostream>
#include <vector>
#include <cstdlib>

using namespace std;
/*
*  交换两个数字
*/
void mySwap(vector<int>& vec, int index1, int index2) {
	int temp = vec[index1];
	vec[index1] = vec[index2];
	vec[index2] = temp;
}

/*
* 划分子数组函数,返回中间子数组左右下标
*/
vector<int> partition(vector<int>& vec, int left, int right) {
	vector<int> res;   // 存储中间子数组的左右下标

	int pivot = vec[left];    // 定义基准数字
	int less = left;          // 左侧子数组开始的下标
	int more = right + 1;     // 右侧子数组开始的下标
	int index = left + 1;     // 遍历数组开始的下标
    
	while (index < more) {
        // 如果小于基准数字,左侧子数组扩展一个位置,并且原地交换,index前进一位
		if (vec[index] < pivot) {
			mySwap(vec, index, less + 1);
			less++;
			index++;
		}
		else if (vec[index] > pivot) {    // 如果大于基准数字,右侧子数组扩展一个位置,原地交换
			mySwap(vec, index, more - 1);
			more--;
		}
		else {
			index++;                      //  中间子数组的范围
		}
	}
	mySwap(vec, less, left);              // 把临界位置的数字和基准数字交换
	res = { less, more - 1 };             // 中间子数组的左右下标

	return res;
}

void randomQuickSort(vector<int>& vec, int left, int right) {
	if (left < right) {
        // 随机产生一个数,范围为[left,right],产生的数和最左侧的交换,打乱原始的数组
		mySwap(vec, left + (rand() % (right - left + 1)), left);  // 这一句是关键
        // pivotPos的长度固定为2,pivotPos[0]为最左侧已经排好的,pivotPos[1]为最右侧
		vector<int> pivotPos = partition(vec, left, right);
		randomQuickSort(vec, left, pivotPos[0]-1); // 其中的pivotPos[0]-1表示的是左侧子数组最右边未排序的数字
		randomQuickSort(vec, pivotPos[0] + 1, right); // 其中的pivotPos[0]+1表示的是右侧子数组最左边未排序的数字
	}
}

void quickSort(vector<int>& vec) {
	if (vec.size() < 2) {
		return;
	}
	// 调用递归
	randomQuickSort(vec, 0, vec.size() - 1);
}


int main() {

	vector<int> arr = { 3,44,38,5,47,15,36,26,27,2,46,4,19,50,48 };

	quickSort(arr);
	for (int i = 0; i < arr.size(); i++) {
		cout << arr[i] << " ";
	}
	cout << endl;

	return 0;
}

总结

  • 稳定性:不稳定
  • 时间复杂度: O ( n × l o g n ) O(n \times logn) O(n×logn)
  • 空间复杂度: O ( l o g n ) O(logn) O(logn)

欢迎大家关注我的个人公众号,同样的也是和该博客账号一样,专注分享技术问题,我们一起学习进步
在这里插入图片描述

  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值