文章目录
- 1、随机快速排序
- 1.1 什么是随机快排
- 1.2 随机快排的好处
- 2、随机选择算法
前言: 快速排序就是每次划分前,通过一种方法将一个基准值的位置确定好,再进入不同的部分重复相同的工作以此确定好每个值的位置以达到有序。如果你之前并不了解快速排序请看快速排序。
1、随机快速排序
1.1 什么是随机快排
随机快速排序顾名思义也是一种快速排序,它和普通的快排不同在有以下的优化。
- 通过随机选取一个基准值,保证应对快排在有序情况下的最坏效率问题。
- 通过三路划分的方式,假设基准值为x,每一趟划分都会确保区间为<x ==x >x。
随机快排的思路:
- 通过数组下标控制范围,在这个范围内通过随机函数选择一个随机数。
- 选好随机数后以此数为基准值,通过三路划分的方式划分区间为<x ==x >x。
- 通过下标控制边界,通过函数递归重复1和2。
第一步:选随机数。 由于采用的是C++语言,用的是rand函数,下面先看用法再看快排相关的。
#include <ctime>
#include <cstdlib>
srand(time(0)); //确保每次运行rand的随机序列不同
int num = rand(); // 会返回0到32767之间的整数
//如果生成[a,b)区间的话,而[a,b]就在后面加1
int num = a + rand() % (b-a) // a + rand() % (b-a+1)////那么快排中选取一个随机数就是(当然这里也可以用下标)
int x = nums[l + rand() % (r - l + 1)];
第二步:确定好基准值后进行以基准值分割区域
int first = 0, last = 0;
void partition(vector<int>& nums, int l, int r, int x){first = l, last = r;int i = l;while (i <= last){if (nums[i] < x) swap(nums[i++], nums[first++]);else if (nums[i] > x) swap(nums[i], nums[last--]);else i++;}}
选定全局first(下图F)和last(下图L)作为等于x区域的左边界和右边界,用i来遍历数组,x代表所选随机数。其中F左边的区域代表小于x的,L右边的区域代表大于x的。F和i开始一起。
- 当nums[i] < x时,交换i和F的数,并且i和F向右走,使得F左边都是小于x的数。
- 当nums[i] > x时,交换i和L的数,这时只有L向左走(因为交换后i位置可能还是大于x的数),保证L右边都是大于x的数。
- 当nums[i] == x时,i直接向右走,此时F左边依旧是小于x的数,L右边依旧是大于x的数。

第三步:通过下标控制边界,通过函数递归重复1和2
void RandomQuickSort(vector<int>& nums, int l, int r)
{if (l >= r) return;int x = nums[l + rand() % (r - l + 1)];partition(nums, l, r, x);RandomQuickSort(nums, l, first - 1);RandomQuickSort(nums, last + 1, r);
}
下面通过一道题进行随机快排的测试。
排序数组
完整代码
class Solution {
public:int first = 0, last = 0;void partition(vector<int>& nums, int l, int r, int x){first = l, last = r;int i = l;while (i <= last){if (nums[i] < x) swap(nums[i++], nums[first++]);else if (nums[i] > x) swap(nums[i], nums[last--]);else i++;}}void RandomQuickSort(vector<int>& nums, int l, int r){if (l >= r) return;int x = nums[l + rand() % (r - l + 1)];partition(nums, l, r, x);RandomQuickSort(nums, l, first - 1);RandomQuickSort(nums, last + 1, r);}vector<int> sortArray(vector<int>& nums) {RandomQuickSort(nums, 0, nums.size() - 1);return nums;}
};
随机快排的时间复杂度为O(N*LogN),空间复杂度为O(LogN)。
1.2 随机快排的好处
接下来我们看看为什么随机快排需要挑选随机数以及三路划分。
我们先来看看什么是快排效率最差的情况。
- 最差情况:当对一个有序数组进行快排,比如[1 2 3 4 5 6 7],如果依据上面partition的做法,普通的按照最后一个数为基准值,那么每次都得拿最后一个数和前面每一个数进行比较(比如第一趟7和前面每一个数都比了一遍),那么时间复杂度就是O(N^2)。如果通过随机选取一个基准值,那么就可以极大概率降低每次都抽到最后一个数的情况。
为什么要用三路划分?
- 如果给定一组数3 3 3 4 3 3 3,如果按照之前快排的挖坑法,第一次排序后还是3 3 3 4 3 3 3,因为其采用的是划分为>=x 和 <=x的区域,而通过三路划分则可以划分为3 3 3 3 3 3 4。(这两个方法都可以实现有序,只是在一些场景下我们要求严格的<x =x >x进行划分,通过三路划分就更好。比如下面随机选择算法的这道题)
2、随机选择算法
随机选择算法主要用于在无序数组中快速定位第k大的元素。其通过分区策略和随机化选择,最终时间复杂度能达到O(N)。
它的原理如下:
- 通过确定随机数后进行三路划分,得到<x ==x >x三个范围,并且这个x所在位置和有序的情况是一样的(因为快排所做的就是让每个基准值放在对应有序的位置)。
- 因为第k大的元素,反过来就是len-k小的元素,在有序情况下这个元素就是对应所在下标位置。
- 因此,假设对应k位置的值为val,那么val大于x,就说明还需要道x的右边区域查找(也就是再细分区域),如果val小于x,就说明要道x的左边区域继续查找。如果等于x就说明第k大的元素就是x。
数组中的第K个最大元素
完整代码
class Solution {
public:int first = 0, last = 0;int findKthLargest(vector<int>& nums, int k) {//找第k大的,就是找nums.size-k小的return findKthSmall(nums, nums.size() - k);}int findKthSmall(vector<int>& nums, int i){int ans = 0;int l = 0, r = nums.size() - 1;while (l <= r){int x = nums[l + rand() % (r - l + 1)];//将x对应的有序位置进行确定partition(nums, l, r, x);//看nums[i]和x的大小关系,确定nums[i]的位置if (nums[i] > x) l = last + 1;else if (nums[i] < x) r = first - 1;else{ans = nums[i];break;}}return ans;}//三路划分, 一定要注意划分范围是 >x ==x >x 不然不行void partition(vector<int>& nums, int l, int r, int x){first = l, last = r;int i = l;while (i <= last){if (nums[i] < x) swap(nums[i++], nums[first++]);else if (nums[i] > x) swap(nums[i], nums[last--]);else ++i;}}
};
为什么只能用三路划分?partition用挖坑法行吗?
如果是3 3 3 4 3 3 3这样一组数找第一大的,如果采用挖坑法只能划分<=x >=x两个范围,对于随机到3而言,第一趟划分就是不变,就无法通过比较对应i位置的大小确定第一大的数的位置。因此一定得保证划分范围是 >x ==x >x 。
算法中有很多精妙又美丽的思想传统,请务必坚持下去!!