堆本质上是一种树,也是一种重要的数据结构。堆排序的时间复杂度和归并排序随机快排一样,都是O(n*logn)。
堆其实是一种完全二叉树,完全二叉树就是若按层序遍历整棵树并将每个节点编号,到最后编号是连续的。
由定义可知,若将数组的下标看作节点编号,任何数组都可以表示为一个完全二叉树。所以,通过将数组看作一个堆结构,就可以实现用堆排序一个数组。
大根堆就是在堆结构这个完全二叉树上,任何一颗子树都满足最大值在顶部。以下是将数组建成大根堆的过程。
其中用到的公式有:父节点=(当前节点-1)/2,左孩子=当前节点*2+1,右孩子=当前节点*2+2。
//向上插入
void heapInsert(int i)
{
while(arr[i]>arr[(i-1)/2])
{
swap(i,(i-1)/2);
i=(i-1)/2;
}
}
首先是向上插入,因为大根堆的特点就是最大值在顶部,所以只要当前节点的值比父节点的值还大,就要交换,然后让i来到交换后原来节点的父节点的位置。
可以理解为,向上插入就是孩子不断向上干他父亲的过程。(孝)
//向下调整
void heapify(int i,int size)
{
int l=2*i+1;
while(larr[l]?l+1:l;
best=arr[best]>arr[i]?best:i;
if(best==i)
{
break;
}
swap(best,i);
i=best;
l=2*i+1;
}
}
当某个节点的值发生改变(这里默认减小),肯定要向下和他的孩子进行调整,所以先让变量l来到当前节点左孩子的位置,然后只要l没越界,即存在左孩子,那就设置best变量表示当前节点左孩子和右孩子中值较大的孩子位置,注意还要保证存在右孩子。之后改变best让其等于当前节点和较大孩子中较大数的位置,若best=i,即当前节点比他的左右孩子都大,就说明调整结束,跳出循环;若没有就交换,让i来到best的位置,l继续等于i的左孩子。
可以理解为,向下调整就是父亲没实力不断降辈分的过程。(乐)
与大根堆相反,小根堆就是最小值在顶部。
借助大根堆最大值在顶部的特性,就可以完成对数组的排序。
#include
using namespace std;
//全局变量
#define n 10
int arr[n];
//交换
void swap(int a,int b)
{
int t=arr[a];
arr[a]=arr[b];
arr[b]=t;
}
//向下调整
void heapify(int i,int size)
{
int l=2*i+1;
while(larr[l]?l+1:l;
best=arr[best]>arr[i]?best:i;
if(best==i)
{
break;
}
swap(best,i);
i=best;
l=2*i+1;
}
}
//向上插入
void heapInsert(int i)
{
while(arr[i]>arr[(i-1)/2])
{
swap(i,(i-1)/2);
i=(i-1)/2;
}
}
//堆排序
void heapSort(void)
{
for(int i=0;i1)
{
swap(0,--size);
heapify(0,size);
}
}
int main()
{
for(int i=0;i>arr[i];
}
//堆排序
heapSort();
cout<<"Sorted:"<
首先从顶到底建大根堆,然后设置size表示此时数组中未排序部分的长度,因为大根堆的特性,数组中第一个数必定是最大值,就可以将其交换到size-1的位置。此时堆结构中顶部的值减小,所以要进行向下调整的步骤。最后,当size=1时,整个数组已经有序。
观察发现,由于二叉树顶部的节点个数必然少于底部的节点个数,而从顶到底的建堆方法中,顶部经历的向上插入过程数又少于底部,所以必然会繁琐一点。而从底到顶的建堆方法中,由于是从底部开始,所以底部经历的向下调整过程数会少于顶部,所以会简便很多。
#include
using namespace std;
//全局变量
#define n 10
int arr[n];
//交换
void swap(int a,int b)
{
int t=arr[a];
arr[a]=arr[b];
arr[b]=t;
}
//向下调整
void heapify(int i,int size)
{
int l=2*i+1;
while(larr[l]?l+1:l;
best=arr[best]>arr[i]?best:i;
if(best==i)
{
break;
}
swap(best,i);
i=best;
l=2*i+1;
}
}
//堆排序
void heapSort(void)
{
for(int i=n-1;i>=0;i--)
{
heapify(i,n);
}
int size=n;
while(size>1)
{
swap(0,--size);
heapify(0,size);
}
}
int main()
{
for(int i=0;i>arr[i];
}
//堆排序
heapSort();
cout<<"Sorted:"<
只需要将从0开始遍历数组向上插入的步骤改为从n-1开始向下调整的步骤即可。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) : val(x), next(nullptr) {}
* };
*/
#include
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param lists ListNode类vector
* @return ListNode类
*/
static bool cmp(ListNode* &a,ListNode* &b)
{
return a->val>b->val;
}
ListNode* solve(vectorlists)
{
priority_queue,decltype(&cmp)>heap(cmp);
for(int i=0;inext!=NULL)
{
heap.push(cur->next);
}
tail->next=heap.top();
heap.pop();
tail=tail->next;
cur=tail;
}
return head;
}
ListNode* mergeKLists(vector& lists) {
// write code here
return solve(lists);
}
};
因为要按顺序合并多个有序链表,可以借助小根堆的特性,即最小值在顶部。
因为priority_queue中存放的是节点地址,而又要根据节点数组进行比较,所以借助decltype比较器和cmp比较函数生成小根堆。注意decltype的用法中,若cmp函数返回值为true,则说明b的优先级大于a。
之后先就将每个头节点入堆,之后每次连接小根堆的堆顶,并将该节点的next节点入堆即可。注意排除指针为NULL的头节点。
#include
using namespace std;
static bool cmp(pair&a,pair&b)
{
return a.first>n;
pairinterval;
vector >line;
int a,b;
for(int i=0;i>a>>b;
line.push_back({a,b});
}
sort(line.begin(),line.end(),cmp);
priority_queue,greater >heap;
int ans=0;
for(int i=0;i
这个问题的思路就比较抽象了,思考重合线段的特点,注意到,重合线段的左边界必然是某一条线段的左边界。(注意力惊人)
之后借助小根堆的特性,先根据每条线段的左边界排序,之后遍历所有线段,当堆不为空且堆顶元素小于等于线段的左端点时,弹出堆顶。然后压入线段右端点。最后让ans等于堆大小和自身的较大值即可。
原理大致可以理解为,堆顶是某条线段右边界的最小值,若该数值小于等于新线段的左边界,则说明堆顶线段已经不包括在重合线段的范围内,就弹出,然后入堆新线段的右边界。最后堆大小的最大值即为最多重合线段数。
class Solution {
public:
int halveArray(vector& nums) {
priority_queueheap;
double sum=0;
for(int i=0;i
这个问题相比上一个就简单很多了,将数组和减半,又要最少次数,所以每次都减半数组中最大值即可。其中数组元素的最大值就可以借助大根堆。
注意在减半操作中会出现小数,要使用double类型。
堆结构在一些重点关注最大值最小值的问题中用处还是很大的。