目录
[4、剑指Offer 40. 最小的k个数](#4、剑指Offer 40. 最小的k个数)
[6、LCR 170.交易逆序对的总数](#6、LCR 170.交易逆序对的总数)
分治:分而治之
将一个大问题,转化为若干个相同或相似的小问题,然后在子问题的基础上再把它划分为更小的相同或相似的小问题。
1、75.颜色分类
数组划分三块、随机选择基准元素
解法:思想是"双指针" 但是比双指针多了一个指针 ---"三指针"

cpp
class Solution
{
public:
void sortColors(vector<int>& nums)
{
int n = nums.size();
int left = -1, right = n, i = 0;//left指向负一 right指向n
while(i < right)
{
if(nums[i] == 0) swap(nums[++left],nums[i++]);
else if(nums[i] == 1) i++;
else swap(nums[--right], nums[i]);
}
}
};
2、912.排序数组

【面试的时候可能会手撕快排】
利用数组划分实现快速排序
快速排序---算一个基准元素,左边的小于等于key,右边的大于key ,key的位置就是他排序后的位置。 然后,再对左边的区间,选一个基准元素(key1),元素的左边为小于等于这个基准元素(key1)的,右边为大于这个基准元素(key1)的。 右边区间和左边区间的做法相同。 快排最重要的就是数据划分这一步。-----但是数组重复的话,时间复杂度就变为O(n^2)
1、用数组分三块的思想来实现快排
将数组分为小于key,等于key,大于key。 等于key的区间就是已经确定了,再去小于key和大于key的区间排序。
用指针i、left、right 三个指针,用i遍历整个数组,left指针标记小于key的区域最右边,right标记大于key区域的最左边
数组重复的话,只需一次就可以解决问题。相比于数组划分两次是超级快的。---时间复杂度是O(n)

如何优化:用随机的方式选择基准元素。
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)//l是数组0位置, r是数组最后一个位置
{
if(l > r) return;
//数组分三块
int key = getRandom(nums, l, r);//随机的方式生成基准元素
int i = l, left = l - 1,right = r + 1;
while(i < right)//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];//生成随机基准元素的公式
}
};
3、215.数组中第k个最大元素

这个题其实是一个TOP K 问题
top k问题的几种问法: 1、第K大 2、第K小,(返回1个元素)
3、前K大 4、前K小 (数组排完序之后找出前K个大或者小的元素)
不管什么问法,涉及到TOP K问题,他的解法就是固定的。
堆排序 和 快速选择算法
快速选择算法就是基于快排去实现的
堆排序解决问题:
cpp
class Solution
{
public:
int findKthLargest(vector<int>& nums, int k)
{
//建一个k个元素的小堆
priority_queue<int, vector<int>, greater<int>>
pq(nums.begin(), nums.begin()+k);
for(int i = k; i < nums.size(); ++i)
{
if(nums[i] > pq.top())
{
pq.pop();
pq.push(nums[i]);//将 nums[i] 插入堆中,保持堆的大小为 k,因为小顶堆中最小的元素总是堆顶元素,所以这样可以确保堆中始终保存的是当前找到的前 k 大的元素。
}
}
return pq.top();
}
};
快速选择算法:时间复杂度是O(N)

快速选择算法是基于快排来实现的。首先利用选择出来基准元素将数组分三块,然后根据每一块元素的个数来和k进行比较。因为我们要找第k大,所以现从大的部分写这样好写,当c>=k,也就是在大数的区域的数的个数比K值大,那就当然啦,我们就去right到f区间去找就好了。下面两个情况意思相同。【根据数量划分,只用去某一个区间去寻找最终结果】
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];
//1、随机选择基准元素
int key = getRandom(nums, l, r);//在l到r这段区间上给我随机返回一个数就可以
//2、根据基准元素将数组分三块
int left = l - 1, right = r + 1, i = l;//left和right先指向-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]);
}
//3、分情况讨论去哪个区间去寻找的问题
int c = r - right + 1;
int b = right - 1 -(left + 1) + 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);//去l到left区间里面去找k-b-c大的元素
}
int getRandom(vector<int>& nums, int left, int right)
{
return nums[rand() % (right - left + 1) + left];//right - left + 1是一个偏移量,偏移量+left找到一个下标
}
};
给qsort函数传进行一个l和r,代表数组的开始和结束位置。
但是在进行快排的时候,left指向数组的-1,right指向数组最后一个元素的下一个位置。
1、取基准元素 2、利用快排思想将数组分三块 3、分情况讨论 再去进行递归
4、剑指Offer 40. 最小的k个数
解法一:排序--调用容器,直接进行排序,排完序之后把最小的数拿出来就好了。【时间复杂度 NlogN】
解法二:利用堆。 找最小的k个数,我们可以创建大小为k的大根堆。【时间复杂度 Nlogk】
解法三:快速选择算法。【时间复杂度N】
所以来说,快速选择算法的时间复杂度是最优的
当分类讨论第三种情况的时候,我们直接就去大于key的区间去找k-a-b个较小的元素就好了,这个时候其实a和b区间还是没有排序的,我们其实就不用排序,直接输出就好了。【我们选择基准元素的时候是等概率划分的】
cpp
class Solution
{
public:
vector<int> getLeastNumbers(vector<int>& nums, int k)
{
srand(time(NULL));
qsort(nums, 0, nums.size() - 1, k);
return {nums.begin(), nums.begin() + k};
}
void qsort(vector<int>& nums, int l, int r, int k)
{
if(l >= r) return;
//1、随机选择一个基准元素+数组分三块
int key = getRanddom(nums, l, r);
int left = l - 1, right = r + 1, i = l;//i=l去遍历数组
while(i < right)
{
if(nums[i] < key) swap(nums[++left], nums[i++]);
else(nums[i] == key) i++;
else swap(nums[right--], nums[i]);
}
//现在数组已经分为三块进行分类讨论
int a = left - l + 1;
int b = right - 1 - (left + 1) + 1;
if(a > k) return qsort(nums, l, left, k);
else if(a + b >= k) return;
else qsort(nums, right, r, k - a - b);
}
int getRanddom(vector<int>& nums, int l, int r)
{
return nums[rand() % (r - l + 1) + l];
}
};
5、分治---归并排序

用排序数组这道题讲归并排序。
归并排序的原理:
根据中间点mid把数组分成两部分,然后先把左边部分排序,排左边部分的时候相当于又是一个排序,一直往下分开,知道数组只有一个元素的时候,就向上返回。去右边排序,再向上返回,当两边都排完序之后,就合并两个有序数组,那么这一层就变为一个有序数组了,然后再返回到上一层,直到返回到第一层,那么数组的左边就已经排完序了。右边排序和左边排序的方法相同。

快排:

快排和归并排序算法其实是非常类似的,只是处理数组的时机不同。
归并排序是:先排左边,再排右边,然后再合并两个。就相当树的后序遍历。
快排:先把数组分三块,然后再把左边的分三块,再把右边的分三块。(先把分好,然后去左边分,分好再分右边)
cpp
class Solution
{
vector<int> tmp; // 1. 定义私有成员变量tmp:临时数组,用于合并两个有序子数组时暂存数据
public:
// 2. 对外暴露的排序接口函数,输入待排序数组nums,返回排序后的数组
vector<int> sortArray(vector<int>& nums)
{
tmp.resize(nums.size()); // 2.1 初始化临时数组大小,和待排序数组nums一致
mergeSort(nums, 0, nums.size() - 1); // 2.2 调用归并排序核心函数,排序整个数组(区间[0, nums.size()-1])
return nums; // 2.3 返回排序后的数组 ---我们用上面的函数将nums排完序,再返回nums数组就好了
}
// 3. 归并排序核心函数:递归排序nums中[left, right]区间的元素
void mergeSort(vector<int>& nums, int left, int right)
{
// 3.1 递归终止条件:区间只有1个元素(left==right)或无元素(left>right),无需排序
if(left >= right) return;
// 3.2 划分区间:计算中间点mid,把[left, right]拆分为[left, mid]和[mid+1, right]
// (left + right) >> 1 等价于 (left + right)/2,位运算效率更高(题目保证无溢出)
int mid = (left + right) >> 1;
// 3.3 递归排序左半区间[left, mid]
mergeSort(nums,left, mid);
// 3.4 递归排序右半区间[mid+1, right]
mergeSort(nums,mid + 1, right);
// 3.5 合并两个有序子数组:[left, mid]和[mid+1, right]都是有序的,合并为整体有序
int cur1 = left; // cur1:左子数组的起始指针(指向[left, mid]的当前元素)
int cur2 = mid + 1; // cur2:右子数组的起始指针(指向[mid+1, right]的当前元素)
int i = 0; // i:临时数组tmp的下标指针(记录当前填充位置)
// 3.5.1 双指针遍历两个子数组,按升序填充到tmp中
while(cur1 <= mid && cur2 <= right)
{
// 取较小的元素放入tmp,同时移动对应指针
tmp[i++] = nums[cur1] <= nums[cur2] ? nums[cur1++] : nums[cur2++];
}
// 3.5.2 处理左子数组剩余元素(如果cur1还没遍历完[left, mid])
while(cur1 <= mid) tmp[i++] = nums[cur1++];
// 3.5.3 处理右子数组剩余元素(如果cur2还没遍历完[mid+1, right])
while(cur2 <= right) tmp[i++] = nums[cur2++];
// 3.6 把tmp中合并好的有序数据,复制回原数组nums的[left, right]区间
for(int i = left; i <= right; i++)
{
// tmp的下标是从0开始的,nums的区间是[left, right],所以用i-left映射tmp的下标
nums[i] = tmp[i - left];
}
}
};
【保留草稿】

理解:这个归并排序对数组的处理时机就相当于树的后续遍历,也就决定了递归的顺序。即,先递归左边再递归右边,然后处理跟【就会决定代码写的顺序】
cpp
// 3.5.2 处理左子数组剩余元素(如果cur1还没遍历完[left, mid])
while(cur1 <= mid) tmp[i++] = nums[cur1++];
// 3.5.3 处理右子数组剩余元素(如果cur2还没遍历完[mid+1, right])
while(cur2 <= right) tmp[i++] = nums[cur2++];
解释:首先肯定这两个while循环只能进去一个,那么为什么没有移动完的指针,的那边指针后面的元素可以直接放到tmp呢?答案是:因为如果递归到最后的话比如说只有两个元素的时候,判断哪个小直接放进tmp数组中,另一边剩的元素直接放进去,然后就会把这个升序排序的两个元素给返回上一层,那么对于这一层的左边其实就已经是升序的了。那么右边会和左边一样,也会返回同样升序的数组,再对这一行的两个升序的数组进行合并,不就是对元素进行逐一比较谁小谁放进tmp,然后指针没有移动到最后的那一边,后面的肯定是要比另一边的所有大的,所以直接放到tmp中就好喽~
6、LCR 170.交易逆序对的总数

解法一:暴力枚举
拿一个数,在后面这段区间里面找找有多少比他小,再固定下一个数,找找有多少个数比他小。
两层for循环即可(肯定会超时)
解法二:归并排序
思维流程---如何想到要用归并排序
1、左半部分->右半部分->左一右一
求逆序对的时候,将数组分为两部分,左边和右边,先求左边有几组逆序对(eg:a对),再求右边有几组逆序对(b个),再在左边选一个数,右边选一个数,看有几组逆序对(c个)。
那么,a + b + c就是总的逆序对个数 【其实本质上还是一个暴力枚举】
2)左半部分-->左排序--->右半部分--->右排序--->左一右一
先将数组按中间值分为两部分,左边挑出来a个,把左部分排有序(这个时候是不影响挑出出来的a组的,因为是在挑完之后排序的),右边挑出来b个,然后排序,然后一左一右挑,虽然两边已经被排完序了,但是这个时候挑并不会影响挑出来的组数,因为左右挑我们是不关心左边右边的顺序的。
我们可以通过前两步分析出来,这道题是用分治的方法来解决的。继续分析又得知,如果数组有序的话我们就可以统计出来一大堆
我们为了让归并递归的算法统一,所以给左一右一也加一个排序
策略一:找出该数之前,有多少个比我大 这个策略应该使用升序
策略二:找出该数之后有多少个数比我小,这个策略应该使用降序
这道题其实用策略一的升序和策略二的降序都可以实现。
下面代码使用升序来解决(找出该值前面所有比他大的)
cpp
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)
{
int ret = 0;
//处理边界情况 -- 没有逆序对,直接返回0
if(left >= right) return 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;//i = 0帮助我们来遍历辅助数组
//策略一:找前面比我大的数(升序)
while(cur1 <= mid && cur2 <= right)
{
if(nums[cur1] <= nums[cur2])
{
tmp[i++] = nums[cur1++];
}
else //nums[cur1] > nums[cur2] --升序cur1值第一次大于cur2值,cur1后面的值全都比cur大
{
ret += mid - cur1 + 1;
tmp[i++] = nums[cur2++];//完成排序和cur指针移动---因为我们排升序,所以先把小的放到数组
}
}
//处理一下排序 //把剩的那个放到数组中
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;
}
};


先合并5、6 返回给父节点,再合并7、8返回给父节点,再合并3、4返回给父节点
cpp
if(nums[cur1] <= nums[cur2])//这个不满足条件,谁小谁移动,cur1++
{
tmp[i++] = nums[cur1++];
}
【代码解释】为什么当cur1的值小于等于cur2的值就能把cur1的值直接放进tmp数组中?难道不会出现cur2后面还有元素比cur2的值更小吗(这个后面指的是[cur2, right]这个区间,即要进行合并的部分,这只是数组的一部分。
肯定不会出现,因为,要进行合并的两个数组肯定是先进行过排序的【因为在递归中,先进行判断看不是符合逆序对的要求,再进行将小的先放到tmp数组,返回过来要进行合并的两个区间的数都是已经经过排序的!!!这点很重要。 每次要进行数组合并之前,都会拿到nums数组,只是每次要合并的区间不一样,区间是不断扩大的,父节点要进行合并操作的数组范围肯定是包含了两个孩子的区域的,只是将两个区域的顺序重新进行排序(升序),其实在排序过程中,就会完成左右区间各取一个时的逆序对的取值。
7、315.计算右侧小于当前元素的个数

题目意思:找出这个位置的后面有多少个数比我小
这个不就是我们上一道题的策略二吗,找出该值后面比他小的数,因此,我们知道用降序来解决这道题比较好。(前面大,后面小)--- 那么救谁大谁移动
nums[cur1] <= nums[cur2] :因为要找的是cur2的值小,这个就是不符合条件的,所以我们没有在后面没有找到比这个值小的元素,所以cur2++
nums[cur1] > nums[cur2] :那么就是找到了,cur2后面的所有元素都比cur1小,cur1++
我们需要找到(nums[cur1]),给原始下标的位置ret+=right-cur2+1
所以,最重要的就是我们要找到当前元素**(nums[cur1])**的原始下标,合并的时候其实是在左边拿一个右边拿(说到底其实都是左边拿一个右边拿一个,因为会分到只有一个元素,再从下往上合并),所有,当左拿一右拿一的时候,元素的位置可能就不是元素的时的位置了,因为进行排序过了。
我们可以创建一个index数组,数组的元素就是原始数组的下标,将原始数组元素和数组下标进行绑定,这样元素进行排序移动的时候,就可以很快找到元素的原始x
cpp
class Solution
{
//这些放在全局就不用给mergeSort函数中传参了
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数组--存nums数组的原始下标
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;
//2、数组被划分为 [left, mid] [mid+1, right]
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];//tmpNums数组进行操作的时候,tmpIndex也要同步操作
tmpIndex[i++] = index[cur2++];
}
else//nums[cur1] > nums[cur2]
{
ret[index[cur1]] += right - cur2 + 1;//因为原始下标和元素是绑定的,所以index[cur2]就是cur2位置的值的下标---
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
}
//4、在while循环之后处理剩下的排序过程
while(cur1 <= mid) //让cur1的值放到两个辅助数组中
{
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
while(cur2 <= right) //让cur1的值放到两个辅助数组中
{
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}
//5、还原两个数组
for(int j = left; j <= right; j++)
{
nums[j] = tmpNums[j - left];
index[j] = tmpIndex[j - left];
}
}
};
cpp
else//nums[cur1] > nums[cur2]
{
ret[index[cur1]] += right - cur2 + 1;//因为原始下标和元素是绑定的,所以index[cur2]就是cur2位置的值的下标---
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
【解释代码】为什么是index[cur1]? 因为这个时候已经找到nums[cur1] > nums[cur2]了,那么我们就知道,此时,cur1值比cur2后面的都大,所以计算的个数就是right - cur2 + 1,而且我们计算的是比cur1值小的所有数的个数,肯定就是index[cur1]喽
8、439.翻转对

前面数大于后面哪个数的两倍 --- 的个数
逆序对的算法是一比一的比较,和归并排序是一样的。

本题是和某一个数的2倍进行比较。和归并排序算法是不一样的,我们此时就不能用归并排序思想来解决了。
利用数组有序,来计算反转对----时间复杂度为O(n)---利用单调性,使用双指针
策略一:计算当前元素后面有多少元素的二倍比我小。---看前面元素(用降序排列)
先让cur1不动(盯着cur1看),cur2向后移动,看后面有多少个两倍比我小,看到什么时候cur2值小于cur1值,如果cur2的值的两倍比我大,cur2就往后移动,是一个降序数组,cur2往后移动的时候值才会变小。当cur2移动到某个位置,发现cur2的两倍比cur1的值小,又应为数组是降序的,那么cur2后面的所有数的二倍都比cur1的值小。此时,ret+=right-cur2+1。 那么我们此时就已经找完了当左边部分为cur1的时候右边所有的比他大的数了,就将cur1++。但是此时我们不能将cur2再移动到开始(回退),因为这样时间复杂度就会变为n^2logn,所以,我们要利用一下数组有序的特性,为什么cur2不用回退?
因为,1的值比2大,右边比他小的都要移动到cur2,现在cur1右移了,移动到2了,那么比他小的数肯定不可能是在cur2的左边,所以不用回退,直接让cur2向右移动,知道出现了比cur1值小的位置,就用公式统计。知道cur1或者cur2移动到最后为止。
策略二:计算当前元素前面有多少元素的一半比我大。-----看后面元素(用升序排列)

先计算反转对,再合并两个有序数组。
cpp
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)//归你管cur1
{
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;
}
};


