快速排序
算法思想
快速排序的基本思想:选择一个数字作为基准数字,比这个数字大的放在右边,小于等于这个数字的放在左边;然后就可以分成两个子序列,对于每个子序列进行同样的操作,直到整个序列是有序的。
算法步骤:
- 从序列中挑出一个数字作为基准(pivot);
- 将给定的序列按照选定的基准分成两个子序列,小于等于基准的放在左侧,大于基准的放在右侧;
- 对于每个子序列采用递归的操作,重复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)
欢迎大家关注我的个人公众号,同样的也是和该博客账号一样,专注分享技术问题,我们一起学习进步