一、堆排
堆排结构就是一个完全二叉树,思路是创建大根堆即最大的值在堆顶,或是小根堆即最小的值在堆顶,一般可用优先队列来模拟堆。
1.自己创建堆
以力扣215. 数组中的第K个最大元素为例.
示例
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
在这个过程中,需要将数组转化堆的形式重新排列,即完全二叉树的形式排列。
先得到heapsize,可以看作树的节点数,完全二叉树的叶节点是总节点数的n/2或(n-1)/2,在堆建立过程中我们只需要排序根节点,所以bulidmaxheap()函数保证了这一点。
紧接着,要做的就是让根节点和左孩子右孩子节点相比较,在三者中选出最大值作为根节点,递归这一操作,保证最后整个树的根节点是最大值,maxheapify()函数执行这一步。
创建完堆后,因为要找的是第K大的元素,所以需要删除并重建堆来不断更新堆顶元素,以找到我们想要的目标,findKthLargest()的for循环里做了这件事。其中具体的操作是,每次将堆最后的叶子节点(nums[n-1],这里的n表示nums.size())和堆顶元素交换,并且将heapsize-1,这样可以看作整个堆删除了原来的堆顶元素,然后进入maxheapify()函数,重建堆,删除k-1次后的堆顶元素就是题目所要求的值。
class Solution {
public:
void maxheapify(vector<int>&arr, int i, int heapsize) {
int lchild = i * 2 + 1;//左孩子节点的下标
int rchild = i * 2 + 2;//右孩子节点的下标
int maxnum = i;//记录根左右三者中最大值的下标
if (lchild<heapsize && arr[lchild]>arr[maxnum])
{
maxnum = lchild;
}//如果这个根节点有左孩子并且左孩子的值大于根节点的值,更新最大值下标
if (rchild<heapsize && arr[rchild]>arr[maxnum])
{
maxnum = rchild;
}//如果这个根节点有右孩子并且右孩子的值大于最大值下标的值,更新最大值下标
if (maxnum != i)
{
swap(arr[i], arr[maxnum]);
maxheapify(arr, maxnum, heapsize);
}//如果最大值下标有所改变,替换根节点的值和最大值下标的值,并且进入递归挨个创建堆
}
void bulidmaxheap(vector<int>&arr, int heapsize)
{
for (int i = heapsize / 2; i >= 0; --i)//剔除叶子节点,保证从根节点开始建立堆
{
maxheapify(arr, i, heapsize);
}
}
int findKthLargest(vector<int>& nums, int k) {
int heapsize = nums.size();//堆容量
bulidmaxheap(nums, heapsize);//进入创建堆函数
for (int i = nums.size() - 1; i >= nums.size() - k+1; --i) {
swap(nums[0], nums[i]);
--heapsize;
maxheapify(nums, 0, heapsize);//删除堆顶元素并重建堆
}
return nums[0];
}
};
2.用优先队列
优先队列的定义
priority_queue<type,container,function>q;
第一项表示数据类型,第二项表示实现堆的容器,并且必须是用数组实现的容器,默认是vector,第三项表示比较方式
for example
priority_queue<int>q;//默认大根堆
priority_queue<int,vector<int>,greater<int>>//小根堆,升序队列
priority_queue<int,vector<int>,less<int>>//大根堆,降序队列
常常会出现自定义类型的数据传入优先队列,这时候function需要做一些改变。
参考这个网址上的内容
优先队列常规操作和队列类似
push()
pop()
size()
empty()
top()
emplace()
以力扣347.前 K 个高频元素为例.
示例:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
首先用哈希表存储每个元素的出现频率
(关于这点,这里也想过vector数组也能做同样的事,为什么要用unordered_map,然后发现进入for循环后有个直接带下标赋值的过程,如果是vector没有初始化的话是不能直接赋值的,要是初始化的话又不知道初始化元素个数到底是多少,但是map初始值默认为0,可以直接赋值,具体底层两个容器结构是怎么实现的也不是很懂…也不知道具体对不对…需要后续详细了解和学习了)
然后将哈希表的值以pair<int,int>的形式push进优先队列
再弹出优先队列的队顶元素,并存储在res数组中,找到题目所需的前k个值。
struct cmp{
bool operator()(pair<int,int> a,pair<int,int> b){
return a.second<b.second;
}
};//自定义数据类型
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
vector<int>res;//存储结果的数组
using e=pair<int,int>;//自定义的数据类型
priority_queue<e,vector<e>,cmp>que;//定义优先队列
unordered_map<int,int>count;//存储每个元素出现频率的哈希表
int n=nums.size();
for(int i=0;i<n;i++)
{
count[nums[i]]++;
}
for(auto i:count)
{
que.push(make_pair(i.first,i.second));
}
while(k--)
{
int x=que.top().first;
que.pop();
res.push_back(x);
}
return res;
}
};
二、快排
快排运用分治的思想,总的来说就是让比index(假设其为要找的值)小的子数组排在index的一边,比index大的子数组排在index的另一边。
还是以力扣215. 数组中的第K个最大元素为例.
首先srand(time(0))表示随机种子,力扣官方题解里面引入随机化来加速递归过程,它的时间代价的期望是 O(n),然而rand()函数提供的值是不变的,所以引入随机时间的种子,使得rand()函数值有所改变。
randompartition()函数随机选择数组中的值作为暂时的index,并将其和最右边的值对换位置,使index位于最大值的位置。
sonnum()函数执行最主要的操作,即将比index大的值和小的值分别放在两边。
quickselect()找到index,此时下标值就是该值在数组中排序的位置,如果不是则进入递归。
class Solution {
public:
int quickselect(vector<int>&nums,int l,int r,int k)
{
int q=randompartition(nums,l,r);
if(q==k)
{
return nums[q];
}
else
{
return q<k?quickselect(nums,q+1,r,k):quickselect(nums,l,q-1,k);
}
}
inline int randompartition(vector<int>&nums,int l,int r)
{
int i=rand()%(r-l+1)+l;//随机一个值
swap(nums[i],nums[r]);//将其置于最右边
return sonnum(nums,l,r);
}
inline int sonnum(vector<int>&nums,int l,int r)
{
int x=nums[r];
int i=l-1;
for(int j=l;j<r;j++)
{
if(nums[j]<=x)
{
swap(nums[++i],nums[j]);
/*注意这里必须用++i,不能用i++,++i才能用作左值,因为第一步,i初始值为-1
++i是先+1再赋值,而i++是赋值再+1,而nums[-1]不存在,所以代码会报错*/
}
}//把比index大的值排到后面来,比index小的值排到前面去
swap(nums[i+1],nums[r]);//交换比index大的值和index的位置
return i+1;//这时候index的位置代表index在原数组中真实的排序位置
}
int findKthLargest(vector<int>& nums, int k) {
srand(time(0));//随机种子
return quickselect(nums,0,nums.size()-1,nums.size()-k);
}
};