一、定义

堆一般用于优先队列的实现(默认情况使用大顶堆)
大顶堆:父亲结点的值大于等于孩子结点的值,每个结点的值都是以它为根结点的子树的最大值。
小顶堆:父亲结点的值小于等于孩子结点的值,每个结点的值都是以它为根结点的子树的最小值。

const int maxn=100;
int heap[maxn],n=10;

 

二、建堆过程--向下调整

思路:总是将当前结点V与它的左右孩子比较(如果存在),加入孩子中存在权值比结点V的权值大的,就将其中权值最大的那个孩子结点与结点V交换;
交换完毕后继续让结点V和孩子比较,直到结点V的孩子的权值都比结点V的权值小或是结点V不存在孩子结点;

时间复杂度为O(logn)

//向下调整的代码
//对heap数组在[low,high]范围进行向下调整
//其中low为欲调整结点的数组下标,high一般为堆的最后一个元素的数组下标
void downAdjust(int low,int high)
{
    int i=low,j=i*2;  //i为欲调整结点,j为其左孩子
    while(j<=high){   //存在孩子结点
        //如果右孩子存在,且右孩子结点值大于左孩子
        if(j+1<=high&&heap[j+1]>heap[j])
        {
            j=j+1;     //让j存储右孩子下标
        }
        //如果孩子中最大的权值比欲调整结点i大
        if(heap[j]>heap[i])
        {
            swap(heap[j],heap[i]);   //交换最大权值的孩子与欲调整结点i
            i=j;
            j=i*2;
        }else{
            break;    //孩子的权值均比欲调整结点i小,调整结束
        }
    }
}

建堆,假设序列中元素个数为n,由于完全二叉树的叶子结点个数为n/2向上取整,因此数组下标在[1,n/2向下取整]范围内结点都是非叶子结点,
因此可以从n/2向下取整号位置开始倒着枚举结点,对每个遍历到的结点i进行[i,n]范围的调整。
倒着枚举可以保证每个结点都是以其为根结点的子树中的权值最大的结点。
时间复杂度O(n)

void createHeap()
{
    for(int i=n/2;i>=1;i--){
        downAdjust(i,n);
    }
}

 

三、删除堆顶元素

思路:将最后一个元素覆盖堆顶元素,然后对根结点进行调整即可
时间复杂度:O(logn)

void deleteTop()
{
    heap[1]=heap[n--];  //用最后一个元素覆盖堆顶元素,并让元素个数减1
    downAdjust(1,n);   //向下调整堆顶元素
}

 

四、插入元素--向上调整

思路:将想要添加的元素放在数组最后(即完全二叉树的最后一个结点后面),然后进行向上调整操作。
向上调整总是把欲调整结点与父亲结点比较,如果权值比父亲结点大,就交换其与父亲结点,反复比较,直到到达堆顶或是父亲结点的权值较大为止。
时间复杂度为O(logn)

//对heap数组在[low,high]范围进行向上调整
//其中low一般设置为1,high表示欲调整结点的数组下标
void upAdjust(int low,int high)
{
    int i=high,j=i/2;   //i为欲调整结点,j为其父亲
    while(j>=low)       //父亲在[low,high]范围内
    {
        //父亲权值小于欲调整结点i的权值
        if(heap[j]<heap[i]){
            swap(heap[j],heap[i]);  //交换父亲结点和欲调整结点
            i=j;                    //保持i为欲调整结点,j为i的父亲
            j=i/2;
        }else{
            break;                  //父亲权值比欲调整结点i的权值大,调整结束
        }
    }
}

插入元素x

void insert(int x){
    heap[++n]=x;  //让元素个数加1,然后将数组末位赋值为x
    upAdjust(1,n);   //向上调整加入的结点n
}

 

五、堆排序

使用堆结构对一个序列进行排序,此处讨论递增排序

堆排序的直观思路:取出堆顶元素,然后将堆的最后一个元素替换至堆顶,再进行一次针对堆顶元素的向下调整--如此反复直到堆中只有一个元素为止
具体实现时为了节省空间,可以倒着遍历数组,假设当前访问到i号位,那么将堆顶元素与i号位的元素交换,接着在[1,i-1]范围内对堆顶元素进行一次向下调整即可。

void heapSort()
{
    createHeap();   //建堆
    for(int i=n;i>1;i--){   //倒着枚举,直到堆中只有一个元素
        swap(heap[i],heap[1]);   //交换heap[i]与堆顶
        downAdjust(1,i-1);       //调整堆顶
    }
}

 

 六、练习题

问题B: 序列合并

题目描述

有两个长度都为N的序列A和B,在A和B中各取一个数相加可以得到N^2个和,求这N^2个和中最小的N个。

输入

第一行一个正整数N(1 <= N <= 100000)。
第二行N个整数Ai,满足Ai <= Ai+1且Ai <= 10^9
第三行N个整数Bi,满足Bi <= Bi+1且Bi <= 10^9

输出

输出仅有一行,包含N个整数,从小到大输出这N个最小的和,相邻数字之间用空格隔开。

样例输入 

3
2 6 6
1 4 8

样例输出

3 6 7

提示

建议用最小堆实现。

题意:给定两个非降序一维数组a和b,长度均为n,在a和b中分别取一个数相加总共有n^2种结果,现在要求这n^2种结果中最小的前n个。

思路:很显然,这道题的数据量较大,若暴力求解很可能会超时,所以蛮力使用二重循环计算n^2种结果然后存储在vector数组中再排序取前n个元素输出的做法不可行。

该题使用的数据结构为自定义排序的优先队列,队列的元素类型为结点类型,结点有两个变量:node.data表示a[j]+b[i]的值,node.i表示b[i]的下标,node.i初始均为1。为啥?因为我们定义的两个一维数组结点下标均从1开始存储,为了避免进行二重循环,我们先用b数组的第一个元素即下标为1的元素分别与a数组的n个元素相加,相加所得到的n个值作为结点的data变量均存储在优先队列vec中,而这n个结点的i变量均为1。那为啥这样取呢?我们知道,最小值一定产生于这n个数中,而且经过排序后的队列顶元素值一定是当前所有未输出的值中最小的那个。输出栈顶元素值后,我们要开始寻找仅次于(大于)栈顶元素的值,(当然,输出栈顶元素后一定要及时pop掉),所以我们要在栈顶元素值的基础上直接原地修改其值为a值和下一个b值(a表示a数组值,b表示b数组值),同时别忘了将其标记i改为现在b值的下标,再次push进优先队列中进行排序。如此反复。在这个循环中,我们用一个计数器从n开始计数,当n为0时循环结束,很显然,这时已经将最小的前n个数输出,程序结束。

#include <cstdio>
#include <iostream>
#include <queue>
using namespace std;
const int maxn=100005;
struct node{
    int data;
    int i;
    friend bool operator<(node n1,node n2)
    {
        return n1.data>n2.data;  //按照data值从小到大排序
    }
};
int main()
{
    int n;
    int a[maxn],b[maxn];
    while(cin>>n)
    {
        priority_queue<node,vector<node> > vec;
        for(int i=1;i<=n;i++) cin>>a[i];
        for(int i=1;i<=n;i++) cin>>b[i];
        for(int i=1;i<=n;i++)
        {
            node temp;
            int num=a[i]+b[1];
            temp.data=num;
            temp.i=1;   //初始化为1加数均是b[1],之后会改变
            vec.push(temp);
        }
        int m=n;
        while(m--)
        {
            node t=vec.top();
            vec.pop();   //!!!
            cout<<t.data<<" ";
            t.data=t.data+b[t.i+1]-b[t.i];
            t.i++;
            vec.push(t);
        }
        cout<<endl;
    }
    return 0;
}

/*
3
2 6 6
1 4 8
3 6 7
*/

 

问题 A: 算法10-10,10-11:堆排序

http://codeup.cn/problem.php?cid=100000616&pid=0

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=100005;
int heap[maxn],n;
//建堆过程,向下调整
void downAdjust(int low,int high)
{
    int i=low,j=i*2;
    while(j<=high){
        if(j+1<=high&&heap[j+1]>heap[j])
        {
            j=j+1;
        }
        if(heap[j]>heap[i])
        {
            swap(heap[j],heap[i]);
            i=j;
            j=i*2;
        }
        else{
            break;
        }
    }
}
//建堆
void createHeap()
{
    for(int i=n/2;i>=1;i--){
        downAdjust(i,n);
    }
}
void heapSort()
{
    createHeap();
    for(int i=n;i>1;i--){
        swap(heap[i],heap[1]);
        downAdjust(1,i-1);   //调整堆顶
    }
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>heap[i];
    heapSort();
    for(int i=1;i<=n;i++)
    {
        if(i!=n) cout<<heap[i]<<" ";
        else cout<<heap[i]<<endl;
    }
    return 0;
}

 

问题C:合并果子(堆)

http://codeup.cn/problem.php?cid=100000616&pid=2

思路:优先队列,小顶堆。

#include <cstdio>
#include <queue>
#include <iostream>
#include <algorithm>
using namespace std;
//小顶堆
priority_queue<int,vector<int>,greater<int> > que;
int n;
int main()
{
    int value,sum=0,temp=0;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>value;
        que.push(value);
    }
    if(que.size()==1){
        cout<<que.top()<<endl;
        return 0;
    }
    while(que.size()>1)
    {
        temp+=que.top();
        que.pop();
        if(!que.empty()){
            temp+=que.top();
            que.pop();
        }
        que.push(temp);
        sum+=temp;
        temp=0;
    }
    cout<<sum<<endl;
    return 0;
}

 

posted @ 2021-02-22 18:00  yyer  阅读(173)  评论(0编辑  收藏  举报