快速排序
颜色分类

**思路:**定义三个指针 i,left,right。(三个指针在遍历过程中会将数组分为四块,但是遍历结束时会将数组分为三块,题目中三种颜色的划分正好需要将数组分为三块)
三个指针的作用:
- i:用来遍历数组
- left:标记 0 区域的最右侧
- right:标记 2 区域的最左侧
遍历过程的四个区间:
-
0,left \]:全都是 0
-
i,right - 1 \]:待遍历区域
当前遍历的元素(nums[i])的三种情况:
- nums[i] = 0:swap( nums[ ++left ] , nums[ i++ ] )
- nums[i] = 1:i++
- nums[i] = 2:swap( nums[ --right ] , nums[i] )
遍历如图所示:

代码:
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
int i = 0;
int left = -1;
int right = nums.size();
while(i < right){
if(nums[i] == 0){
swap(nums[++left], nums[i++]);
}
else if(nums[i] == 1){
i++;
}
else{
swap(nums[--right], nums[i]);
}
}
}
};
排序数组

**思路:**使用快排来解决这道题,核心排序步骤和上一道题(颜色分类)一样,使用数组分三块的思想来排序。选择一个基准值key,定义三个指针 i,left,right,然后根据基准值进行数组分三块的过程,这个过程结束,[left + 1, right - 1] 这个区间其实就排好了(区间内元素都等于 key),然后递归排序还没有排好的左区间和右区间。
遍历过程的四个区间:
-
0,left \]:小于 key 的值
-
i,right - 1 \]:待遍历区域
优化:用随机的方式选择基准元素,这样可以让时间复杂度更加接近 O(nlogn),即 r = rand() % (right - left + 1) + left。(r 是随机的基准元素的下标, 这里的 left,right 是要排序区间的左右端点)
代码:
cpp
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 = nums[rand() % (r - l + 1) + l];
int i = l;
int left = l - 1;
int right = r + 1;
while(i < right){
if(nums[i] < key){
swap(nums[++left], nums[i++]);
}
else if(nums[i] > key){
swap(nums[--right], nums[i]);
}
else{
i++;
}
}
qsort(nums, l, left);
qsort(nums, right, r);
}
};
数组中的第K个最大元素

**思路:**我们使用快排(数组分三块 + 随机选择基准元素)的方式来解决这道题,首先使用本文第二道题快排的方式对区间数组进行排序,但是并不是将数组直接彻底排好,快排彻底排好序需要进行若干次递归,这道题中每次递归排序结束后我们进行如下判断:

- c >= k:不需要管 right 左边的区间,直接去 [right, r] 区间继续排序找第 k 大即可。
- b + c >= k:直接返回 key(随机选择的基准元素)。
- 上面两个不成立时:去 [l , left] 区间继续排序找第 k 大元素。
代码:
cpp
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];
int key = nums[rand() % (r - l + 1) + l];
int left = l - 1;
int right = r + 1;
int i = l;
while(i < right){
if(nums[i] < key){
swap(nums[++left], nums[i++]);
}
else if(nums[i] > key){
swap(nums[--right], nums[i]);
}
else {
i++;
}
}
int c = r - right + 1;
int 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);
}
}
};
库存管理 III

**思路:**我们使用快排(数组分三块 + 随机选择基准元素)的方式来解决这道题,首先使用本文第二道题快排的方式对区间数组进行排序,但是并不是将数组直接彻底排好,快排彻底排好序需要进行若干次递归,这道题中每次递归排序结束后我们进行如下判断:

- a > k:不需要管 left 右边的区间,直接去 [ l, left ] 区间继续找最小的 k 个元素。
- a + b >= k:排序结束,直接返回前 k 个元素即可(建立在第一个判断为 false 的前提下)。
- 上面两个不成立时:去 [ right , r ] 区间继续排序找最小的 k - a - b 个元素。
这些判断的作用其实就是快速将前 k 个小的元素放到数组的前 k 个位置,并且这前 k 个小的元素之间没有必要排序。
代码:
cpp
class Solution {
public:
vector<int> inventoryManagement(vector<int>& stock, int cnt) {
srand(time(NULL));
qsort(stock, 0, stock.size() - 1, cnt);
return {stock.begin(), stock.begin() + cnt};
}
void qsort(vector<int>& nums, int l, int r, int k){
if(l >= r)
return;
int key = nums[rand() % (r - l + 1) + l];
int left = l - 1;
int right = r + 1;
int i = l;
while(i < right){
if(nums[i] < key){
swap(nums[++left], nums[i++]);
}
else if(nums[i] > key){
swap(nums[--right], nums[i]);
}
else{
i++;
}
}
int a = left - l + 1;
int b = right - left - 1;
if(a > k){
qsort(nums, l, left, k);
}
else if(a + b > k){
return;
}
else{
qsort(nums, right, r, k - a - b);
}
}
};
归并排序
排序数组

**思路:**这里使用归并排序的方法,计算数组中间下标,根据中间下标将数组分为左右两部分,分别将左右两部分排好序,然后左右部分进行合并,左右部分的排序只需要继续递归,不断将待排序区间分为左右两部分,直到待排序区间只有一个元素,此时无需排序了,递归结束,开始向上返回,返回过程中将排好序的左右两部分进行合并。
代码:
cpp
class Solution {
vector<int> tmp;
public:
vector<int> sortArray(vector<int>& nums) {
tmp.resize(nums.size());
mergeSort(nums, 0, nums.size() - 1);
return nums;
}
void mergeSort(vector<int>& nums, int left, int right){
if(left >= right) return;
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
int cur1 = left;
int cur2 = mid + 1;
int 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++];
for(int i = left; i <= right; i++){
nums[i] = tmp[i - left];
}
}
};
交易逆序对的总数

**思路:**这里使用归并排序的思想解决这道问题,找一个数组中的所有逆序对,我们可以根据数组的中间下标将数组分为左右两部分,分别找左边部分的逆序对,右边部分的逆序对,一左一右(即左右部分各出一个数)的逆序对,三种情况的逆序对数加起来就是最终结果。其中只在左边部分找逆序对的情况可以继续向下递归,分成左右部分,然后继续找左边部分的逆序对,右边部分的逆序对,一左一右的逆序对。右边部分同理,向下递归即可。所以最终其实就是在处理一左一右的情况。
找一左一右情况逆序对的两种策略:
1. 找出该数之前,有多少个数比我大
这种情况必须排升序,否则计算逆序对数时会重复计算。计算过程如下图:(横线表示数组)

因为两边区间已经排好序了,所以当 nums[cur1] > nums[cur2] 时,nums[cur1] 后面的元素都大于 nums[cur2],所以[cur1 , mid] 这个区间(包含 cur1 和 mid)的数都能和 nums[cur2] 构成逆序对。
2. 找出该数之前,有多少个数比我小
这种情况必须排降序,否则计算逆序对数时会重复计算。计算过程和上面同理。
代码:
cpp
class Solution {
vector<int> tmp;
public:
int reversePairs(vector<int>& record) {
tmp.resize(record.size());
return mergeSort(record, 0, record.size() - 1);
}
int mergeSort(vector<int>& nums, int left, int right){
if(left >= right)
return 0;
int ret = 0;
int mid = left + (right - left) / 2;
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
int cur1 = left;
int cur2 = mid + 1;
int 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++];
}
}
while(cur1 <= mid)
tmp[i++] = nums[cur1++];
while(cur2 <= right)
tmp[i++] = nums[cur2++];
for(int i = left; i <= right; i++)
nums[i] = tmp[i - left];
return ret;
}
};
计算右侧小于当前元素的个数

**思路:**这道题可以借助归并排序来解决,对数组进行归并排序,在左右区间排好序进行合并的时候进行如下判断:(注意:此题中归并排序排的是降序)

但是由于排序会导致数组元素位置变化,导致算出来的数据如果直接填入结果数组位置会不正确,所以需要一个辅助数组来记录当前元素原始的数组下标是多少,这个用来记录下标的数组只需要将正确的下标先记录下来,然后题目给出的数组中的元素一同移动位置即可。
代码:
cpp
class Solution
{
vector<int> ret;
vector<int> index;//记录nums数组中当前元素的原始下标
int tmpNums[500010];
int tmpIndex[500010];
public:
vector<int> countSmaller(vector<int>& nums)
{
ret.resize(nums.size());
index.resize(nums.size());
for(int i = 0; i < nums.size() - 1; i++)
{
index[i] = i;
}
mergeSort(nums, 0, nums.size() - 1);
return ret;
}
void mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right)
return;
int mid = (left + right) >> 1;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
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++];
}
}
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];
}
}
};
翻转对

**思路:**这道题的思路和上面两题一样,就不在赘述了,不同的是这道题给出的比较关系是二倍的关系,而归并排序中合并有序数组时是直接比较的,不需要乘 2,所以此题计算有多少符合条件的翻转对是在左右区间排好序后,合并前单独进行一次循环来统计。具体参考如下代码(下面代码采用的是降序,升序也可以解决这道题)。
代码:
cpp
class Solution
{
vector<int> temp;
public:
int reversePairs(vector<int>& nums)
{
temp.resize(nums.size());
int ret = mergeSort(nums, 0, nums.size() - 1);
return ret;
}
int mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right)
return 0;
int ret = 0;
int mid = (left + right) >> 1;
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid)
{
while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0)
cur2++;
if(cur2 > right)
break;
ret += right - cur2 + 1;
cur1++;
}
cur1 = left;
cur2 = mid + 1;
while(cur1 <= mid && cur2 <= right)
{
if(nums[cur1] > nums[cur2])
temp[i++] = nums[cur1++];
else
temp[i++] = nums[cur2++];
}
while(cur1 <= mid)
temp[i++] = nums[cur1++];
while(cur2 <= right)
temp[i++] = nums[cur2++];
for(int i = left; i <= right; i++)
nums[i] = temp[i - left];
return ret;
}
};