【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 来测试的!


👉总结👈


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












相关文章
|
6天前
|
编译器 C语言 C++
【C++进阶(七)】仿函数深度剖析&模板进阶讲解
【C++进阶(七)】仿函数深度剖析&模板进阶讲解
|
6天前
|
设计模式 C语言 C++
【C++进阶(六)】STL大法--栈和队列深度剖析&优先级队列&适配器原理
【C++进阶(六)】STL大法--栈和队列深度剖析&优先级队列&适配器原理
|
6天前
|
C++
C++ operator关键字的使用(重载运算符、仿函数、类型转换操作符)
C++ operator关键字的使用(重载运算符、仿函数、类型转换操作符)
32 0
|
6天前
|
存储 算法 C++
C++初阶(十六)优先级队列
C++初阶(十六)优先级队列
27 0
|
6天前
|
搜索推荐 C++
【C++】lambda解决个性化排序问题(对比仿函数)(代码演示)
【C++】lambda解决个性化排序问题(对比仿函数)(代码演示)
|
6天前
|
存储 C++ 容器
C++ STL学习之【优先级队列】
C++ STL学习之【优先级队列】
38 0
|
6天前
|
C++ 容器
【C++】仿函数在模板中的应用——【默认模板实参】详解(n)
【C++】仿函数在模板中的应用——【默认模板实参】详解(n)
|
6天前
|
算法 C++ 容器
【C++】STL容器适配器——priority_quene(堆/优先级队列)类的使用指南(含代码使用)(19)
【C++】STL容器适配器——priority_quene(堆/优先级队列)类的使用指南(含代码使用)(19)
|
5月前
|
存储 算法 C++
C++优先级队列priority_queue详解及其模拟实现
C++优先级队列priority_queue详解及其模拟实现
53 0
|
6月前
|
存储 算法 测试技术
【C++从0到王者】第十八站:手把手教你写一个简单的优先级队列
【C++从0到王者】第十八站:手把手教你写一个简单的优先级队列
67 0