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

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

一. 优先级队列的使用


头文件:<queue>


0a2653c851af460fa595bd959398a8f1.png


Container :默认情况下,它适配的是vector(因为要大量用到[]找下标)。理论上底层的容器可以是任何标准容器类模板,也可以是其他特定设计的容器类,但是必须支持随机访问迭代器访问,以及一系列基本接口。

Compare:默认情况下,大的优先级高(即默认是大堆),仿函数给的是less(这确实有点奇怪)。小堆需要传入仿函数类型,它的头文件在<functional>中

void test_priority_queue()
{
  //默认大的优先级高
  priority_queue<int> pq;
  pq.push(3);
  pq.push(1);
  pq.push(2);
  pq.push(5);
  pq.push(9);
  pq.push(0);
  while (!pq.empty())
  {
  cout << pq.top() << " ";
  pq.pop();
  }
  cout << endl;
  int a[] = { 3, 1, 5, 7, 4, 0, 3, 2 };
  priority_queue<int> heap(a, a + sizeof(a) / sizeof(int));//区间初始化
  while (!heap.empty())
  {
  cout << heap.top() << " ";
  heap.pop();
  }
  cout << endl;
}

0a2653c851af460fa595bd959398a8f1.png

多说无益,做一道题目上手:215. 数组中的第K个最大元素


方法一:建大堆(堆排) + pop (k-1)次取堆顶


时间复杂度:O(N+k*logN)


class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        //建堆  O(N)
        priority_queue<int> maxHeap(nums.begin(), nums.end());
        //O(logN* K)
        while(--k)
        {
            maxHeap.pop();
        }
        return maxHeap.top();
    }
};


一. priority_queue的模拟实现


priority_queue框架,他的底层是一个随机容器,模板第三个参数(仿函数)后面详谈——


🌈size & empty & top


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


🌈仿函数


灵活控制的开关(仿函数):是排大堆还是小堆,总不可能改代码吧


我们写一个类,没有任何成员变量,重载了operator()运算符 ——


//仿函数/函数对象 ---- 类 重载了operator()
//类对象可以像函数一样去使用
namespace ljj
{
  template<class T>
  class less
  {
  public:
  bool operator()(const T& l, const T& r) const
  {
    return l < r;
  }
  };
  template<class T>
  class greater
  {
  public:
  bool operator()(const T& l, const T& r) const
  {
    return l > r;
  }
  };
}
  priority_queue<int>  heap(a, a + sizeof(a) / sizeof(int));
  priority_queue<int, vector<int>, greater<int>> heap(a, a + sizeof(a) / sizeof(int));


不同仿函数类型的对象,用()来调用对应的函数比较方式,就实现了像函数一样调用 ——


int main()
{
  ljj::less<int> lsFunc;
  cout << lsFunc(1, 2) << endl;
  //等价于下面
  //cout << lsFunc.operator()(1, 2) << endl;
  ljj::greater<int> gtFunc;
  cout << gtFunc(1, 2) << endl;
  return 0;
}


🧐优缺点:

🌍优点如下:


在同一时间里,由某个仿函数所代表的单一函数,可能有不同的状态(可以根据不同的类型代表不同的状态)


仿函数即使定义相同,也可能有不同的类型(可以有不同类型)


仿函数使程序代码变简单(仿函数在一定程度上使代码更通用,本质上简化了代码)


🌍缺点:


仿函数通常比一般函数速度慢


🎨push 和向上调整算法


优先级队列的插入及删除,就是在堆的基础上做插入删除,学到这我还回去复习了一下堆


堆插 = 尾插+ 向上调整


//插入 —— 向上调整
  void push(const T& x)
  {
  _con.push_back(x);
  adjust_up(_con.size()-1);
  }


🍅重头戏:向上调整算法 (看图5分钟内写出来,才可以)


0a2653c851af460fa595bd959398a8f1.png


//向上调整
void adjust_up(size_t child)
{
  Compare com;//仿函数
  int parent = (child - 1) / 2;
  while (child > 0)
  {
  //if ( _con[child] > _con[parent])
  if (com(_con[parent] , _con[child]))
  {
    std::swap(_con[child], _con[parent]);
    child = parent;
    parent = (child - 1) / 2;
  }
  else
  {
    break;
  }
  }
}


🎨pop 和向下调整算法


堆删 = 交换 + 删除 + 向下调整(不会破坏树的结构)


//交换 —— 删除 —— 向下调整
  void pop()
  {
  std::swap(_con[0], _con[_con.size() - 1]);
  _con.pop_back();
  adjust_down(0);
  }


💦向下调整算法


0a2653c851af460fa595bd959398a8f1.png


//高度次  logN  
void adjust_down(size_t parent)
{
  Compare com;//仿函数
  size_t child = parent * 2 + 1;//左孩子
  while (child < _con.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]))
  {
    std::swap(_con[child], _con[parent]);
    parent = child;
    child = parent + 1;
  }
  else
  {
    break;
  }
  }
}


建大堆还是建小堆本质是由于ajust_up和ajust_down中的比较符号不同,那么就要通过仿函数来控制


0a2653c851af460fa595bd959398a8f1.png


构造函数

🌈 迭代器区间构造:_con自定义类型会调用它的迭代器区间构造,不用再迭代器遍历+push了。在这基础上还要构建成堆,为了使左右都是大(小)堆且向下调整是O(N),要倒着建,从最后一个非叶子节点(即最后一个节点的父亲)开始向下调整。


priority_queue()
{}
//迭代器区间构造
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
  while (first != last)
  {
  _con.push_back(*first);
  ++first;
  }
  //向下调整 建堆
  for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
  {
  adjust_down(i);
  }
}


如果T是自定义类型

⚡我们考虑到如果T 是日期类Date等 —— 要看情况


如果是类里面有支持比较大小的,就直接用 比如:string类

如果是库里面的、人家写好的类,我们只能写仿函数,我们不可能改人家的代码,如果是自己写的类,二者都可以(看情况)

但有些情况是必须写仿函数的,因为原生比较大小的行为不一定是我们想要的,比如:Date*比较大小,但是我们不想用指针比较,那就写仿函数

void test_priority_queue()
  {
  //priority_queue<Date> pq;
  priority_queue<Date, vector<Date>, greater<Date>> pq;
  pq.push(Date(2022, 3, 26));
  pq.push(Date(2022, 4, 1));
  pq.push(Date(2022, 2, 19));
  pq.push(Date(2022, 4, 10));
  while (!pq.empty())
  {
    cout << pq.top() << endl;
    pq.pop();
  }
  }


我们当然可以重载这些运算符


但是如果数据类型 不支持比较 或 比较的方式不是你想要的,可以自己实现仿函数,按照你想要的方式(Compare给我们留的口子)去控制比较逻辑,比如 —— 我想比较地址大小:Date*


void test_priority_queue3()
  {
  //priority_queue<Date*> pq; //默认比较地址大小
  //priority_queue<Date*, vector<Date*>, lessPDate> pq;
  priority_queue<Date*, vector<Date*>, greaterPDate> pq;
  pq.push(new Date(2022, 3, 26));
  pq.push(new Date(2022, 4, 1));
  pq.push(new Date(2022, 2, 19));
  pq.push(new Date(2022, 4, 10));
  //默认比较地址大小,若想比较日期大小,自己写仿函数
  while (!pq.empty())
  {
    cout << *pq.top() << endl;
    pq.pop();
  }
  }


于是我们自己写了仿函数,又因为私有成员类外无法访问,我们把这两个仿函数类声明为priority_queue的友元类 ——


struct lessPDate
  {
  bool operator()(const Date* d1,const Date* d2)
  {
    //return *d1 < *d2;
    return (d1->_year < d2->_year) ||
    (d1->_year == d2->_year && d1->_month < d2->_month) ||
    (d1->_year == d2->_year && d1->_month == d2->_month && d1->_day < d2->_day);
  }
  };
  struct greaterPDate
  {
  bool operator()(const Date* d1, const Date* d2)
  {
    //return *d1 > *d2;
    return (d1->_year > d2->_year) ||
    (d1->_year == d2->_year && d1->_month > d2->_month) ||
    (d1->_year == d2->_year && d1->_month == d2->_month && d1->_day > d2->_day);
  }
  };
相关文章
|
2月前
|
存储 算法 调度
【C++打怪之路Lv11】-- stack、queue和优先级队列
【C++打怪之路Lv11】-- stack、queue和优先级队列
37 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)
43 0
|
6月前
|
C++
C++函数对象(仿函数)
C++函数对象(仿函数)
|
16天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
26 2
|
22天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
55 5
|
28天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
58 4
|
29天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
71 4