【C++】优先级队列priority_queue模拟实现&&仿函数

简介: 【C++】优先级队列priority_queue模拟实现&&仿函数

> 作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等

> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:能手撕仿函数模拟

> 毒鸡汤:你活得不快乐的原因是:既无法忍受目前的状态,又没能力改变这一切。

> 望小伙伴们点赞👍收藏✨加关注哟💕💕  



🌟前言

我们在vector讲解中已经了解到了priority_queue,只能说是浅谈,priority_queue底层到底是个啥勒?今天带大家揭晓它的面纱。



⭐主体

这里就创建两个文件priority_queue.h(头文件),test.cpp(测试代码文件)

咱们按照下面图解来学习今天的内容:



🌙什么是priority_queue

优先级队列priority_queue,即数据结构中的堆,堆是一种通过使用数组来模拟实现特定结构二叉树的二叉树的数据结构,根据父亲节点与孩子节点的大小关系,可以将堆分为大堆和小堆:


大堆:所有父亲节点的值都大于或等于它的孩子节点的值。

小堆:所有父亲节点的值都小于或等于它的孩子节点的值。

在C++ STL中,priority_queue的声明为:template <class T, class Container = vector<T>, class Compare = std::less<T>>  class priority_queue;


其中,每个模板参数的含义为:


  • T:优先级队列中存储的数据的类型
  • Container:用于实现优先级队列的容器,默认为vector
  • Comapre:比较仿函数,用于确定是建大堆还是建小堆。Compare默认为std::less<T>,建大堆,如果要建小堆,需要显示传参std::greater<T>,同时还有显示的声明容器类型。



🌙priority_queue常见接口的使用

  1. 优先级队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素是所有元素中最大的。
  2. 优先级队列的底层是用堆进行实现的,大根堆的堆顶是最大的。
  3. 标准容器vector和queue都满足以上要求,如果没有特定要求,默认使用vector作为底层容器类。
  4. 需要支持随机访问迭代器,保证内部始终保持堆结构。容器适配器在需要的时候调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
  5. 优先级队列的底层容器可以使任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
  • empty():检测容器是否为空。
  • size():返回容器有效元素个数。
  • front():返回容器第一个元素的引用
  • push_back():在容器尾部插入元素
  • pop_back():删除容器尾部元素


使用priority_queue:

#include<iostream>
#include<queue>
#include<functional>
 
int main()
{
  std::priority_queue<int> maxHeap;   //建大堆
  int data[10] = { 56,12,78,23,14,34,13,78,23,97 };
  //让arr中的数据依次入大堆
  for (int i = 0; i < 10; ++i)
  {
    maxHeap.push(data[i]);
  }
 
  std::cout << maxHeap.empty() << std::endl;  //判空 -- 0
  std::cout << maxHeap.size() << std::endl;   //获取堆中数据个数 -- 10
 
  //依次提取大堆堆顶数据并打输出
  while (!maxHeap.empty())
  {
    //97 78 78 56 34 23 23 14 13 12
    std::cout << maxHeap.top() << " ";
    maxHeap.pop();
  }
  std::cout << std::endl;
 
  std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;  //建小堆
  //让arr中的数据依次入小堆
  for (int i = 0; i < 10; ++i)
  {
    minHeap.push(data[i]);
  }
 
  //依次提取堆顶数据并打输出
  while (!minHeap.empty())
  {
    //12 13 14 23 23 34 56 78 78 97
    std::cout << minHeap.top() << " ";
    minHeap.pop();
  }
  std::cout << std::endl;
 
  return 0;
}


运行结果:



🌙priority_queue的模拟实现

💫仿函数的实现

仿函数是使用struct定义的类对象,通过重载操作符(),即operator()(参数列表)实现类似函数的功能。在类中定义运算符()的重载函数,通过类对象,来使调用运算符重载函数的语法,仿函数的调用语法为:类对象名称(参数列表)


在priority_queue的构造函数中,就经常使用less和greater两个仿函数,less和greater都是C++标准库中给出的判断两数之间大小关系的仿函数,他们被包含在头文件<functional>中:


  • less:给两个操作数,判断前者是否小于后者。
  • greater:给两个操作数,判断前者是否大于后者。


代码实现:

// 仿函数
template<class T>
struct less
{
  bool operator()(const T& a, const T& b)
  {
    return a < b;
  }
};
template<class T>
struct greater
{
  bool operator()(const T& a, const T& b)
  {
    return a > b;
  }
};


💫构造函数的实现

构造函数有两种重载形式:

  • (1)构造空堆,这时构造函数无需额外编写代码进行任何工作,容器成员和_con和类对象_comp都会被调用他们的默认构造函数被创建出来。
  • (2)通过迭代器区间进行构造,此时先通过迭代器区间构造容器对象,然后执行向下调整建堆操作。


向下调整函数AdjustDown需要有2个参数,分别为:堆中数据个数n和开始向下调整的父亲节点下标parent,其中还会用到类的成员(容器和用于比较的对象),AdjustDown执行的操作依次为(以建大堆为例):


  1. 根据父亲节点的下标,获取其左孩子节点的下标child。
  2. 判断孩子节点下标是否越界,如果越界(child>=n),则函数终止。
  3. 判断左孩子节点和右孩子节点那个较大,如果右孩子节点值较大,则将child更新为右孩子节点下标。
  4. 判断父亲节点是否小于较大的孩子节点,如果小于,则交换父子节点值,然后将父节点更新为孩子节点,然后回到1继续执行向下调整操作,如果大于或等于,则终止向下调整操作。


注意:对于开始执行向下操作的父节点parent,一定要保证它的左子树和右子树都为大或小堆。



构造函数代码实现
// 默认构造函数
priority_queue()
{}
// 通过迭代器区间的构造函数
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
  :_con(first, last)
{
  size_t lastchild = size() - 1;
  size_t parent = (lastchild - 1) / 2;
  for (size_t i = parent; i >= 0; i--)
  {
    AdjustDown(i);
  }
}


向下调整代码实现
void AdjustDown(size_t parent)
{
  size_t child = parent * 2 + 1;
  while (child < size())
  {
    //if (child + 1 < size() && _con[child + 1] > _con[child])
    if (child + 1 < size() && com(_con[child], _con[child + 1]))
      ++child;
    //if (_con[child] > _con[parent])
    if (com(_con[parent], _con[child]))
      swap(_con[parent], _con[child]);
    else
      break;
    parent = child;
    child = parent * 2 + 1;
  }
}


💫插入数据函数的实现

向堆中插入数据需要两步操作:先向容器中尾插数据,然后调用AdjustUp函数上调整数据。


向上调整函数AdjustUp执行的操作流程为:(建大堆为例)


  1. 根据开始执行向上调整的孩子节点下标,计算出其父亲节点下标。
  2. 判断孩子节点下标是否>0,如果是,继续执行向上调整操作,否则终止函数。
  3. 判断孩子节点值是否大于父亲节点,如果大于,交换父子节点值,然后更新孩子节点为当前父亲节点,根据更新后孩子节点下标计算父亲节点下标,然后回到1继续执行向上调整操作。如果孩子节点值小于或等于父亲节点值,那么终止向上调整操作。



向堆中插入数据函数
void push(const T& x)
{
  _con.push_back(x);
  AdjustUp(size() - 1);
}


向上调整操作函数
void AdjustUp(size_t child)
{
  size_t parent = (child - 1) / 2;
  while (child > 0)
  {
    //if (_con[child] > _con[parent])->父亲小于孩子就调整
    if (com(_con[parent], _con[child]))//
    {
      swap(_con[child], _con[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
      break;
  }
}


💫删除堆顶数据函数的实现

如果直接将除堆顶之外的全部数据向前移动一个单位,那么数组中剩余的数据大概率无法满足堆的结构要求,重新再建堆效率过低。那么,就需要一些额外的技巧来解决问题:

  • 交换堆顶数据和数组中最后一个数据。
  • 将数组中的最后一个数据删除。
  • 以当前根节点为起始父亲节点,执行向下调整操作,使数组中的数据重新满足堆的结构。


void pop()
{
  swap(_con[size() - 1], _con[0]);
  _con.pop_back();
  AdjustDown(0);
}


💫其它函数功能实现

size_t size() const
{
  return _con.size();
}
T& top()
{
  return _con[0];
}
bool empty()
{
  return _con.empty();
}


🌙priority_queue的完整代码

#include <iostream>
#include <vector>
using namespace std;
 
 
namespace lyk
{
  // 仿函数
  template<class T>
  struct less
  {
    bool operator()(const T& a, const T& b)
    {
      return a < b;
    }
  };
  template<class T>
  struct greater
  {
    bool operator()(const T& a, const T& b)
    {
      return a > b;
    }
  };
 
  // container 容器 ,compare 用于调用比较函数的类对象
  template<class T, class Container = vector<T>, class Compare = less<T>>
  class priority_queue
  {
  private:
    Compare com;
    void AdjustUp(size_t child)
    {
      size_t parent = (child - 1) / 2;
      while (child > 0)
      {
        //if (_con[child] > _con[parent])->父亲小于孩子就调整
        if (com(_con[parent], _con[child]))//
        {
          swap(_con[child], _con[parent]);
          child = parent;
          parent = (child - 1) / 2;
        }
        else
          break;
      }
    }
    void AdjustDown(size_t parent)
    {
      size_t child = parent * 2 + 1;
      while (child < size())
      {
        //if (child + 1 < size() && _con[child + 1] > _con[child])
        if (child + 1 < size() && com(_con[child], _con[child + 1]))
          ++child;
        //if (_con[child] > _con[parent])
        if (com(_con[parent], _con[child]))
          swap(_con[parent], _con[child]);
        else
          break;
        parent = child;
        child = parent * 2 + 1;
      }
    }
  public:
    // 默认构造函数
    priority_queue()
    {}
    // 通过迭代器区间的构造函数
    template<class InputIterator>
    priority_queue(InputIterator first, InputIterator last)
      :_con(first, last)
    {
      size_t lastchild = size() - 1;
      size_t parent = (lastchild - 1) / 2;
      for (size_t i = parent; i >= 0; i--)
      {
        AdjustDown(i);
      }
    }
    void push(const T& x)
    {
      _con.push_back(x);
      AdjustUp(size() - 1);
    }
    void pop()
    {
      swap(_con[size() - 1], _con[0]);
      _con.pop_back();
      AdjustDown(0);
    }
    size_t size() const
    {
      return _con.size();
    }
    T& top()
    {
      return _con[0];
    }
    bool empty()
    {
      return _con.empty();
    }
  private:
    Container _con;
  };
  void priority_test1()
  {
    priority_queue<int> pq;
    pq.push(40);
    pq.push(30);
    pq.push(56);
    pq.push(26);
    pq.push(45);
    while (!pq.empty())
    {
      cout << pq.top() << " ";
      pq.pop();
    }
    cout << endl;
  }
  void priority_test2()
  {
    priority_queue<int, vector<int>, greater<int>> pq;
    pq.push(40);
    pq.push(30);
    pq.push(56);
    pq.push(26);
    pq.push(45);
    cout << "greater<int>建小堆-->  ";
    while (!pq.empty())
    {
      cout << pq.top() << " ";
      pq.pop();
    }
    cout << endl;
    priority_queue<int> pq2;
    pq2.push(40);
    pq2.push(30);
    pq2.push(56);
    pq2.push(26);
    pq2.push(45);
    cout << "默认less<int>建大堆-->  ";
    while (!pq2.empty())
    {
      cout << pq2.top() << " ";
      pq2.pop();
    }
    cout << endl;
  }
}


💫运行结果



🌟结束语

      今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。


目录
相关文章
|
1月前
|
存储 算法 C语言
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
|
2月前
|
C++ 容器
【c++】优先级队列|反向迭代器(vector|list)
【c++】优先级队列|反向迭代器(vector|list)
14 0
|
2月前
|
C++
C++函数对象(仿函数)
C++函数对象(仿函数)
15 0
|
2月前
|
存储 设计模式 算法
【C++航海王:追寻罗杰的编程之路】priority_queue(优先队列) | 容器适配器你知道哪些?
【C++航海王:追寻罗杰的编程之路】priority_queue(优先队列) | 容器适配器你知道哪些?
30 0
|
2月前
|
C++ 容器
【C++】学习笔记——优先级队列
【C++】学习笔记——优先级队列
20 0
|
6天前
|
C++ 容器
C++中自定义结构体或类作为关联容器的键
C++中自定义结构体或类作为关联容器的键
12 0
|
6天前
|
存储 算法 搜索推荐
【C++】类的默认成员函数
【C++】类的默认成员函数
|
5天前
|
存储 安全 编译器
【C++】类和对象(下)
【C++】类和对象(下)
【C++】类和对象(下)
|
4天前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
4天前
|
编译器 C++
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决