随机快速排序的原理:在数组中随机选取一个数作为分隔值a,将小于a的数放到a的左边,大于a
的数放到a的右边,之后再递归排列右数组和左数组。
注:关于c语言中随机选择的函数rand需注意:rand()%(n)就会随机选取0~n-1中的任意一个数,
其头文件是#include<cstdlib>(c++中也有自己的随机选择函数库)
#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;
int first,last;//记录每次排序后左右数组的末尾和开头的下标
void swap(vector<int>& nums, int first, int i) {//交换两数
int a = nums[first];
nums[first] = nums[i];
nums[i] = a;
}
void partion(vector<int>& nums, int l, int r, int x) {
first = l;
last = r;
int i = l;
while (i <= last) {
if (nums[i] == x)//等于x的数值不动,等下面碰到小于x的数值时,交换nums[i]和nums[first]
i++; //相当于推着等于x 的数向前走
else if (nums[i] < x) {//
swap(nums,first, i);
first++;
i++;
}
else {
swap(nums, last, i);
last--;//为什么不i++?因为交换后原last位置的数还没判断与x的大小。与first不一样,
} //first的数交换到后面未来还会进行判断
}
}
void quicksort(vector<int>& nums,int l,int r) {
if (l >= r) return;
int x = nums[l + rand() % (r - l + 1)];//在l~r的数组区间内随机选择一个数
partion(nums, l, r, x);//将数值等于x的所有数都放在一起
int left = first - 1;//first为x左边小于x的数的第一个下标
int right = last + 1;//last为x右边大于x的数的第一个下标
quicksort(nums, l, left);
quicksort(nums, right, r);
}
int main() {
vector<int> nums = {1,5,2,6,12,5,0};
quicksort(nums, 0, nums.size() - 1);
for (int i = 0; i < nums.size(); i++)
cout << nums[i] << " ";
return 0;
}
上述随机排序是将每次等于取得的随机值的所有的数都放在一起,也叫"荷兰国旗优化随机排序算
法"其实也可以每次只将一个等于x的数值操作,其他等于x的数值在下一次递归中进行排列,但如
果给的数据全部都是同一个值,那么就会调用多次递归,不如一次性处理好所有等于x的元素,所
以不再介绍一次递归只处理一个数据的写法。
随机快排的时间复杂度:
随机快排作为一个随机性过程,不能取其最差情况来估计其时间复杂(只有当代码的执行过程是固
定流程时,才可以取其最差的情况估计其时间复杂度)。对于随机过程,我们应取其期望值来估计
其时间复杂度。如果我们每次取的x值正好是区间的中间大小的值,那么根据master公式可以得出
其时间复杂度为T(N)=2*T(N/2)+O(N)(中间每次将大于x和小于x的数分布到两侧的时间复杂度为
O(N),因为只遍历了一遍)=O(N*logN);但当我们取得x值每次都是区间的最大值时,比如区间的范
围为1~n,第一次如果排n,需遍历1~n-1;第二次如果排n-1,需要遍历1~n-2.....;依次类推,可
见其时间复杂度为O(N^2)。数学家们推算其时间复杂的期望为O(N*logN)(过程很复杂,知不知
道不影响后面的学习)。由此我们得出一个重要的结论:在进行随机快排时,我们会想一个问题,
就是为什么要 "随机",时间复杂度就是原因,如果我们固定流程,那如果我提供一组数据1~n(从
小到大已经排好序了),显然根据上面的推算,其最坏的时间复杂度的情况就是O(N^2),所以我
们选择随机排序。
练习随机排序链接数组排序
随机选择算法
以leetcode"第k个大小的数"为例 第k大的数
#include<iostream>
#include <vector>
using namespace std;
int first, last;
void swap(vector<int>& nums, int left, int right) {
int rem = nums[left];
nums[left] = nums[right];
nums[right] = rem;
}
void partion(vector<int>& nums, int l, int r, int x) {//和随机排序算法一样,将等于x的数
//放在一起
first = l, last = r ;
int i = l;
while (i <= last) {
if (nums[i] == x)
i++;
else if (nums[i] > x) {
swap(nums, i, last);
last--;
}
else {
swap(nums, first, i);
first++;
i++;
}
}
}
int randselect(vector<int>& nums,int k) {
int l = 0, r = nums.size() - 1;
while (l <= r) {
int a = nums[l+rand() %(r-l+1)];
partion(nums, l, r, a);
if (k < first)//排好序后将返回的first和last与要求的下标比较
r = first;
else if (k > last)
l = last + 1;
else {//处于last和first之间就说明下标为k的数就是区间内的数
return nums[k];
}
}
}
int main() {
vector<int>nums = { 2,5,1,11,3,0 };
int k;
cin >> k;
int ans = randselect(nums, nums.size() - k);//第k大的数,就是排好序后下标为nums.size()-k
//的数
for (int i = 0; i < nums.size(); i++)
cout << nums[i]<<" ";
cout << endl;
cout << ans;
return 0;
}
时间复杂度:
如果每次随机出来的数都在最右端,而我们要选的数为最小的那个,第一次递归要遍历n-1个数;
第二次要遍历n-2个数.....,所以最差情况的时间复杂度为O(N^2)。而好点的的就是我们每次随机
的数都在最中间,那么每次都折半查找,第一次找n个,第二次n/2个.....,所以其时间复杂度为O(N)
(因为我们只排序一半,不需要两边都排。不像随机快排一样,每次两边都要排序)。根据概率计
算,其时间复杂度的期望为O(N)