Learning C++ No.18【STL No.8】优先级队列

简介: Learning C++ No.18【STL No.8】优先级队列

引言:

北京时间:2023/3/18/21:47,周末,不摆烂,但是欠钱终于还是遭报应了,导致坐牢7小时(上午3.5,下午3.5),难受,充分意识到行哥是那么的和蔼可亲,励志下次上蛋哥的课可以还清债务(所以下一篇,乃至更多篇博客,都将是关于系统编程的知识);周末时光:昨天12点睡觉,今天7点40起床,然后到9点上课,12:50追一集动漫,1点整睡觉,睡到2点25分起床上第二节课,到6点,下楼丢垃圾,然后洗澡,到7点,开始看最后一节C++的录屏,现在写博客(吃饭都是在上课的时候完成),无论是上午还是下午,牢底坐穿,但是不怕,小强有韧性,记录周末第一天,还行,不怎么摆烂,但是感觉自己似乎也没学什么东西;这篇博客,我们就来学习一下上篇博客谈到的,优先级队列(堆)的实现和反向迭代器等知识。


image.png


自我实现优先级队列(堆)

上篇博客,我们了解到了优先级队列基本使用,就是类似于一个堆的结构(二叉堆),并且大致结构和二叉树是没有什么区别的,所以总的来说,优先级队列有如下几个特点:优先级队列是一个容器适配器,优先级最高的数据是位于堆的顶部(top),并且实现优先级队列,可以使用任意类型的容器(但一般使用vector),了解了这些,此时我们就来正式的自我实现一下优先级队列吧!


结构如图所示:


41.png


功能函数:

empty():检测容器是否为空

size():返回容器中有效元素个数

front():返回容器中第一个元素的引用

push_back():在容器尾部插入元素

pop_back():删除容器尾部元素

如下图:



42.png


如上代码,我们可以发现, 在一个堆中插入和删除数据,为了提高效率最终都需要重新建堆才可以,所以此时就涉及到了两个建堆的函数:adjust_up、adjust_down,所以接下来,让我们一起看一下优先级队列中最关键的两个函数,如下代码:


43.png

在写这两个函数的时候,我们可以和上图(堆的结构图)在脑海中想象,然后结合在一起,可以很好的把代码正确的写出来。


搞定了上述优先级队列中的几个函数关键函数和向上建堆、向下建堆的两个函数,此时优先级队列的大致我们就实现了,但是此时可以发现,在上述的代码中,我们默认都是建一个小堆,如果此时我们要建大堆怎么办,虽然此时是可以通过直接将大于改成小于的方式来实现,但是这样并不是很好,所以此时我们引入一个新概念,也就是昨天浅浅的了解了的仿函数概念。


浅谈仿函数

如下图:

44.png


如上图,我们通过函数重载的形式实现了两个模板类(用于比较大小),此时优先级队列就可以通过调用上述的模板类来实现建大堆和建小堆的直接切换,如下代码所示:

45.png


所以,如上图,当我们使用了仿函数之后,我们就可以直接通过更改模板参数的类型来进行仿函数的切换,来进行大堆和小堆的切换,不需要去使用什么函数指针的方法去调用相应的函数来实现,所以仿函数的发明,就是为了可以让一个模板类(运算符重载)直接被另一个模板类的模板参数使用,然后该模板类直接通过使用模板参数来实现相应的类似函数的功能,不需要使用函数指针,进而调用相应的函数;


总:模板的设计真的非常的牛,什么都可以通过模板的形式进行直接传递和使用


优先级队列完整代码如下:

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
//要明白,此时的堆是一个数组(容器适配器默认是vector),就是使用数组的形式,给我们弄成了一个树的结构,就叫堆
namespace wwx
{
  template<class T>
  struct less//小堆仿函数
  {
    bool operator()(const T& x, const T& y)
    {
      return x < y;
    }
  };
  template<class T, class Container = vector<T>>//建大堆,我们用小于
  struct greater//大堆仿函数
  {
    bool operator()(const T& x, const T& y)
    {
      return x > y;
    }
  };
  template<class T, class Container = vector<T>, class Compare = greater<T>>//此时就可以不需要使用函数指针来调用某个函数了,可以直接使用模板类型来调用仿函数
  class priority_queue
  {
  public:
    void adjust_up(int child)
    {
      int parent = (child - 1) / 2;//这边一定要去把堆排序给复习一下(这个是可以自己推出来的)
      while (child > 0)//建大堆,最坏的情况就是当child为根结点的时候(也就是下标为0的时候),当下标为0就可以停下来了(不然它是会自己break出去的)
      {
        Compare com;
        //if (_con[parent] < _con[child])//建大堆
        if (com(_con[parent], _con[child]))//调用仿函数去比较
        {
          std::swap(_con[parent], _con[child]);
          child = parent;//建大堆(孩子结点大,此时就是让孩子变成父亲,然后重复再去寻找它的父结点)
          parent = (child - 1) / 2;//迭代走走
        }
        else
        {
          break;//此时此时只是向上调整,并不是堆排序
        }
      }
    }
    void adjust_down(int parent)//注意,此时是在类和对象中,所以可以直接使用this指针,所以可以直接使用我们的适配器来存储数据
    {
      int child = parent * 2 + 1;//父亲结点是唯一的,但是孩子结点是有两个
      while (child < _con.size())//这个条件不会写,就是把this指针给漏掉了
      {
        Compare com;
        //if (child + 1 < _con.size() && _con[child] < _con[child + 1])//左孩子小于右孩子(但是前提是右孩子存在,因为有的地方右孩子是不存在的),所以又漏了一个条件
        if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))
        //if (child + 1 < _con.size() && Compare()(_con[child], _con[child + 1]))//使用匿名对象的写法
        {
          child += 1;
        }
        //if (_con[parent] < _con[child])//但是这种写法是有一个前提的(那种没有前提的写法要去复习)
        if (com(_con[parent], _con[child]))
        {
          std::swap(_con[parent], _con[child]);
          parent = child;
          child = parent * 2 + 1;//迭代
        }
        else
        {
          break;
        }                                  
      }
    }
    //总结:写这种代码,就是要把堆的结构给深深的烙印在脑海里面(树状结构)
    void push(const T& x)
    {
      _con.push_back(x);
      adjust_up(_con.size() - 1);//尾插数据之后,建堆,通过向上调整的形式
    }
    void pop()//上述是堆的插入,现在是堆的删除
    {//这边可以刚好去把堆排序给复习一下
      std::swap(_con[0], _con[_con.size() - 1]);
      _con.pop_back();
      adjust_down(0);
    }
    const T& top()
    {
      return _con[0];
    }
    size_t size()
    {
      return _con.size();
    }
    bool empty()
    {
      return _con.empty();
    }
  private:
    Container _con;
  };
  void test_priority_queue()
  {
    priority_queue<int> pq;
    pq.push(1);
    pq.push(2);
    pq.push(3);
    pq.push(4);
    pq.push(5);
    while (!pq.empty())
    {
      cout << pq.top() << " ";
      pq.pop();
    }
    cout << endl;
  }
}
int main()
{
  wwx::test_priority_queue();
  return 0;
}

反向迭代器

我们当时在学习list的时候,已经将正向迭代器给实现了,所以现在,我们就来实现一下返向迭代器,区分为(我们认为的反向迭代器和源码中的反向迭代器),如下代码:


46.png如上图就是我们认为的,反向迭代器和正向迭代器的实现和区别,但是在真正的源码中却不是如上图中一样,而是实现了更高级的写法,如下代码所示:

47.png



通过上述的结构图,实现代码如下:

48.png



总:反向迭代器的源码是非常的高级的

image.png

总结:周末不摆烂,睡觉啦!

相关文章
|
3天前
|
存储 算法 Java
【C++】优先级队列priority_queue模拟实现&&仿函数
【C++】优先级队列priority_queue模拟实现&&仿函数
6 1
|
3天前
|
设计模式 算法 Java
【c++】STL之stack和queue详解
【c++】STL之stack和queue详解
6 1
|
10天前
|
存储 算法 C++
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
【C++高阶】探索STL的瑰宝 map与set:高效数据结构的奥秘与技巧
15 0
|
12天前
|
存储 算法 数据处理
【C++】STL简介
**STL是C++标准库的关键部分,源于Alexander Stepanov的泛型编程研究。它提供了数据结构(如vector、list)和算法,是高效、通用的软件框架。STL始于惠普,后由SGI发展,现已成为C++1998标准的一部分并不断进化。它包括容器、迭代器、算法、仿函数、配接器和分配器六大组件,带来高效性、通用性和可扩展性,但也存在性能开销和学习难度。学习STL涉及理解底层数据结构、用法、实现和实践。推荐[cplusplus.com](https://cplusplus.com)作为学习资源。**
|
12天前
|
存储 算法 程序员
C++基础知识(八:STL标准库(Vectors和list))
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), lists(链表), 和 stacks(栈)等. STL容器的提供是为了让开发者可以更高效率的去开发,同时我们应该也需要知道他们的底层实现,这样在出现错误的时候我们才知道一些原因,才可以更好的去解决问题。
|
12天前
|
算法 前端开发 C++
C++基础知识(八:STL标准库 deque )
deque在C++的STL(Standard Template Library)中是一个非常强大的容器,它的全称是“Double-Ended Queue”,即双端队列。deque结合了数组和链表的优点,提供了在两端进行高效插入和删除操作的能力,同时保持了随机访问的特性。
|
12天前
|
存储 C++ 索引
C++基础知识(八:STL标准库 Map和multimap )
C++ 标准模板库(STL)中的 map 容器是一种非常有用的关联容器,用于存储键值对(key-value pairs)。在 map 中,每个元素都由一个键和一个值组成,其中键是唯一的,而值则可以重复。
|
12天前
|
存储 算法 数据处理
|
3天前
|
设计模式 安全 编译器
【C++11】特殊类设计
【C++11】特殊类设计
22 10
|
8天前
|
C++
C++友元函数和友元类的使用
C++中的友元(friend)是一种机制,允许类或函数访问其他类的私有成员,以实现数据共享或特殊功能。友元分为两类:类友元和函数友元。类友元允许一个类访问另一个类的私有数据,而函数友元是非成员函数,可以直接访问类的私有成员。虽然提供了便利,但友元破坏了封装性,应谨慎使用。
39 9