1、数组中的第K个最大元素(215)
《算法笔记》中介绍过查找第K个顺序统计量的O(n)复杂度算法:即利用快速排序的划分过程,每次可以随机确定一个第 i 大的元素,根据 i 和 k的关系在对应区间继续该过程,直到 i=k。为避免最坏情况下落入O(n^2)复杂度,使用随机选主元方法。
掌握快排的划分过程非常重要!!!
class Solution {
public:
int divide(vector<int>&nums,int l, int r){
if(l>=r) return l;
//rand()%n产生的是[0,n-1]间的整数,这里要产生[l,r],所以加上l
int i = l + rand()%(r-l+1);
swap(nums[i],nums[l]);//随机选主元
int st = l,ed = r, key = nums[l];
while(st<ed){
while(st<ed && nums[ed]<=key){
//由于本题是找第K大的元素,所以把大于主元的放左边区间
--ed;
}
nums[st] = nums[ed];
while(st<ed && nums[st]>=key){
++st;
}
nums[ed] = nums[st];
}
nums[st] = key;
return st;
}
int findKthLargest(vector<int>& nums, int k) {
//srand(time(0))的意思是:用当前时间来设定rand函数所用的随机数产生演算法的种子值
srand(time(0));
k -=1;
int len =nums.size()-1;
int q= divide(nums,0,len);
while(q!=k){
if(k<q){
q = divide(nums,0,q-1);
}else{
q = divide(nums,q+1,len);
}
}
return nums[k];
}
};
语法:随机函数的运用,srand(time(0))获得当前时间作为种子,rand()%n可以获得[0, n-1]间的随机整数。
方法二:使用堆排序方法。
下面先练习堆的一些操作方法。(使用数组方式)
(1)建堆(以大根堆为例)
要求把原来的完全二叉树调整为堆,按完全二叉树的存储方法: i 结点的左孩子为在2i位,右孩子在2i+1位。时间复杂度为O(n)
建堆是从堆的底部开始枚举每个结点,对每个结点自上而下检查是否需要调整(downAdjust函数)。
//这里设heap是从1号位开始存储,共n个结点
//对数组在[l,r]范围内向下调整,其中l为待调整的结点
void downAdjust(vector<int>& heap, int l, int r){
int i = l, j = i*2;//i为待调整结点,j为i结点的左孩子
while(j <= r){//检查i是否有孩子
//如果i还有右孩子,把j更新为最大的孩子结点
if(j+1<=r && heap[j+1]>heap[j]){
++j;
}
//根据孩子和i的大小,决定是否需要调整
if(heap[j] > heap[i]){
swap(heap[j],heap[i]);
//交换过后,要检查底下是否会受影响,也得调整,保持i为待调整结点,j为i结点的左孩子
i = j;
j = i*2;
}else{//没有交换,无需再调整
break;
}
}
}
//建堆
void createHeap(vector<int>& heap){
int n = heap.size()-1;
for(int i=n/2; i>=1; i--){
//由于n/2之后的结点都是叶子结点,没有孩子,所以不需要调整
downAdjust(heap,i,n);
}
}
(2)删除堆顶元素
删除堆顶元素后,用最后一个元素覆盖堆顶,并进行向下调整,时间复杂度log(n)。
//删除堆顶元素
void deleteTop(vector<int>& heap){
heap[1] = heap[n--];
downAdjust(heap,1,n);
}
(3)插入元素
把元素放在最后,然后向上调整,时间复杂度log(n)。
//对数组在[l,r]范围内向上调整,其中r为待调整的结点,l一般设为1
void upAdjust(vector<int>& heap, int l, int r){
int i = r, j = i/2; //i为待调整结点,j为i的父亲
while(j>=l){
if(heap[j] < heap[i]){
swap(heap[i],heap[j]);
i = j;
j = i/2;
}else{
break;
}
}
}
void insert(int x){
heap[++n] = x;
upAdjust(heap,1,n);
}
(4)堆排序。
以递增排序为例,由于大顶堆堆顶存储着最大值,只要每次取出堆顶元素,逆序排列即可。为了节省空间,取出的堆顶放在末尾。这样时间复杂度O(nlgn),空间复杂度O(1)。
void heapSort(vector<int>& heap){
createHeap(heap);//建堆
for(int i=n ; i>1; i--){
swap(heap[i],heap[1]);//倒着枚举,每次取出当前堆顶到后面
downAdjust(heap,1,i-1);//删除堆顶,重新调整堆
}
}
回到本问题,官方解答:建立大顶堆,删除k-1次堆顶后,这时的堆顶就是第k大的元素,注意数组范围是[0,n-1],与之前有所不同。
class Solution {
public:
void downAdjust(vector<int>& nums,int l,int r){
//由于序号从0开始,i结点的左孩子为2i+1,右孩子为2i+2
int i = l, j = i*2+1;
while(j<=r){
if(j+1 <= r && nums[j+1] > nums[j]){
++j;
}
if(nums[i]<nums[j]){
swap(nums[i],nums[j]);
i = j;
j = i*2+1;
}else{
break;
}
}
}
void createHeap(vector<int>& nums){
int n = nums.size();
for(int i=n/2;i>=0;i--){//逆序枚举
downAdjust(nums,i,n-1);
}
}
int findKthLargest(vector<int>& nums, int k) {
createHeap(nums);
int n = nums.size()-1;
//删除k-1次顶点,第K大元素就在堆顶
for(int i=n;i>n-k+1;i--){
swap(nums[0],nums[i]);
downAdjust(nums,0,i-1);
}
return nums[0];
}
};
或者:对前k个元素建立小根堆,再将剩余元素逐个与堆顶相比,如果大于堆顶,则替换堆顶并调整。比较完后,小根堆里只剩下最大的k个元素,且堆顶就是第k大的元素。
class Solution {
public:
void downAdjust(vector<int>& nums,int l,int r){
//由于序号从0开始,i结点的左孩子为2i+1,右孩子为2i+2
int i = l, j = i*2+1;
while(j<=r){
if(j+1 <= r && nums[j+1] < nums[j]){
++j;
}
if(nums[i]>nums[j]){
swap(nums[i],nums[j]);
i = j;
j = i*2+1;
}else{
break;
}
}
}
//对前k个结点建立小根堆
void createHeap(vector<int>& nums,int k){
for(int i=k-1;i>=0;i--){//逆序枚举
downAdjust(nums,i,k-1);
}
}
int findKthLargest(vector<int>& nums, int k) {
createHeap(nums,k);
int n = nums.size();
//剩余元素逐个与堆顶比较,如果大于堆顶则替换
for(int i=k;i<n;i++){
if(nums[i]>nums[0]){
nums[0]=nums[i];//替换原堆顶,重新调整
downAdjust(nums,0,k-1);
}
}
return nums[0];
}
};
2、前 K 个高频元素(347)
思路:先遍历数组,用哈希方法存储每个数字出现次数(可以看作一种桶排序)。之后对这些桶按出现次数进行排序,把前k个元素返回即可。
复杂度分析:遍历进行映射O(n),之后用比较排序方法排序O(mlogm),m为不同数字个数。
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> cnt;//统计每个数字出现次数
int max_count = 0;//统计最大次数
for (const int& num : nums) {
//遍历统计并更新max_count
max_count = max(max_count, ++cnt[num]);
}
// unordered_map本身不支持sort排序,需要转化为pair放入vector中排序
vector<pair<int, int>> temp;
for (const auto& it : cnt) {
temp.push_back(it); //map和unordered_map中元素都以pair形式存在
}
sort(temp.begin(), temp.end(), [](const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;//按键值,即出现次数排序
});
vector<int> ans;
for (const auto& it : temp) {
ans.push_back(it.first);
if (ans.size() == k) {
break;
}
}
return ans;
}
};
语法:unordered_map无法用sort直接排序,但由于其内部元素是用pair形式,可以先转存为vector<pair<int,int>>形式,再进行排序。
这个排序过程也可以自己用桶排序思想试下,把出现次数相同的元素放在一起。但是实测效率不高,可能是测试用例中很多数字出现次数存在很大差别,用vector可能有些浪费,再用unordered_map可能会好些。
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> cnt;//统计每个数字出现次数
int max_count = 0;//统计最大次数
for(const int& num: nums){
//遍历统计并更新max_count
max_count = max(max_count, ++cnt[num]);
}
//按出现次数进行桶排序
vector<vector<int>> bucket(max_count+1);
for(const auto& it : cnt){
//把数字按出现次数放入对应桶内,即出现次数相同的放在一起
bucket[it.second].push_back(it.first);
}
vector<int> ans;
//倒着把bucket内的k个数字加入ans中
for(int i = max_count;i>=0 && ans.size()<k;--i){
for(const int& num : bucket[i]){
ans.push_back(num);
if(ans.size()==k) break;
}
}
return ans;
}
};
进一步思考这个问题,发现在遍历完成个数统计后,问题实质上变成了求一个无序数组的前k个大的元素。这与上一题求第K个统计量非常像,因此也可以用上面的快排思路或者是堆排序。
使用快排思路,也是看确定位次的idx和k的大小关系,如果[left, idx-1]长度大于k,则继续在左边找k个的划分;如果[left, idx]长度小于k,则结果为左边全部元素加上右边区间的前k-(idx - left +1)个元素。
class Solution {
public:
void quickSort(vector<pair<int,int>>& arr,int left,int right,int k){
int pivot = left + rand()%(right - left +1);
swap(arr[left],arr[pivot]);
//划分过程快捷写法,把大的划分到左边
int idx = left,key = arr[left].second;
for(int i=idx+1;i<=right;i++){
if(arr[i].second>key){
swap(arr[i],arr[idx+1]);
++idx;
}
}
swap(arr[idx],arr[left]);
if(idx-left >= k){//在左区间继续划分找出k个数
quickSort(arr,left,idx-1,k);
}
else if(idx - left +1 <k){//左区间全部考虑,再加上右边的k-(idx-left+1)个元素
quickSort(arr,idx+1,right,k-(idx-left+1));
}
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> cnt;//统计每个数字出现次数
int max_count = 0;//统计最大次数
for(const int& num: nums){
//遍历统计并更新max_count
max_count = max(max_count, ++cnt[num]);
}
// unordered_map本身不支持sort排序,需要转化为pair放入vector中排序
vector<pair<int,int>> temp;
for(const auto& it : cnt){
temp.push_back(it); //map和unordered_map中元素都以pair形式存在
}
quickSort(temp,0,temp.size()-1,k);
vector<int> ans;
for(const auto& it : temp ){
ans.push_back(it.first);
if(ans.size()==k){
break;
}
}
return ans;
}
};
使用堆思路。可以像上题那样,对前K个元素建小顶堆,再将剩余元素与堆顶比较,如果大于堆顶,则替换更新。最后,堆里就剩下前k大的元素。
这里,尝试练习用优先队列priority_queue(其底层由堆实现)实现这一思路。
如果当前堆元素小于k,说明堆还没建完,直接插入;否则要比较堆顶决定是否替换。
class Solution {
public:
static bool cmp(pair<int,int>& a, pair<int,int>& b){
//键值大的优先级高,放在队列前面,与sort的cmp排序结果正好相反
return a.second > b.second;
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> cnt;//统计每个数字出现次数
int max_count = 0;//统计最大次数
for(const int& num: nums){
//遍历统计并更新max_count
max_count = max(max_count, ++cnt[num]);
}
priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(&cmp)> q(cmp);
for(auto& [num, count] : cnt){
if(q.size()==k){
if(q.top().second < count){
q.pop();
// 此函数用于将新元素插入优先级队列容器,该新元素将添加到优先级队列的顶部
// 队列之后会按优先级要求自动调整
q.emplace(num,count);
}
}
else{
q.emplace(num,count);
}
}
vector<int> ret;
while (!q.empty()) {
ret.emplace_back(q.top().first);
q.pop();
}
return ret;
}
};
语法:priority_queue的用法
定义方式:priority_queue<Type, Container, Functional>。其中,Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。
ex: priority_queue(int, vector<int>, less<int>> q;less<int>表示数字大的优先级高,放在队首。
代码中priority_queue<pair<int,int>, vector<pair<int,int>>, decltype(&cmp)> q(cmp); 是自己给定比较方式cmp, decltype作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
注意优先队列比较函数定义的是优先级,优先队列默认优先级大的放在队首,这与sort的比较函数正好相反。
优先队列的emplace(num)函数,会把num插入到队首,之后进行调整。
3、根据字符出现频率排序(451)
本题与上一题思路非常类似,先遍历统计字符出现次数,再按频次排序,最后按频次构造结果。
class Solution {
public:
string frequencySort(string s) {
//遍历统计各字符次数
unordered_map<char,int> letter;
for (char c : s){
++letter[c];
}
//转化为vector,按出现次数排序
vector<pair<char,int>> word;
for(const auto& it : letter){
word.push_back(it);
}
sort(word.begin(),word.end(),[](const pair<char,int>& a,const pair<char,int>& b){
return a.second > b.second;
});
//构造结果字符串
string ans = "";
for(const auto& it : word){
int temp = it.second;
while(temp!=0){
ans += it.first;
--temp;
}
}
return ans;
}
};
排序过程的sort也可以用桶排序代替。
class Solution {
public:
string frequencySort(string s) {
//遍历统计各字符次数
unordered_map<char,int> letter;
int max_count = 0;
for (char c : s){
max_count = max(max_count,++letter[c]);
}
//用桶存储对应频次的字母
vector<string> bucket(max_count+1);
for(const auto& it : letter){
bucket[it.second].push_back(it.first);
}
/*也可以这么写
for(const auto& [ch,num] : letter){
bucket[num].push_back(ch);
}
*/
string ans;
for(int i = max_count; i>0; i --){
for(const auto& ch: bucket[i]){
for(int j = 0 ; j<i; j++){
ans.push_back(ch);
}
}
}
return ans;
}
};
4、颜色分类(75)
基本思路:先遍历统计数组中各个数字个数,再按照个数修改数组。共遍历2次数组。
class Solution {
public:
void sortColors(vector<int>& nums) {
int cnt[3] = {0};
for(const int& num : nums){
++cnt[num];
}
int idx = 0;
for(int i=0;i<3;i++){
while(cnt[i]>0){
nums[idx++] = i;
--cnt[i];
}
}
}
};
如何只用一次遍历呢?
方法一:双指针。排序的基本操作是交换元素,这里我们要把0和1交换到数组头部,可以设置两个指针P0和P1,分别交换0和1,其指示当前头部0和1的下一位。开始遍历数组,碰到0就换到P0位置,碰到1就换到P1位置。注意0要在1前面,所以有P0<=P1,但是P0交换时,可能会把0后面的1给交换出去,所以若P0<P1,说明0后面已经有1了,P0交换后,还得再换一遍P1。
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int p0 =0, p1 =0;
for(int i=0; i<n; i++){
if(nums[i]==1){
swap(nums[i],nums[p1]);
++p1;
}
else if(nums[i]==0){
swap(nums[p0],nums[i]);
if(p0<p1){//p0<p1,则p0位的1被换到i位置上了
swap(nums[p1],nums[i]);
}
++p0;
++p1;
}
}
}
};
类似的可以用P0和P2分别去交换0和2,不过P2从末尾向前移动。直到我们遍历位置超过P2。这里要注意的是:当nums[ i ] == 2时,会与P2交换,但是交换过后如果nums[ i ] ==0或2,还得继续进行交换。
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int p0 =0, p2 =n-1;
for(int i=0; i<=p2; i++){
while(i<=p2 && nums[i]==2){
swap(nums[i],nums[p2]);
--p2;
}
if(nums[i] == 0){
swap(nums[i],nums[p0]);
++p0;
}
}
}
};