【C++】优先级队列、仿函数和反向迭代器

简介: 【C++】优先级队列、仿函数和反向迭代器

👉priority_queue 的介绍👈


优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包的元素中最大的。

此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队中位于顶部的元素)。

优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类.priority_queue 提供一组特定的成员函数来访问其元素,元素从特定容器的尾部弹出,其称为优先队列的顶部。

底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:

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

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

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

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

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

标准容器类 vector 和 deque 满足这些需求。默认情况下,如果没有为特定的 priority_queue 类实例化指定容器类,则使用 vector。

需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数 make_heap、push_heap 和 pop_heap 来自动完成此操作。


👉priority_queue 的使用👈


优先级队列默认使用 vector 作为其底层存储数据的容器。在 vector 上又使用了向上调整算法和向下调整算法将 vector 中元素构造成堆的结构,因此 priority_queue 就是堆。所有需要用到堆的地方,都可以考虑使用priority_queue。注意:默认情况下 priority_queue 是大堆。


函数声明 接口说明
priority_queue() 构造一个空的优先级队列
priority_queue(first, last) 迭代器区间构造优先级队列
empty( ) 检测优先级队列是否为空,是返回 true,否则返回 false
top( ) 返回优先级队列中最大元素(最小元素),即堆顶元素
push(x) 在优先级队列中插入元素 x
pop() 删除优先级队列中最大元素(最小元素),即堆顶元素
size() 返回优先级队列中元素的个数


代码示例:


#include <queue>  // 优先级队列priority的头文件
#include <iostream>
using namespace std;
#include <functional> // greater算法的头文件
int main()
{
  // 默认情况下,创建的是大堆,其底层是按照小于号比较的
  priority_queue<int> pq1; 
  pq1.push(3);
  pq1.push(2);
  pq1.push(8);
  pq1.push(5);
  pq1.push(1);
  while (!pq1.empty())
  {
    cout << pq1.top() << " ";
    pq1.pop();
  }
  cout << endl;
  // 如果要创建小堆,将第三个模板参数换成greater比较方式,其底层是按照大于号比较的
  priority_queue<int, vector<int>, greater<int>> pq2;
  pq2.push(3);
  pq2.push(2);
  pq2.push(8);
  pq2.push(5);
  pq2.push(1);
  while (!pq2.empty())
  {
    cout << pq2.top() << " ";
    pq2.pop();
  }
  cout << endl;
  return 0;
}

3e195bd4a2be4ee2a57862be38285ede.png


如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供 > 或者 < 的重载。


#include <queue>  // 优先级队列priority的头文件
#include <functional> // greater算法的头文件
#include <iostream>
using namespace std;
class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
  bool operator<(const Date& d)const
  {
    return (_year < d._year) ||
      (_year == d._year && _month < d._month) ||
      (_year == d._year && _month == d._month && _day < d._day);
  }
  bool operator>(const Date& d)const
  {
    return (_year > d._year) ||
      (_year == d._year && _month > d._month) ||
      (_year == d._year && _month == d._month && _day > d._day);
  }
  friend ostream& operator<<(ostream& _cout, const Date& d)
  {
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
  }
private:
  int _year;
  int _month;
  int _day;
};
void TestPriorityQueue()
{
  // 大堆,需要用户在自定义类型中提供<的重载
  priority_queue<Date> q1;
  q1.push(Date(2018, 10, 29));
  q1.push(Date(2018, 10, 28));
  q1.push(Date(2018, 10, 30));
  while (!q1.empty())
  {
    cout << q1.top() << "  ";
    q1.pop();
  }
  cout << endl;
  // 如果要创建小堆,需要用户提供>的重载
  priority_queue<Date, vector<Date>, greater<Date>> q2;
  q2.push(Date(2018, 10, 29));
  q2.push(Date(2018, 10, 28));
  q2.push(Date(2018, 10, 30));
  while (!q2.empty())
  {
    cout << q2.top() << "  ";
    q2.pop();
  }
  cout << endl;
}
int main()
{
  TestPriorityQueue();
  return 0;
}

e42dc48ca4174b0c918d6d85f10edda8.png


优先级的队列使用起来没有什么难的,接下来我们来做一道题目,顺便回顾一下堆。


数组中的第K个最大元素


给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。


请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。


你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

9ec2e5dc5bc54b77b791ea374b92d846.png


解决这道题有两种思路,第一种就是用数组的元素建大堆,然后 pop 掉 k - 1 个元素,此时堆顶的元素就是第 k 大的数。这种思路的时间复杂度为O(N + k * lgN),当 N 很大时,就会需要非常多的空间。这时候,我们可以考虑第二种思路就是用前 k 个数建只包含 k 个数的小堆,再遍历数组剩下的元素,比堆顶元素大则替换掉堆顶元素,再向下调整堆。这种思路的时间复杂度为O(k + (N - k) * lgk)注:顶元素不允许修改,因为堆顶元素修改可以会破坏堆的特性。


class Solution 
{
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        // 建n个数的大堆
        priority_queue<int> pq(nums.begin(), nums.end());
        // pop掉前k-1个大的数
        while(--k)  pq.pop();
        return pq.top();
    }
};

904e93625e4142ec875c0ac74362a1ee.png


class Solution 
{
public:
    int findKthLargest(vector<int>& nums, int k) 
    {
        // 建k个数的小堆,从下标k开始遍历数组
        // 如果num[i]大于堆顶的数据,那么nums[i]入堆
        // 遍历结束,堆顶的数就是第k大的数
        priority_queue<int, vector<int>, greater<int>> pq(nums.begin(), nums.begin() + k);
        for(size_t i = k; i < nums.size(); ++i)
        {
            if(nums[i] > pq.top())
            {
                pq.pop();
                pq.push(nums[i]);
            }
        }
        return pq.top();
    }
};

1295f568ca384678b31c5f6bb945b1d0.png


👉仿函数👈


仿函数也称为函数对象,它是具有operator()的类对象,可以想函数一样来使用它。


#include <iostream>
using namespace std;
// 仿函数/函数对象
template <class T>
class Less
{
public:
  template <class T>
  bool operator()(const T& left, const T& right)
  {
    return left < right;
  }
};
template <class T>
class Greater
{
public:
  template <class T>
  bool operator()(const T& left, const T& right)
  {
    return left > right;
  }
};
int main()
{
  // 仿函数用起来就像是函数
  // 有名对象
  Less<int> lessFunc;
  cout << lessFunc(1, 2) << endl;
  // lessFunc(1, 2) <==> lessFunc.operator()(1, 2);
  // 匿名对象
  cout << Greater<int>()(1, 2) << endl;
  return 0;
}

a9a79e5b51cb4d4bae7c5a6466e93846.png


这样一看,好像仿函数也没有特别大的又是,和函数指针也没有什么大的区别嘛!其实不然,函数指针需要写函数的参数和返回值类型。所以仿函数用起来还是比函数指针舒服的。


#include <iostream>
using namespace std;
// 仿函数/函数对象
template <class T>
class Less
{
public:
  template <class T>
  bool operator()(const T& left, const T& right)
  {
    return left < right;
  }
};
template <class T>
class Greater
{
public:
  template <class T>
  bool operator()(const T& left, const T& right)
  {
    return left > right;
  }
};
template <class T, class Compare>
void BubbleSort(T* a, int n, Compare com)
{
  for (int i = 0; i < n - 1; ++i)
  {
    int exchange = 0;
    for (int j = 1; j < n - i; ++j)
    {
      // if (a[i] < a[i - 1])
      if (com(a[j], a[j - 1]))
      {
        swap(a[j], a[j - 1]);
        exchange = 1;
      }
    }
    if (exchange == 0)
      break;
  }
}
int main()
{
  Less<int> lessFunc;
  int arr[] = { 2, 7,3, 1, 5, 9 ,10,20 };
  BubbleSort(arr, sizeof(arr) / sizeof(arr[0]), lessFunc);
  for (auto e : arr)
    cout << e << " ";
  cout << endl;
  BubbleSort(arr, sizeof(arr) / sizeof(arr[0]), Greater<int>());
  for (auto e : arr)
    cout << e << " ";
  cout << endl;
  return 0;
}

301d5c803f8f40719ea94b15da5e2c82.png


注:没有成员变量的类的大小是 1 个字节,所以传参时加不加引用都没有关系。如果参数用 const 修饰了,那么仿函数也要用 const 来修饰。


如果库提供的仿函数不符合我们的需求,我们可以自己写!


#include <queue>  // 优先级队列priority的头文件
#include <functional> // greater算法的头文件
#include <iostream>
using namespace std;
class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
  bool operator<(const Date& d)const
  {
    return (_year < d._year) ||
      (_year == d._year && _month < d._month) ||
      (_year == d._year && _month == d._month && _day < d._day);
  }
  bool operator>(const Date& d)const
  {
    return (_year > d._year) ||
      (_year == d._year && _month > d._month) ||
      (_year == d._year && _month == d._month && _day > d._day);
  }
  friend ostream& operator<<(ostream& _cout, const Date& d)
  {
    _cout << d._year << "-" << d._month << "-" << d._day;
    return _cout;
  }
private:
  int _year;
  int _month;
  int _day;
};
struct PDateLess
{
  bool operator()(const Date* d1, const Date* d2)
  {
    return *d1 < *d2;
  }
};
struct PDateGreater
{
  bool operator()(const Date* d1, const Date* d2)
  {
    return *d1 > *d2;
  }
};
void TestPriorityQueue()
{
  priority_queue<Date*, vector<Date*>, PDateLess> q3;
  q3.push(new Date(2018, 10, 29));
  q3.push(new Date(2018, 10, 28));
  q3.push(new Date(2018, 10, 30));
  while (!q3.empty())
  {
    cout << *q3.top() << "  ";
    q3.pop();
  }
  cout << endl;
  priority_queue<Date*, vector<Date*>, PDateGreater> q4;
  q4.push(new Date(2018, 10, 29));
  q4.push(new Date(2018, 10, 28));
  q4.push(new Date(2018, 10, 30));
  while (!q4.empty())
  {
    cout << *q4.top() << "  ";
    q4.pop();
  }
  cout << endl;
}
int main()
{
  TestPriorityQueue();
  return 0;
}


41bdf7666c1447b0a0ef675876c51eac.png


现在学习到的仿函数还只是冰山一角,以后还会有更多的仿函数要学!!!


👉priority_queue 的模拟实现👈


// PriorityQueue.h
#pragma once
namespace Joy
{
  // 仿函数
  template <class T>
  struct less
  {
    bool operator()(const T& left, const T& right)
    {
      return left < right;
    }
  };
  template <class T>
  struct greater
  {
    bool operator()(const T& left, const T& right)
    {
      return left > right;
    }
  };
  template <class T, class Container = vector<T>, class Compare = less<T>>
  class priority_queue
  {
  public:
    priority_queue()
      : _con()
    {}
    template <class InputIterator>
    priority_queue(InputIterator first, InputIterator last)
      : _con(first, last)
    {
      // 注意:这里要使用int!如果使用size_t,如果只有一个元素会出现Bug
      int size = _con.size();
      for (int i = (size - 2) / 2; i >= 0; --i)
        adjust_down(i);
    }
    void push(const T& val)
    {
      _con.push_back(val);
      adjust_up(_con.size() - 1);
    }
    void pop()
    {
      //swap(_con[0], _con[_con.size() - 1]);
      swap(_con.front(), _con.back());
      _con.pop_back();
      adjust_down(0);
    }
    bool empty() const
    {
      return _con.empty();
    }
    size_t size() const
    {
      return _con.size();
    }
    const T& top() const
    {
      assert(!empty());
      return _con[0];
    }
  private:
    Container _con; // 底层容器
    // 向上调整算法
    void adjust_up(size_t child)
    {
      size_t parent = (child - 1) / 2;
      while (child > 0)
      {
        //if(_con[parent] < _con[child])
        if (Compare()(_con[parent], _con[child])) // 匿名对象
        {
          swap(_con[parent], _con[child]);
          child = parent;
          parent = (child - 1) / 2;
        }
        else
          break;
      }
    }
    void adjust_down(size_t parent)
    {
      size_t child = 2 * parent + 1;  
      size_t size = _con.size();
      while (child < size)
      {
        // 找出较大的孩子
        if (child + 1 < size && Compare()(_con[child], _con[child + 1]))
          ++child;
        if (Compare()(_con[parent], _con[child]))
        {
          swap(_con[parent], _con[child]);
          parent = child;
          child = 2 * parent + 1;
        }
        else
          break;
      }
    }
  };
}
// Test.cpp
#include <vector>
#include <iostream>
using namespace std;
#include "PriorityQueue.h"
int main()
{
  //priority_queue<int> pq; 默认是大堆
  //Joy::priority_queue<int> pq;
  Joy::priority_queue<int, vector<int>, greater<int>> pq;
  pq.push(3);
  pq.push(2);
  pq.push(8);
  pq.push(5);
  pq.push(1);
  while (!pq.empty())
  {
    cout << pq.top() << " ";
    pq.pop();
  }
  cout << endl;
  return 0;
}

c03609845ca94711a9192a1439724e2b.png


优先级队列的实现就不讲解了,和用C语言最大的区别就是不需要自己造轮子了,直接使用 vector 适配出优先级队列。还有就是使用了仿函数,可以根据传入的仿函数生成大堆还是小堆。如果还有相关的实现细节不了解的话,可以看这篇文章:堆的实现


👉反向迭代器👈


方向迭代器其实也是一种适配器,它可以适配出各种容器的反向迭代器,其主要的是将正向迭代器进行了封装,反向迭代器 ++ 就复用正向迭代器的 - -,反向迭代器 - - 就复用正向迭代器的 ++。


#pragma once
template <class Interator, class Ref, class Ptr>
class ReverseInterator
{
public:
  typedef ReverseInterator<Interator, Ref, Ptr> Self;
  ReverseInterator(Interator it)
    : _it(it)
  {}
  Self& operator++()
  {
    --_it;
    return *this;
  }
  Self operator++(int)
  {
    auto tmp = _it;
    --_it;
    return tmp;
  }
  Self& operator--()
  {
    ++_it;
    return *this;
  }
  Self operator--(int)
  {
    auto tmp = _it;
    ++_it;
    return tmp;
  }
  Ref operator*()
  {
    auto tmp = _it;
    return *(--tmp);
  }
  // 返回当前对象的地址
  Ptr operator->()
  {
    return &(operator*());
  }
  bool operator!=(const Self& s) const
  {
    return _it != s._it;
  }
  bool operator==(const Self& s) const
  {
    return _it == s._it;
  }
private:
  Interator _it;  // 对正向迭代器进行封装
};

9b771b4a38ab426c9d67206a2e9d30d7.png

4d735c675e2d4097b74c4537210efebd.png


899ddf32ce69408c989463e90c922fde.png


因为rbegin()是用end()来构造的,rend()使用begin()来构造的,所以反向迭代器的解引用需要创建一个临时对象tmp,再返回*(--tmp)


测试反向迭代器

c9943506458c4c758ddd07fa8addde05.png

9a2ddc581db84d5497b94fd9a72ec921.png


注:以上的反向迭代器是用我们自己模拟实现的 list 来测试的!


👉总结👈


本篇博客主要讲解了什么是优先级队列、优先级队列的使用和模拟实现、仿函数以及反向迭代器的实现等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️












相关文章
|
2月前
|
存储 算法 调度
【C++打怪之路Lv11】-- stack、queue和优先级队列
【C++打怪之路Lv11】-- stack、queue和优先级队列
40 1
|
3月前
|
C++
C++(十七)仿函数
### Functor 仿函数简介 Functor(仿函数)是一种通过在类中实现 `operator()` 使其行为像函数的对象。这种方式可以让类拥有更多可定制的功能,同时保持函数般的简洁调用方式。下面展示了如何定义和使用一个计算幂运算的仿函数类,并通过示例说明了其在 `std::sort` 中的优势与灵活性。
|
5月前
|
存储 算法 Java
【C++】优先级队列priority_queue模拟实现&&仿函数
【C++】优先级队列priority_queue模拟实现&&仿函数
42 1
|
5月前
|
存储 算法 C语言
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
【C++】详解STL的适配器容器之一:优先级队列 priority_queue
|
6月前
|
C++ 容器
【c++】优先级队列|反向迭代器(vector|list)
【c++】优先级队列|反向迭代器(vector|list)
47 0
|
6月前
|
C++
C++函数对象(仿函数)
C++函数对象(仿函数)
|
6月前
|
C++ 容器
【C++】学习笔记——优先级队列
【C++】学习笔记——优先级队列
46 0
|
25天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
41 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
81 4