一、priority_queue
1. priority_queue的介绍
- 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
- 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元
素)。- 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
- 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素- 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
2. priority_queue的使用
优先级队列
默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。
void test()
{
//默认是大堆
priority_queue<int> pq;
pq.push(2);
pq.push(5);
pq.push(0);
pq.push(9);
pq.push(3);
pq.push(1);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
//建立小堆
priority_queue<int,vector<int>,greater<int>> pq1;
pq1.push(2);
pq1.push(5);
pq1.push(0);
pq1.push(9);
pq1.push(3);
pq1.push(1);
while (!pq1.empty())
{
cout << pq1.top() << " ";
pq1.pop();
}
cout << endl;
}
经典例题
数组中第k大的元素
💕 解题思路
根据堆的性质,我们可以考虑使用数组建大堆,然后pop掉k-1个元素,这时候堆顶的数就是第k大的数,但是这种思路的时间复杂度是O(N+k*logN)
,夫如果N很大的时候,就需要浪费很多的空间,这里我们还可以考虑第二种做法,就是利用topK的思路,先用前k个数建一个小堆,然后遍历剩下的元素,遇到比堆顶大的元素就替换掉堆顶的元素。注意这里的替换是指,先pop掉堆顶的元素,然后再插入目标元素。这里的时间复杂度是O(k+(N-k)*logk)
,虽然时间上没有太大的提升,但是空间上却减少了浪费。
💕 解题代码
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
//使用topk问题的求解方式
priority_queue<int,vector<int>,greater<int>> pq(nums.begin(),nums.begin()+k);
for(int i = k;i < nums.size();i++)
{
if(nums[i] > pq.top())
{
pq.pop();
pq.push(nums[i]);
}
}
return pq.top();
}
};
二、仿函数
1. 仿函数的使用
仿函数(Functor)
又称为函数对象(Function Object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载 operator()
运算符。因为调用仿函数,实际上就是通过类对象调用重载后的 operator() 运算符。,这里的()
指的就是函数调用操作符。
template<class T>
struct Greater {
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template<class T>
struct Less {
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
int main()
{
Greater<int> gt;
cout << gt(1, 2) << endl;//相当于gt.operator()(1,2),gt是一个对象,可以像函数一样使用
cout << Less<int>()(1, 2) << endl;//直接使用匿名对象调用operator(1,2)
return 0;
}
内置类型使用仿函数
template<class T, class Compare>
void BubbleSort(T* a, int n, Compare com)
{
for (int j = 0; j < n; j++)
{
int exchange = 0;
for (int i = 1; i < n - j; i++)
{
if (com(a[i], a[i - 1]))
{
swap(a[i - 1], a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
template<class T>
struct Greater {
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template<class T>
struct Less {
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
int main()
{
Less<int> ls;
int a[] = { 2,3,4,5,6,1,2,8 };
BubbleSort(a, sizeof(a) / sizeof(int), ls);
cout << "升序排序的结果为:";
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
Greater<int> gt;
BubbleSort(a, sizeof(a) / sizeof(int), gt);
cout << "降序排序的结果为:";
for (auto e : a)
{
cout << e << " ";
}
cout << endl;
return 0;
}
下面我们看一下仿函数的优缺点:
优点:
- 在同一时间里,由某个仿函数所代表的单一函数,可能有不同的状态(可以根据不同的类型代表不同的状态)
- 仿函数即使定义相同,也可能有不同的类型(可以有不同的类型)
- 仿函数使程序代码变得简单(仿函数在一定程度上使代码更通用,本质上简化了代码)
缺点:
仿函数比一般函数速度慢
自定义类型使用仿函数
前面我们谈到了内置类型,下面我们来看一下日期类这个自定义类型如何使用仿函数:
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;
};
int main()
{
//建立小堆
priority_queue<Date,vector<Date>, greater<Date>> q;
q.push(Date(2023, 1, 1));
q.push(Date(2023, 4, 7));
q.push(Date(2020, 1, 1));
cout << q.top() << endl;
//建立大堆
priority_queue<Date> q2;
q2.push(Date(2023, 1, 1));
q2.push(Date(2023, 4, 7));
q2.push(Date(2020, 1, 1));
cout << q2.top() << endl;
return 0;
}
自定义类型自己定义仿函数
如果传入的是一个Date*的指针的时候,这个时候仿函数中的运算符重载已经不符合我们的要求了,这个时候我们就需要自己定义仿函数
template<class T>
struct Greater {
bool operator()(const Date* x, const Date* y)
{
return *x > *y;
}
};
template<class T>
struct Less {
bool operator()(const Date* x, const Date* y)
{
return *x < *y;
}
};
2. priority_queue的模拟实现
这里的priority_queue就是我们在数据结构阶段讲过的优先级队列——堆,所以我们在这里只需要用类模板和适配器模式来封装一下即可,当然了,这里我们还会用到仿函数来调整堆的构建方式是大堆还是小堆。
namespace cjl
{
template<class T>
struct greater {
bool operator()(const T& x, const T& y) const
{
return x > y;
}
};
template<class T>
struct less {
bool operator()(const T& x, const T& y) const
{
return x < y;
}
};
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
priority_queue()
{}
template<class Iterator>
priority_queue(Iterator first, Iterator last)
{
while (first != last)
{
push(*first);
first++;
}
}
//向上调整算法
void adjust_up(int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (_com(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整算法
void adjust_down(int parent)
{
int child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && _com(_con[child], _con[child + 1]))
{
child++;
}
if (_com(_con[parent], _con[child]))
{
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()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
//向下调整算法
adjust_down(0);
}
//取堆中数据个数
size_t size() const
{
return _con.size();
}
//取堆顶数据
const T& top() const
{
return _con[0];
}
//堆的判空操作
bool empty() const
{
return _con.empty();
}
private:
Container _con;
Compare _com;
};
}
三、反向迭代器
1. 反向迭代器的使用
前面我们讲解list的时候对正向迭代器已经有了一个很好的认识,下面我们来认识一下反向迭代器
其中正向迭代器是iterator
和const_iterator
,反向迭代器则为:reverse_iterator
和const_reverse_iterator
,其实反向迭代器和正向迭代器的区别如下:
- rbegin()相当于end()
- rend()相当于begin()
- 反向迭代器的++相当于正向迭代器–
- 其他的操作和正向迭代器相同
int main()
{
int arr[] = { 1,2,3,4,5 };
list<int> lt(arr, arr + 5);
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
return 0;
}
2. 反向迭代器的模拟实现
这里我们可以先来看一下STL库中的反向迭代器是如何实现的,在stl_iterator.h
这个头文件中,部分源码如下:
template <class Iterator>
class reverse_iterator {
protected:
Iterator current;
public:
typedef Iterator iterator_type;
typedef reverse_iterator<Iterator> self;
public:
reverse_iterator() {}
explicit reverse_iterator(iterator_type x) : current(x) {}
reverse_iterator(const self& x) : current(x.current) {}
reference operator*() const {
Iterator tmp = current;
return *--tmp;
}
#ifndef __SGI_STL_NO_ARROW_OPERATOR
pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
self& operator++() {
--current;
return *this;
}
self& operator--() {
++current;
return *this;
}
//...
};
这里我们也需要模仿库里面,反向迭代器是一个容器适配器,他的适配器就是正向迭代器,这样它就能根据传递过来的正向迭代器的不同实例化出对应的反向迭代器了。因此,我们只需要将每个容器对应的正向迭代器传递过来,我们就可以实现出对应的反向迭代器了。
namespace cjl
{
template<class Iterator,class Ref,class Ptr>
class ReverseIterator
{
public:
typedef ReverseIterator<Iterator, Ref, Ptr> Self;
//构造函数
ReverseIterator(const Iterator& it)
:_cur(it)
{}
//重载*
Ref operator*()
{
auto tmp = _cur;
return *--tmp;
}
//重载&
Ptr operator->()
{
return &(*_cur);
}
//重载前置++
Self& operator++()
{
--_cur;
return *this;
}
//重载后置++
Self operator++(int)
{
auto tmp = _cur;
_cur--;
return tmp;
}
//重载前置--
Self& operator--()
{
++_cur;
return *this;
}
//重载后置--
Self& operator--(int)
{
auto tmp = _cur;
_cur++;
return tmp;
}
//重载!=
bool operator!=(const Self& s)
{
return _cur != s._cur;
}
//重载==
bool operator==(const Self& s)
{
return _cur = s._cur;
}
private:
Iterator _cur;
};
}
这里我们可以在我们之前自己模拟实现的list容器中测试一下: