
题目链接: 75. 颜色分类
题目描述:
给定一个包含红色、白色和蓝色的数组 nums,共 n 个元素,要求对它们进行原地排序,使得相同颜色的元素相邻,并按红色、白色、蓝色的顺序排列。这里使用整数 0、1 和 2 分别表示红色、白色和蓝色,且不能使用库的排序函数。
示例 1:
nums = [2,0,2,1,1,0][0,0,1,1,2,2]示例 2:
nums = [2,0,1][0,1,2]提示:
n == nums.length1 <= n <= 300nums[i] 为 0、1 或 2解题思路
对于颜色分类问题,我们可以采取三路划分的思想来解决,利用三个指针
将数组划分成
块,分别是
,这样可以实现一次遍历就将颜色快速分类。
具体如下:
:区间内的元素全是0
:区间内的元素全是1
:待处理的区间
:区间内的元素全是2
从左往右遍历数组。
,然后
。
即可,无需做额外操作。
,然后
,此时
需要保持不变,因为
位置的元素是待处理区间的元素。
时,结束循环,数组被成功划分成
区间。
代码实现
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int left = -1, right = n, cur = 0;
while(cur < right)
{
if(nums[cur] == 0) swap(nums[++left], nums[cur++]); //将0交换到0区间的末尾
else if(nums[cur] == 1) cur++; //元素为1,直接让cur++
else if(nums[cur] == 2) swap(nums[--right], nums[cur]); //将2交换到2区间的起始的前一个位置
}
}
};题目链接: 912. 排序数组
题目描述:
给定一个整数数组 nums,请将该数组按升序排列。
你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。
示例 1:
nums = [5,2,3,1][1,2,3,5]示例 2:
nums = [5,1,1,2,0,0][0,0,1,1,2,5]提示:
nums.length <= 5 * 104nums[i] <= 5 * 104解题思路
题目要求实现 O(nlog(n)) 的算法来排序数组,我们这里选择实现快排算法。
这里快排的实现主要的要点是 随机选择基准元素+三路划分。
O(nlog(n)) 。具体的三路划分思路与75. 颜色分类类似,使用三个指针
来划分区间:
:小于
的区间
等于
的区间
待处理的区间
大于
的区间
代码实现
class Solution {
public:
//随机选择基准元素
int getRandom(vector<int>& nums, int l, int r)
{
int randomNum = rand();
return nums[randomNum % (r - l + 1) +l];
}
//对区间[l, r]的数组进行快排
void qsort(vector<int>& nums, int l, int r)
{
//当区间无效或者区间元素个数为1时,退出递归
if(l >= r) return;
//生成随机选择的key
int key = getRandom(nums, l, r);
//初始化三个指针
int left = l - 1, right = r + 1, cur = l;
while(cur < right)
{
if(nums[cur] < key) swap(nums[++left], nums[cur++]);
else if(nums[cur] == key) cur++;
else swap(nums[--right], nums[cur]);
}
//分别对左区间和右区间进行快排
qsort(nums, l, left);
qsort(nums, right, r);
}
vector<int> sortArray(vector<int>& nums) {
//生成随机数的种子
srand(time(NULL));
qsort(nums, 0, nums.size() - 1);
return nums;
}
};,递归的空间开销,某些极端情况下会退化成
题目链接:215. 数组中的第K个最大元素
题目描述:
给定一个整数数组 nums 和一个整数 k,返回数组中第 k个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例1:
[3,2,1,5,6,4], k = 25示例2:
[3,2,3,1,2,4,5,5,6], k = 44提示:
k <= nums.length <= 105nums[i] <= 104解题思路
题目要求的第
大元素就是位置为
元素,我们可以利用快排的思想来构造快速选择算法。
、等于
、大于
,根据各区间的长度确定第
大的元素处在哪个区间,从而再去目标区间寻找第
大元素。
、等于
、大于
的区间长度分别为
。
,说明第
大元素在大于
的区间,再去该区间递归寻找。
,说明第
大元素在等于
的区间,直接返回该区间的任意一个元素即可。
,说明第
大元素在小于
的区间,再去该区间递归寻找第
大的元素。
代码实现
class Solution {
public:
int GetRandomNum(vector<int>& nums, int l, int r)
{
return nums[rand() % (r - l + 1) + l];
}
int quickSelect(vector<int>& nums, int l, int r, int k)
{
if(l == r) return nums[l];
//随机选择+三路划分
int key = GetRandomNum(nums, l, r);
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]);
}
//[l, left] [left + 1, right - 1] [right, r]
//将各个区间长度与k比较从而确定目标元素在哪个区间
int b = right - left - 1, c = r - right + 1;
if(c >= k) return quickSelect(nums, right, r, k);
else if(b + c >= k) return nums[left + 1];
else return quickSelect(nums, l, left, k - b -c);
}
int findKthLargest(vector<int>& nums, int k) {
srand(time(NULL));
return quickSelect(nums, 0, nums.size() - 1, k);
}
};,递归的额外空间开销
题目链接: 剑指 Offer 40. 最小的k个数
题目描述:
设计一个算法,找出数组中最小的 k 个数。以任意顺序返回这k个数均可。
示例:
arr = [1,3,5,7,2,4,6,8], k = 4[1,2,3,4]提示:
len(arr) <= 1000000 <= k <= min(100000, len(arr))解题思路
这道题与上一道题类似,使用快速选择算法。
进行一次快排操作后(随机选择基准元素+三路划分),根据各个区间的长度确定前
小的元素在哪个区间,再去该区间递归寻找。
小于
、等于
、大于
的区间长度分别为
:
,说明前
小元素在小于
的区间,再去该区间递归寻找。
,说明前
小元素刚好在小于
的区间或者横跨小于
和等于
的区间,我们直接返回即可。
,说明前
小元素在大于
的区间,再去该区间递归寻找前
小的元素。
代码实现
class Solution {
public:
int getRandom(vector<int>& arr, int l, int r)
{
return arr[rand() % (r - l + 1) + l];
}
//将数组arr的[l, r]范围的前k个小的元素排列到数组的[l, l + k - 1]位置,不考虑顺序
void quickSelect(vector<int>& arr, int l, int r, int k)
{
if(l >= r) return;
int key = getRandom(arr, l, r);
int left = l - 1, right = r + 1, i = l;
while(i < right)
{
if(arr[i] < key) swap(arr[++left], arr[i++]);
else if(arr[i] == key) i++;
else swap(arr[--right], arr[i]);
}
//[l, left] [left + 1, right - 1] [right, r]
int a = left - l + 1, b = right - left - 1;
if(a > k) quickSelect(arr, l, left, k);
else if(a + b >= k) return;
else quickSelect(arr, right, r, k - a - b);
}
vector<int> smallestK(vector<int>& arr, int k) {
srand(time(NULL));
//将数组arr前k个小的元素排列到数组的[0, k - 1]位置,不考虑顺序
quickSelect(arr, 0, arr.size() - 1, k);
return {arr.begin(), arr.begin() + k};
}
};,随机选择基准值大大降低了效率退化的可能性
,递归的额外空间开销。
题目链接: 912. 排序数组
题目描述:
给定一个整数数组 nums,请将该数组按升序排列。
你必须在 不使用任何内置函数 的情况下解决问题,时间复杂度为 O(nlog(n)),并且空间复杂度尽可能小。
示例 1:
nums = [5,2,3,1][1,2,3,5]示例 2:
nums = [5,1,1,2,0,0][0,0,1,1,2,5]提示:
nums.length <= 5 * 104nums[i] <= 5 * 104解题思路
我们直接使用归并排序来排序数组即可。
归并排序的过程:
对划分的左右区间进行排序。
数组,由于是合并成升序,因此小的元素先转移到
数组。
数组赋值回原来的数组
的对应位置。
因为
要递归调用,我们需要确定递归的出口,当传入的区间元素个数为
个时或者区间无效时,递归结束。
代码实现
class Solution {
public:
//合并两个有序数组需要一个临时数组
vector<int> tmp;
void mergeSort(vector<int>& nums, int left, int right)
{
if(left >= right) return;
//1.求中点划分区间
int mid = (right - left) / 2 + left;
//[left, mid] [mid + 1, right]
//2.递归调用mergeSort对两个区间进行排序
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
//3.利用tmp合并两个数组,合并成升序
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.将tmp数组赋值回nums的对应位置
for(int j = left; j <= right; j++)
nums[j] = tmp[j - left];
}
vector<int> sortArray(vector<int>& nums) {
int n = nums.size();
tmp.resize(n);
mergeSort(nums, 0, n - 1);
return nums;
}
};,但是一般忽略掉
,所以为
题目链接: 剑指 Offer 51. 数组中的逆序对
题目描述:
在一个数组中的两个数字,如果前面的一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
record = [9, 7, 5, 4, 6]8限制:
0 <= record.length <= 50000解题思路
计算数组中的逆序对,可以将数组划分两个区间,分别计算出两个区间对应的逆序对的数量,再计算两个区间缺失的逆序对,最后将得到的数量全部加起来就能得到整个数组中的逆序对的数量,而缺失的逆序对数量主要由于前一个区间的某个元素可能大于后一个区间的某个元素,也能构成逆序对。
因此,这道题我们基于归并排序来解决。
函数的作用是返回数组
的
范围内的逆序对的个数,并且将数组排序好。
进行划分区间,并对得到的区间递归调用
函数,获取了两个区间的逆序对的数量且对两个区间进行排序(假设为升序)。
,不更新逆序对的数量,将
赋值给
数组,然后
。
,由于两个区间已经是升序,区间
内的元素都大于
,更新逆序对的数量,并且将
赋值给
数组,然后
。
赋值回原数组对应的位置,最后返回逆序对的数量。
代码实现
class Solution {
public:
//用于合并两个数组所用到的临时数组
int tmp[50005];
//返回数组record的[left, right]范围内的逆序对的个数,并且将数组排序好
int Merge(vector<int>& record, int left, int right)
{
if(left >= right) return 0;
//记录逆序对的数量
int ret = 0;
//1.计算中点划分区间
int mid = (right - left) / 2 + left;
//[left, mid] [mid + 1, right]
//2.对两个区间排序并计算其逆序对的数量
ret += Merge(record, left, mid);
ret += Merge(record, mid + 1, right);
//3.合并两个有序区间的元素并统计缺失逆序对的数量
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid && cur2 <= right)
{
if(record[cur1] <= record[cur2])
{
tmp[i++] = record[cur1++];
}
else
{
//更新逆序对的数量
ret += mid - cur1 + 1;
tmp[i++] = record[cur2++];
}
}
while(cur1 <= mid) tmp[i++] = record[cur1++];
while(cur2 <= right) tmp[i++] = record[cur2++];
//4.将tmp数组赋值回原来的数组
for(int j = left; j <= right; j++)
record[j] = tmp[j - left];
return ret;
}
int reversePairs(vector<int>& record) {
return Merge(record, 0, record.size() - 1);
}
};题目链接: 315. 计算右侧小于当前元素的个数
题目描述:
给你一个整数数组 nums ,按要求返回一个新数组 counts。
数组 counts 有如下性质:counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
示例 1:
nums = [5,2,6,1][2,1,1,0] 5 的右侧有 2 个更小的元素 (2 和 1)2 的右侧仅有 1 个更小的元素 (1)6 的右侧有 1 个更小的元素 (1)1 的右侧有 0 个更小的元素示例 2:
nums = [-1][0]示例 3:
nums = [-1,-1][0,0]提示:
nums.length <= 105nums[i] <= 104解题思路
这道题与上一道题类似,一样基于归并排序去解决。
函数是对数组进行排序,计算每个元素右侧小于当前元素的个数并且记录在返回数组
。注意,这里排的是降序,便于快速统计右侧小于当前元素的个数
函数
问题来了,如何正确去修改
中的值?
因为递归调用 MergeCount 函数,两个区间的元素已经是有序了,相较于原来未排序的数组,每个元素已经已经不是对应原来的下标了,无法得知当前元素在未排序前的下标,从而导致无法修改
数组中右侧小于当前元素的个数
解决方法是创建一个与
数组同等大小的
数组,用于记录未排序之前每个元素对应的下标,让
数组与
数组同时参与归并排序,实现
数组元素与其原来的下标的绑定效果。
具体如何实现同时参与归并排序请参考代码实现。
代码实现
class Solution {
public:
//记录结果的数组
vector<int> ret;
//记录数组未排序之后每个元素的下标
vector<int> index;
//用于合并两个有序数组的辅助数组
int tmpNums[100005];
//用于合并两个"排序"后index数组的辅助数组
int tmpIndex[100005];
void MergeCount(vector<int>& nums, int left, int right)
{
if(left >= right) return;
//1.计算中点划分区间
int mid = (right - left) / 2 + left;
//[left, mid] [mid + 1, right]
//2.左右区间调用Merge
MergeCount(nums, left, mid);
MergeCount(nums, mid + 1, right);
//3.合并+修改左区间元素对应ret数组的元素值
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];
i++; cur2++;
}
else
{
//通过index数组找到当前元素的未排序之前的下标
ret[index[cur1]] += right - cur2 + 1;
tmpNums[i] = nums[cur1];
tmpIndex[i] = index[cur1];
i++; cur1++;
}
}
while(cur1 <= mid)
{
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
while(cur2 <= right)
{
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}
//4.将tmpNums和tmpIndex数组恢复到nums数组和index数组
for(int i = left; i <= right; ++i)
{
nums[i] = tmpNums[i - left];
index[i] = tmpIndex[i - left];
}
}
vector<int> countSmaller(vector<int>& nums) {
int n = nums.size();
ret.resize(n);
index.resize(n);
for(int i = 0; i < n; ++i) index[i] = i;
MergeCount(nums, 0, n - 1);
return ret;
}
};题目链接: 493. 翻转对
题目描述:
给定一个数组 nums,如果 i < j 且 nums[i] > 2 * nums[j],我们就将 (i, j) 称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
示例 1:
[1,3,2,3,1]2示例 2:
[2,4,3,5,1]3注意:
50000。解题思路
与上两道题的思路基本一致,还是基于归并排序去解决。
与上两道题不同的是,本道题不能与归并排序完美适配,不能在合并数组时候统计翻转对的数量,因为翻转对的要求是前一个数大于后一个数的两倍。
所以我们在对划分出来的两个区间递归调用完 Merge 函数后,就直接统计翻转对的数量,统计完后再合并数组。
这里我们采用将数组合并成降序,当然,合并成升序同样能解决。
代码实现
class Solution {
public:
//用于合并
int tmp[50005];
int Merge(vector<int>& nums, int left, int right)
{
if(left >= right) return 0;
int ret = 0;
//1.求中点划分区间
int mid = (right - left) / 2 + left;
//[left mid] [mid + 1, right]
//2.对划分出来的两个数组递归调用Merge
ret += Merge(nums, left, mid);
ret += Merge(nums, mid + 1, right);
//3.统计翻转对的数量
int cur1 = left, cur2 = mid + 1, i = 0;
while(cur1 <= mid)
{
while(cur2 <= right && nums[cur1] / 2.0 <= nums[cur2]) 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++];
//5.将tmp数组的每个元素恢复到原来数组的对应地方
for(int j = left; j <= right; ++j)
nums[j] = tmp[j - left];
return ret;
}
int reversePairs(vector<int>& nums) {
return Merge(nums, 0, nums.size() - 1);
}
};Have a good day😏
See you next time, guys!😁✨🎞