堆——神奇的优先队列 大根堆小根堆详解,附小根堆C++代码实现与STL相关

本文详细介绍了堆数据结构,特别是小根堆和大根堆的概念、特性以及操作。通过代码实现展示了小根堆的插入和删除过程,并提供了STL中的`priority_queue`容器适配器的使用方法,包括创建、成员函数及其应用场景。
摘要由CSDN通过智能技术生成

前置知识

  1. 堆是一个完全二叉树(最后一层可以不满,上面的每一层都是满的。一个结点若只有一个孩子结点,那一定是它的左孩子。如下图)这是一个逻辑上基于完全二叉树、物理上一般基于线性数据结构(如数组、向量、链表等)的一种数据结构。
    完全二叉树

  2. 完全二叉树最重要的性质:如果n个节点的完全二叉树的节点按照层次并按从左到右的顺序从0开始编号,对于人一个绩点都有:

    • 序号为0的节点是根
    • 对于 i > 0 i>0 i>0,其父节点的编号为 ( i − 1 ) / 2 (i-1)/2 (i1)/2
    • 2 ⋅ i + 1 < n 2·i+1<n 2i+1<n ,其左子节点的序号为 2 ⋅ i + 1 2·i+1 2i+1 ,否则没有左子节点。
    • 2 ⋅ i + 2 < n 2·i+2<n 2i+2<n ,其右子节点的序号为 2 ⋅ i + 2 2·i+2 2i+2 ,否则没有右子节点。

    另外我们下边举例子根节点为数组下标为1的位子,相应的公式也会有点变化

  3. 堆的有序性

    • 堆可分为两种:大根堆(最大堆)、小根堆(最小堆)。

    • 大根堆
      何为大根堆?顾名思义,大根堆即指在逻辑上的二叉树结构中,根结点>子结点,总是最大的,并且在堆的每一个局部都是如此。例如 3 , 1 , 2 {3,1,2} 3,1,2 可以看作为大根堆,而 3 , 2 , 1 {3,2,1} 3,2,1 亦可以看作为大根堆。大根堆的根结点在整个堆中是最大的元素。

    • 小根堆
      小根堆的性质与大根堆类似,只不过在二叉树的结构中,根结点<子结点。例如 1 , 2 , 3 {1,2,3} 1,2,3 为小根堆, 1 , 3 , 2 {1,3,2} 1,3,2 同样也是小根堆。小根堆的根结点在整个堆中是最小的元素。

堆操作

这边主要以小根堆实现讲解

小根堆插入

  1. 往堆中加入一个元素的算法(put)如下:

    (1)在堆尾加入一个元素,并把这个结点置为当前结点。

    (2)比较当前结点和它父结点的大小

    ​ 如果当前结点小于父结点,则交换它们的值,并把父结点置为当前结点。转(2)
    ​ 如果当前结点大于等于父结点,则转(3)

    (3)结束。

    image-20220403132544246

    类似于这样,这是一个循环操作,直到所有父节点小于其子节点,且注意,在插入之前堆内就是符合性质的,只需要将新插入的节点放好。

    代码实现

    void put(int d)
    {
        int now, next;
        heap[++heap_size] = d;
        now = heap_size;		//新节点
        while (now > 1) {		//1为根节点位置
            next = now >> 1;	//父节点
            if (heap[now] >= heap[next])//若插入的节点值小于父节点,则不用调整
                break;
            swap(heap[now], heap[next]);//交换父节点和新节点的值进行调整
            now = next;				  //追踪新节点的位置,继续进行调整
        }
    }
    

    小根堆删除

    这种大根堆小根堆的操作中的删除一般都是删除根节点,因为根节点具有最值性

    从堆中取出并删除一个元素的算法(get)如下
    (1)取出堆的根结点的值。

    (2)把堆的最后一个结点放到根的位置上,把根覆盖掉。把堆的长度减一。

    (3)用p指向当前被覆盖后的根节点

    (4)如果p无儿子(2*p>len,完全二叉树性质,左子树节点的位置,前置知识),则转(6);否则,选出 p 的两(或一)个儿子中值最小的那个称为son。

    (5)比较p与son的值,如果p的值小于或等于son,则转(6);否则,交换这两个结点的值,接着用p追踪被交换过去的新值,转(4)

    (6)结束。

代码实现

int get() //取出堆顶元素,heap[1]为堆顶
{
    int now = 1, next, res = heap[1];
    heap[1] = heap[heap_size]; //取出堆顶元素,将最后一个元素放于堆顶
    heap_size--;
    //重新调整为小根堆
    while (now * 2 <= heap_size) { //存在左孩子next = now*2;
        //找出左右孩子最小的那个
        if (next < heap_size && heap[next + 1] < heap[next])
            next++;
        if (heap[now] <= heap[next])
            break;
        swap(heap[now], heap[next]); //交换
        now = next;   //追踪过去
    }
    return res;
}

测试代码

#include <iostream>
#include <vector>
using namespace std;
int heap[1000];
int heap_size;
void put(int d)
{
    int now, next;
    heap[++heap_size] = d;
    now = heap_size;
    while (now > 1) {
        next = now >> 1;
        if (heap[now] >= heap[next])
            break;
        swap(heap[now], heap[next]);
        now = next;
    }
}
int get() //取出堆顶元素,heap[1]为堆顶
{
    int now = 1, next, res = heap[1];
    heap[1] = heap[heap_size]; //取出堆顶元素,将最后一个元素放于堆顶
    heap_size--;
    //重新调整为小根堆
    while (now * 2 <= heap_size) { //存在左孩子
    	next = now*2; 		//找出左右孩子最小的那个   
        if (next < heap_size && heap[next + 1] < heap[next])
            next++;
        if (heap[now] <= heap[next])
            break;
        swap(heap[now], heap[next]); //交换
        now = next;
    }
    return res;
}

int main(int argc, char)
{
    int n;
    cin >> n;
    int temp;
    for (int i = 1; i <= n; i++){
        cin>>temp;
        put(temp);
    }
    for(int i=1;i<=n;i++){
        cout<<heap[i]<<" ";
    }
  
    
}

STL实现——priority_queue

priority_queue 容器适配器定义了一个元素有序排列的队列。默认队列头部的元素优先级最高。因为它是一个队列,所以只能访问第一个元素,这也意味着优先级最高的元素总是第一个被处理。但是如何定义“优先级”完全取决于我们自己。如果一个优先级队列记录的是医院里等待接受急救的病人,那么病人病情的严重性就是优先级。如果队列元素是银行的借贷业务,那么借记可能会优先于信贷。

1.使用

由于 priority_queue 容器适配器模板位于<queue>头文件中,并定义在 std 命名空间里,因此在试图创建该类型容器之前,程序中需包含以下 2 行代码:

#include <queue>
using namespace std;

priority_queue 容器适配器的定义如下:

template <typename T,
        typename Container=std::vector<T>,
        typename Compare=std::less<T> >
class priority_queue{
    //......
}

可以看到,priority_queue 容器适配器模板类最多可以传入 3 个参数,它们各自的含义如下:

  • typename T:指定存储元素的具体类型;

  • typename Container:指定 priority_queue 底层使用的基础容器,默认使用 vector 容器。

    作为 priority_queue 容器适配器的底层容器,其必须包含 empty()、size()、front()、push_back()、pop_back() 这几个成员函数,STL 序列式容器中只有 vector 和 deque 容器符合条件。

  • typename Compare:指定容器中评定元素优先级所遵循的排序规则,默认使用std::less<T>按照元素值从大到小进行排序,还可以使用std::greater<T>按照元素值从小到大排序,但更多情况下是使用自定义的排序规则。

2.创建
  1. 创建一个空的 priority_queue 容器适配器,第底层采用默认的 vector 容器,排序方式也采用默认的 std::less 方法:
std::priority_queue<int> values;
  1. 可以使用普通数组或其它容器中指定范围内的数据,对 priority_queue 容器适配器进行初始化:
//使用普通数组
int values[]{4,1,3,2};
std::priority_queue<int>copy_values(values,values+4);//{4,2,3,1}

//使用序列式容器
std::array<int,4>values{ 4,1,3,2 };
std::priority_queue<int>copy_values(values.begin(),values.end());//{4,2,3,1}

注意,以上 2 种方式必须保证数组或容器中存储的元素类型和 priority_queue 指定的存储类型相同。另外,用来初始化的数组或容器中的数据不需要有序,priority_queue 会自动对它们进行排序。

  1. 还可以手动指定 priority_queue 使用的底层容器以及排序规则,比如:
int values[]{ 4,1,2,3 };
std::priority_queue<int, std::deque<int>, std::greater<int>> copy_values(values, values+4);//{1,3,2,4}

事实上,std::less 和 std::greater 适用的场景是有限的,更多场景中我们会使用自定义的排序规则。

3.成员函数
成员函数功能
empty()如果 priority_queue 为空的话,返回 true;反之,返回 false。
size()返回 priority_queue 中存储元素的个数。
top()返回 priority_queue 中第一个元素的引用形式。
push(const T& obj)根据既定的排序规则,将元素 obj 的副本存储到 priority_queue 中适当的位置。
push(T&& obj)根据既定的排序规则,将元素 obj 移动存储到 priority_queue 中适当的位置。
emplace(Args&&… args)Args&&… args 表示构造一个存储类型的元素所需要的数据(对于类对象来说,可能需要多个数据构造出一个对象)。此函数的功能是根据既定的排序规则,在容器适配器适当的位置直接生成该新元素。
pop()移除 priority_queue 容器适配器中第一个元素。
swap(priority_queue& other)将两个 priority_queue 容器适配器中的元素进行互换,需要注意的是,进行互换的 2 个 priority_queue 容器适配器中存储的元素类型以及底层采用的基础容器类型,都必须相同。

和 queue 一样,priority_queue 也没有迭代器,因此访问元素的唯一方式是遍历容器,通过不断移除访问过的元素,去访问下一个元素。

  • 7
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值