思想:分而治之,将大问题转化为若干个相同或相似的子问题。快排的题目常见的方法是利用三指针法将数组分三块搭配随机选择基准元素的思想
颜色分类
下面模拟一下流程
3.然后i所指的元素是0,将left+1位置元素和他交换(还是自己和自己交换),交换完之后i++、left++
当i与right相遇停止循环。
class Solution {
public:
void sortColors(vector<int>& nums)
{
int n = nums.size();
int left = -1, right = n, i = 0;
while(i < right)
{
if(nums[i] == 0) swap(nums[++left], nums[i++]);
else if(nums[i] == 1) i++;
else swap(nums[--right], nums[i]);
}
}
};
排序数组
用数组划分的思想实现快排从而解决这个问题
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
srand(time(NULL)); // 种下⼀个随机数种⼦
qsort(nums, 0, nums.size() - 1);
return nums;
}
// 快排
void qsort(vector<int>& nums, int l, int r)
{
if(l >= r) return;
// 数组分三块
int key = getRandom(nums, l, r);
int i = l, left = l - 1, right = r + 1;
while(i < right)
{
if(nums[i] < key) swap(nums[++left], nums[i++]);
else if(nums[i] == key) i++;
else swap(nums[--right], nums[i]);
}
// [l, left] [left + 1, right - 1] [right, r]
qsort(nums, l, left);
qsort(nums, right, r);
}
int getRandom(vector<int>& nums, int left, int right)
{
int r = rand();
return nums[r % (right - left + 1) + left];
}
};
数组中第k的最大元素
本题是我们之前写过的TOP—K问题,共有四种问法:第K大、第K小、前K大、前K小。解决此问题有两种方法:一种是堆排序,时间复杂度O(nlogn);另一种就是这次的快速选择算法,时间复杂度O(n)。
需要找的是数组排序后的第 k 个最大的元素
设计并实现时间复杂度为 O(n) 的算法
该算法是基于快排改良的
数组分三块+随机选择基准元素:
class Solution {
public:
int findKthLargest(vector<int>& nums, int k)
{
srand(time(NULL));
return qsort(nums, 0, nums.size() - 1, k);
}
int qsort(vector<int>& nums, int l, int r, int k)
{
if(l == r) return nums[l];
// 1. 随机选择基准元素
int key = getRandom(nums, l, r);
// 2. 根据基准元素将数组分三块
int left = l - 1, right = r + 1, i = l;
while(i < right)
{
if(nums[i] < key) swap(nums[++left], nums[i++]);
else if(nums[i] == key) i++;
else swap(nums[--right], nums[i]);
}
// 3. 分情况讨论
int c = r - right + 1, b = right - left - 1;
if(c >= k) return qsort(nums, right, r, k);
else if(b + c >= k) return key;
else return qsort(nums, l, left, k - b - c);
}
int getRandom(vector<int>& nums, int left, int right)
{
return nums[rand() % (right - left + 1) + left];
}
};
最小的k个数——库存管理III
输入整数数组 arr ,找出其中最小的 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
快速选择算法仅仅是把最小的k个数丢到了前面,并没有把前面几个数字排序
这个算法是在递归的过程中直接去对应符合条件的区间里找数字,而不是去区间里排序。所以快速选择算法比快排更快。并且在《算法导论》里有证明,当我们用随机选择基准元素的方法时,我们的三个区间都是等概率划分的,此时他的时间复杂度会逼近与O(N)。
class Solution {
public:
vector<int> inventoryManagement(vector<int>& stock, int cnt) {
srand(time(NULL));
qsort(stock, 0, stock.size() - 1, cnt);//这里是快速选择,不是排序,所以我们只需要返回前k个数就行,里面是无序的
return {stock.begin(), stock.begin() + cnt};
}
void qsort(vector<int>& stock, int l, int r, int cnt)
{
if(l >= r) return;
// 1. 随机选择⼀个基准元素 + 数组分三块
int key = getRandom(stock, l, r);
int left = l - 1, right = r + 1, i = l;
while(i < right)
{
if(stock[i] < key) swap(stock[++left], stock[i++]);
else if(stock[i] == key) i++;
else swap(stock[--right], stock[i]);
}
// [l, left][left + 1, right - 1] [right, r]
// 2. 分情况讨论
int a = left - l + 1, b = right - left - 1;
if(a > cnt) qsort(stock, l, left, cnt);
else if(a + b >= cnt) return;
else qsort(stock, right, r, cnt - a - b);
}
int getRandom(vector<int>& stock, int l, int r)
{
return stock[rand() % (r - l + 1) + l];
}
};
排序数组
整数数组 nums,请你将该数组升序排列。
用归并算法给数组排序,首先先选择mid中间点,先把左边部分排序,排左边的时候相当于又是一个归并排序的过程,直至只剩下一个元素的时候,向上返回,排右边区间,直至剩下一个元素时,开始向上返回,当这一层都排完时,合并两个有序数组。相当于二叉树中的后序遍历,快排的过程是先把数组分两块,然后把左边继续用一个key值分成左右两部分。相当于前序遍历
class Solution {
vector<int>tmp; //节省时间消耗
public:
vector<int> sortArray(vector<int>& nums) {
tmp.resize(nums.size()); //在归并前更改大小
srand(time(NULL)); // 种下⼀个随机数种⼦
mergeSort(nums, 0, nums.size() - 1);
return nums;
}
void mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right) return;
// 1. 选择中间点划分区间
int mid = (left + right) >> 1;
// [left, mid] [mid + 1, right]
// 2. 把左右区间排序
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 3. 合并两个有序数组
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right)
tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
// 处理没有遍历完的数组
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
// 4. 还原
for(int i = left; i <= right; i++)
nums[i] = tmp[i - left];
}
};
数组中的逆序对
这时候在cur1之前的数都是比它小的,所以cur1之前的数就会比cur2之前的数字小,(因为cur1比cur2位置的数字小,cur1会先归并到辅助数组中)。我们找逆序对是在找到比我大的数之前,有多少数字能和我组成逆序对。所以我们分情况讨论:
当cur1[num] <= cur2[num]:说明此时还没有比cur2位置上大的数,就继续找,直到找到cur1位置大于cur2位置的数,所以让cur1++(本质上是先把cur1位置的数放到辅助数组里面,然后让cur1++)
当cur1[num] > cur2[num]:此时cur1后面的数全都比cur2大。我们就根据归并排序的以此比较,就找出了一堆比cur2大的数,此时我们用ret+=mid-cur1+1 记录cur1后面有多少个数字。并且让cur2++
处理细节问题:如果数组降序,可以怎样处理呢。
class Solution {
int tmp[50010];
public:
int reversePairs(vector<int>& nums)
{
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right) return 0;
int ret = 0;
// 1. 找中间点,将数组分成两部分
int mid = (left + right) >> 1;
// [left, mid][mid + 1, right]
// 2. 左边的个数 + 排序 + 右边的个数 + 排序
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
// 3. ⼀左⼀右的个数
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right) // 升序的时候
{
if(nums[cur1] <= nums[cur2])
{
tmp[i++] = nums[cur1++];
}
else
{
ret += mid - cur1 + 1;
tmp[i++] = nums[cur2++];}
}
// 4. 处理⼀下排序
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
for(int j = left; j <= right; j++)
nums[j] = tmp[j - left];
return ret;
}
};
class Solution
{
int tmp[50010];
public:
int reversePairs(vector<int>& nums)
{
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right) return 0;
int ret = 0;
// 1. 找中间点,将数组分成两部分
int mid = (left + right) >> 1;
// [left, mid][mid + 1, right]
// 2. 左边的个数 + 排序 + 右边的个数 + 排序
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
// 3. ⼀左⼀右的个数
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right) // 降序的版本
{
if(nums[cur1] <= nums[cur2])
{
tmp[i++] = nums[cur2++];
}
else
{
ret += right - cur2 + 1;
tmp[i++] = nums[cur1++];
}
}
// 4. 处理⼀下排序
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
for(int j = left; j <= right; j++)
nums[j] = tmp[j - left];
return ret;
}
};
计算右侧小于当前元素的个数
归并排序(分治):因为要找比该位置小的数,所以我们可以用上到题的策略二——当前元素后面有多少个比我小的数。数组降序
class Solution
{
vector<int> ret;
vector<int> index; // 记录 nums 中当前元素的原始下标
int tmpNums[500010];
int tmpIndex[500010];
public:
vector<int> countSmaller(vector<int>& nums)
{
int n = nums.size();
ret.resize(n);
index.resize(n);
// 初始化⼀下 index 数组
for(int i = 0; i < n; i++)
index[i] = i;mergeSort(nums, 0, n - 1);
return ret;
}
void mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right) return;
// 1. 根据中间元素,划分区间
int mid = (left + right) >> 1;
// [left, mid] [mid + 1, right]
// 2. 先处理左右两部分
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 3. 处理⼀左⼀右的情况
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right) // 降序
{
if(nums[cur1] <= nums[cur2])
{
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}
else
{
ret[index[cur1]] += right - cur2 + 1; // 重点 +会覆盖,所以要+=
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
}
// 4. 处理剩下的排序过程
while(cur1 <= mid)
{
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
while(cur2 <= right)
{
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}
for(int j = left; j <= right; j++)
{
nums[j] = tmpNums[j - left];
index[j] = tmpIndex[j - left];
}
}
};
翻转对
分治: 将整个数组分治为两部分,求出左半部分翻转对数a,右半部分翻转对数为b,一左一右翻转对数为c,最后a+b+c即为所求。但有些细节问题
class Solution
{
int tmp[50010];
public:
int reversePairs(vector<int>& nums)
{
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right) return 0;
int ret = 0;
// 1. 先根据中间元素划分区间
int mid = (left + right) >> 1;
// [left, mid] [mid + 1, right]
// 2. 先计算左右两侧的翻转对
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
// 3. 先计算翻转对的数量
int cur1 = left, cur2 = mid + 1, i = left;
while(cur1 <= mid) // 降序的情况
{
while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0) cur2++;
if(cur2 > right)
break;
ret += right - cur2 + 1;
cur1++;
}
// 4. 合并两个有序数组
cur1 = left, cur2 = mid + 1;
while(cur1 <= mid && cur2 <= right)
tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur2++] : nums[cur1++];
while(cur1 <= mid) tmp[i++] = nums[cur1++];
while(cur2 <= right) tmp[i++] = nums[cur2++];
for(int j = left; j <= right; j++)
nums[j] = tmp[j];
return ret;
}
};