【C++初阶】STL详解(九) priority_queue的使用与模拟实现

简介: 【C++初阶】STL详解(九) priority_queue的使用与模拟实现


priority_queue的使用

priority_queue的介绍

优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中的元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。

注意: 默认情况下priority_queue是大堆

priority_queue的定义方式

priority_queue的介绍

方式一: 使用vector作为底层容器,内部构造大堆结构。

priority_queue<int, vector<int>, less<int>> q1;

方式二: 使用vector作为底层容器,内部构造小堆结构。

priority_queue<int, vector<int>, greater<int>> q2;

方式三: 不指定底层容器和内部需要构造的堆结构。

priority_queue<int> q;

注意: 此时默认使用vector作为底层容器,内部默认构造大堆结构

priority_queue各个接口使用

priority_queue的各个成员函数及其功能如下:

使用示例:

#include <iostream>
#include <functional>
#include <queue>
using namespace std;
int main()
{
  priority_queue<int> q;
  q.push(3);
  q.push(6);
  q.push(0);
  q.push(2);
  q.push(9);
  q.push(8);
  q.push(1);
  while (!q.empty())
  {
    cout << q.top() << " ";
    q.pop();
  }
  cout << endl; //9 8 6 3 2 1 0
  return 0;
}

priority_queue的模拟实现

priority_queue的底层实际上就是堆结构,实现priority_queue之前,我们先认识两个重要的堆算法。(下面这两种算法我们均以大堆为例)

堆的向上调整法

当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法。

向上调整算法的基本思想(以建小堆为例):

 1.将目标结点与其父结点比较。

 2.若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了。

代码如下:

//交换函数
void Swap(HPDataType* x, HPDataType* y)
{
  HPDataType tmp = *x;
  *x = *y;
  *y = tmp;
}
//堆的向上调整(小堆)
void AdjustUp(HPDataType* a, int child)
{
  int parent = (child - 1) / 2;
  while (child > 0)//调整到根结点的位置截止
  {
    if (a[child] < a[parent])//孩子结点的值小于父结点的值
    {
      //将父结点与孩子结点交换
      Swap(&a[child], &a[parent]);
      //继续向上进行调整
      child = parent;
      parent = (child - 1) / 2;
    }
    else//已成堆
    {
      break;
    }
  }
}

堆的向下调整法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。

但是:向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

1.若想将其调整为小堆,那么根结点的左右子树必须都为小堆。

2.若想将其调整为大堆,那么根结点的左右子树必须都为大堆。

向下调整算法的基本思想(以建小堆为例):

 1.从根结点处开始,选出左右孩子中值较小的孩子。

 2.让小的孩子与其父亲进行比较。


 若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。


 若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。

代码如下:

//交换函数
void Swap(int* x, int* y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}
//堆的向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{
  //child记录左右孩子中值较小的孩子的下标
  int child = 2 * parent + 1;//先默认其左孩子的值较小
  while (child < n)
  {
    if (child + 1 < n&&a[child + 1] < a[child])//右孩子存在并且右孩子比左孩子还小
    {
      child++;//较小的孩子改为右孩子
    }
    if (a[child] < a[parent])//左右孩子中较小孩子的值比父结点还小
    {
      //将父结点与较小的子结点交换
      Swap(&a[child], &a[parent]);
      //继续向下进行调整
      parent = child;
      child = 2 * parent + 1;
    }
    else//已成堆
    {
      break;
    }
  }
}

使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN)

上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。

代码如下:

//建堆
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(php->a, php->size, i);
  }

建堆时间复杂度:

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

利用错位相减法进行计算:

因此:建堆的时间复杂度为O(N)

总结:
堆的向下调整算法的时间复杂度:T(n)=O(logN)。
建堆的时间复杂度:T(n)=O(N)。

priority_queue的模拟实现

只要知道了堆的向上调整算法和堆的向下调整算法,priority_queue的模拟实现就没什么困难了。

priority_queue的模拟实现代码:

namespace NIC //防止命名冲突
{
  //比较方式(使内部结构为大堆)
  template<class T>
  struct less
  {
    bool operator()(const T& x, const T& y)
    {
      return x < y;
    }
  };
  //比较方式(使内部结构为小堆)
  template<class T>
  struct greater
  {
    bool operator()(const T& x, const T& y)
    {
      return x > y;
    }
  };
  //优先级队列的模拟实现
  template<class T, class Container = vector<T>, class Compare = less<T>>
  class priority_queue
  {
  public:
    //堆的向上调整
    void AdjustUp(int child)
    {
      int parent = (child - 1) / 2; //通过child计算parent的下标
      while (child > 0)//调整到根结点的位置截止
      {
        if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置
        {
          //将父结点与孩子结点交换
          swap(_con[child], _con[parent]);
          //继续向上进行调整
          child = parent;
          parent = (child - 1) / 2;
        }
        else//已成堆
        {
          break;
        }
      }
    }
    //插入元素到队尾(并排序)
    void push(const T& x)
    {
      _con.push_back(x);
      AdjustUp(_con.size() - 1); //将最后一个元素进行一次向上调整
    }
    //堆的向下调整
    void AdjustDown(int n, int parent)
    {
      int child = 2 * parent + 1;
      while (child < n)
      {
        if (child + 1 < n && _comp(_con[child], _con[child + 1]))
        {
          child++;
        }
        if (_comp(_con[parent], _con[child]))//通过所给比较方式确定是否需要交换结点位置
        {
          //将父结点与孩子结点交换
          swap(_con[child], _con[parent]);
          //继续向下进行调整
          parent = child;
          child = 2 * parent + 1;
        }
        else//已成堆
        {
          break;
        }
      }
    }
    //弹出队头元素(堆顶元素)
    void pop()
    {
      swap(_con[0], _con[_con.size() - 1]);
      _con.pop_back();
      AdjustDown(_con.size(), 0); //将第0个元素进行一次向下调整
    }
    //访问队头元素(堆顶元素)
    T& top()
    {
      return _con[0];
    }
    const T& top() const
    {
      return _con[0];
    }
    //获取队列中有效元素个数
    size_t size() const
    {
      return _con.size();
    }
    //判断队列是否为空
    bool empty() const
    {
      return _con.empty();
    }
  private:
    Container _con; //底层容器
    Compare _comp; //比较方式
  };
}

测试一下:

相关文章
|
24天前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
55 21
|
2月前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
42 1
|
2月前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
69 7
|
3月前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
136 4
|
3月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
140 5
|
3月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
89 2
|
3月前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
100 0
|
2月前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
67 0
|
4月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
99 5
|
4月前
|
存储 自然语言处理 程序员
C++常用基础知识—STL库(1)
C++常用基础知识—STL库(1)
95 1