仿函数
优先级队列是一个容器适配器,它的默认容器是vector。优先级队列底层是由堆写的,所以它的push和pop也要参考堆来写。优先级队列默认为大堆,库中写的是less,如果要用小堆,要用模板参数greater作为第三个参数,第二个参数必须要写。像greater就是仿函数,也叫函数对象。使用优先级队列库的时候也要引入< functional >头文件来使用仿函数。
priority_queue<int, vector<int>, greater<int>>//小堆
priority_queue<int>//大堆
先写仿函数,再写优先级队列。
假设我们要写一个小于的仿函数,仿函数都有一个特点,重载()函数。
struct Less
{
bool operator()(int x, int y)
{
return x < y;
}
};
int main()
{
Less lessFunc;
cout << lessFunc(1, 2) << endl;
return 0;
}
使用这个类对象的时候,写出来的代码就和使用函数一样,但实际上它是一个类的使用。当然应该加上模板参数T,要不这个类只能比较int类型。
template<class T>
struct Less
{
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
int main()
{
Less<int> lessFunc;
cout << lessFunc(1, 2) << endl;
return 0;
}
为什么加引用?之前写过,引用会让代码更高效;const则是为了适应const和普通版本。
仿函数的出现有一部分原因是函数指针使用上的一些不便,而仿函数就可以易于理解。
优先级队列
练习
void test_priority_queue()
{
//priority_queue<int, vector<int>, greater<int>> pq;
priority_queue<int> pq;
//priority_queue<int, deque<int>> pq;
pq.push(1);
pq.push(0);
pq.push(5);
pq.push(2);
pq.push(1);
pq.push(7);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
知道了优先级队列的部分属性,我们做一个题。
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
之前通过堆来写,现在就可以优先级队列来做。当然sort一下也可以。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int> pq(nums.begin(), nums.begin() + k);
while(--k)
{
pq.pop();
}
return pq.top();
}
};
默认大堆,生成优先级队列后,一次次pop,最后停的位置就是第k个大的数字。这个代码的时间复杂度O(N + k * logN),建堆的时候是N,pop向下调整的时候是k * logN。
如果N远大于k,且N很大,这时候就要用到之前文件排序时提到的,我们只建k个数的堆,然后遍历所有数据,和堆顶依次比较入堆,这里就需要小堆,最后取堆顶就行。
int findKthLargest(vector<int>& nums, int 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();
}
拿前k个数据建堆,拿之后的所有来比较。这时候时间度就变成了O(k + (N - k) * logk)。
模拟实现
先不写仿函数less和greater,先写两个模板参数class T和class Container = vector< T >。优先级队列是用堆实现的,它的插入和删除就需要向上调整和向下调整
template <class T, class Container = vector<T>>
class priority_queue
{
public:
void adjust_up(int child)
{
//Comapre com;
int parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
//if (Comapre()(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void adjust_down(int parent)
{
size_t child = parent * 2 + 1;
while (child < _con.size())
{
Comapre com;
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size()
&& com(_con[child], _con[child + 1]))
{
++child;
}
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
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);
}
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;
priority_queue<int, deque<int>> pq;
pq.push(1);
pq.push(0);
pq.push(5);
pq.push(2);
pq.push(1);
pq.push(7);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
即使第二个模板参数传了一个双端队列,也可以运行程序,这就是适配器的体现。
现在是大堆,如果要建小堆要怎么写?我们不可能把代码改一下,而是应该很简便地一用就换成小堆,所以这里就要用到仿函数了,新增一个Compare参数,默认 = less< T >。我们跟着库里的写。写好这个后,向下/上调整就可以改一下判断了。
template<class T>
struct less
{
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
struct greater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template <class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:
void adjust_up(int child)
{
Comapre com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void adjust_down(int parent)
{
size_t child = parent * 2 + 1;
while (child < _con.size())
{
Comapre com;
//if (child + 1 < _con.size() && _con[child] < _con[child + 1])
if (child + 1 < _con.size()&& com(_con[child], _con[child + 1]))
{
++child;
}
//if (_con[parent] < _con[child])
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
实际上,仿函数是一种泛型编程
优先级队列还可以处理自定义类型的数据。假如用日期类数据,可以排出日期类的数据堆,拿到最大或者最小的那个。如果这样写
priority_queue<Date*> pq;
pq.push(new Date(2023, 3, 17));
pq.push(new Date(2023, 3, 18));
pq.push(new Date(2023, 3, 19));
cout << *(pq.top()) << endl;
建立小堆,会打印出2023,3,17,但是多次运行,下面那行数字会变大,17变成18,变成19。
此时的T是Date*, new出来的数据的大小无法准确地掌握,不一定会返回我们想要的结果,比较的也是指针。
这里的解决办法就是不用优先级队列里用的less和greater类,再定义一个Date数据比较大小的类
class PDateless
{
public:
bool operator()(constDate* p1, const Date* p2)
{
return *p1 < *p2;
}
};
priority_queue<Date*, vector<Date>, PDateless> pq;
建一个简单的日期类,传给优先级队列:
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;
};
class PDateLess
{
public:
bool operator()(const Date* p1, const Date* p2)
{
return *p1 < *p2;
}
};
class PDateGreater
{
public:
bool operator()(const Date* p1, const Date* p2)
{
return *p1 > *p2;
}
};
void test_priority_queue2()
{
// 大堆,需要用户在自定义类型中提供<的重载
//priority_queue<Date> q1;
priority_queue<Date, vector<Date>, greater<Date>> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 30));
q1.push(Date(2018, 10, 28));
cout << q1.top() << endl;
//priority_queue<Date*, vector<Date*>, PDateLess> q2;
priority_queue<Date*, vector<Date*>, PDateGreater> q2;
q2.push(new Date(2018, 10, 29));
q2.push(new Date(2018, 10, 30));
q2.push(new Date(2018, 10, 28));
cout << *(q2.top()) << endl;
}
反向迭代器(重点理解)
List类的反向迭代器
List是带头双向循环链表
反向迭代器的++要倒着走,那是不是把正向迭代器改变几个符号就可以了?begin就是rend,end就是rbegin,++就是- -,- -就是++,typedef一个reverse_iterator,然后照着正向的代码,也传入模板参数T,Ref,Ptr,用的时候传普通和const版本就好了,反向迭代器也就完成了,然而当这样写出来后,才发现了这代码根本不高效。
库中的源代码是怎么实现的?
typedef reverse_iterator<const_iterator> const_reverse_iterator;
typedef reverse_iterator<iterator> reverse_iterator;
源码传了一个正向迭代器作为模板参数,源码中把迭代器实现为一个适配器。C++很不喜欢冗余的代码,既然有相似的正向迭代器,就用相似的来实现。
实际写的话,还是有不同之处。反向迭代器的解引用取的是当前位置的前一个。
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
const_reverse_iterator rbegin() const
{
return const_reverse_iterator(end());
}
iterator begin() { return (link_type)((*node).next); }
iterator end() { return node; }
上面的代码就显示了,end在begin前面。我们看下面的解析。
list是带头双向循环链表,end在头节点位置,begin在头节点的下一个位置。rbegin在end位置,rend在begin位置。使用反向迭代器的时候我们会这样写
rit = rbegin();
while(rit != rend())
{
cout << *rit << " ";
++rit;
}
rit取rbegin,也就是头节点位置,判断条件是不等于rend,然后解引用,前置++。其实rit就是一个指针,指向头节点,不过这个指针是正向迭代器类型,它要解引用,它要++,- -就要使用正向迭代器类里的方法,就如同内置类型的指针做法一样,只是内置类型指针的做法库中已经定义,我们直接用即可。所以我们为什么要重载解引用,重载++?自定义类型的数据,我们要自己写对应的方法来实现其功能。
rbegin和rend知道了,那么解引用和++呢?库中这样实现解引用
reference operator*() const//这是源码,我们只看思路,reference不用看
{
Iterator tmp = current;//取现在的节点
return *--tmp;//返回前一个节点的值
}
而正向迭代器的–是这样实现的
self& operator--()
{
_node = _node->prev;
return *this;
}
解引用时,tmp拿到当前节点,但是当前节点是从头节点开始的,这里不能直接解引用,所以它往前走一步,就用到了前置- -,也就是正向迭代器的- -,可以看到取节点的前一个,然后返回,返回后对此解引用,得到的就是前一个的值,也就是5,打印出了5,但是此时rit没有动,只是tmp在移动,前面tmp拿到rit指向的节点来进行操作;解引用完后++rit,反向迭代器的++就是正向迭代器的–,节点就来到了5这个位置,这时候再次解引用,还是上面的步骤,拿到了4,打印4,再次++,节点来到4。最后节点是2时,打印出1,已经全部打印出来了,++rit,节点来到1,也就是rend的位置,这时候条件不符合,while循环结束。
这里就体现了反向迭代器是如何封装正向迭代器来实现自己的。
我们还是要自己模拟实现。
正向迭代器在物理空间是一个指针,访问内置类型,就是解引用,访问自定义类型,就是调用一个对应的函数,那么这时候它就起到了函数指针的作用,只是这个指针的类型是自定义类型。反向迭代器也一样。
template<class Iterator>
struct ReverseIterator
{
typedef ReverseIterator<Iterator> Self;
Iterator _cur;
ReverseIterator(Iterator it)
:_cur(it)
{}
operator*()
{
Iterator tmp = _cur;//为了不改变原有结构,就用tmp
--tmp;
return *tmp;
}
Self& operator++()
{
--_cur;
return *this;
}
Self operator++(int)
{
Iterator tmp = _cur;
--_cur;
return tmp;
}
Self& operator--()
{
++_cur;
return *this;
}
Self operator--(int)
{
Iterator tmp = _cur;
++_cur;
return tmp;
}
bool operator!=(const Self& s)
{
return _cur != s._cur;
}
};
重载*函数,需要有const和普通版本,因为传过来迭代器模板参数有const和普通版本。加两个模板参数。
template<class Iterator, class Ref, class Ptr>
struct ReverseIterator
{
typedef ReverseIterator<Iterator, Ref, Ptr> Self;
Iterator _cur;
ReverseIterator(Iterator it)
:_cur(it)
{}
Ref operator*()
{
Iterator tmp = _cur;//为了不改变原有结构,就用tmp
--tmp;
return *tmp;
}
list中
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
typedef ReverseIterator<iterator, const T&, const T*> const_reverse_iterator;
//......
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
测试用例
void test_list()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
(*it) *= 2;
cout << *it << " ";
++it;
}
cout << endl;
list<int>::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
整体的迭代器部分代码
template<class T>
struct list_node
{
list_node<T>* next;
list_node<T>* prev;
T data;
list_node(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
};
template <class T, class Ref, class Ptr>
struct _list_iterator
{
typedef list_node<T> node;
typedef _list_iterator<T, Ref, Ptr> self;
node* _node;
_list_iterator(node* n)
:_node(n)
{}
Ref operator*()//const &
{
return _node->data;
}
Ptr operator->()//const T*
{
return &_node->data;
}
self& operator++()
{
_node = _node->next;
return *this;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
self& operator++()
{
_node = _node->next;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->next;
return tmp;
}
self& operator--()
{
_node = _node->prev;
return *this;
}
self operator--(int)
{
self tmp(*this);
_node = _node->prev;
return tmp;
}
bool operator==(const self& s)
{
return _node != s._node;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
};
template<class Iterator, class Ref, class Ptr>
struct ReverseIterator
{
typedef ReverseIterator<Iterator, Ref, Ptr> Self;
Iterator _cur;
ReverseIterator(Iterator it)
:_cur(it)
{}
Ref operator*()
{
Iterator tmp = _cur;//为了不改变原有结构,就用tmp
--tmp;
return *tmp;
}
Self& operator++()
{
--_cur;
return *this;
}
Self operator++(int)
{
Iterator tmp = _cur;
--_cur;
return tmp;
}
Self& operator--()
{
++_cur;
return *this;
}
Self operator--(int)
{
Iterator tmp = _cur;
++_cur;
return tmp;
}
bool operator!=(const Self& s)
{
return _cur != s._cur;
}
};
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
typedef ReverseIterator<iterator, const T&, const T*> const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
vector反向迭代器
vector如何写反向迭代器?其实刚才的思路就已经写出来了适配器。只要支持++和–,就能通过正向迭代器适配出反向迭代器。如果只是按照正向迭代器去写一个反向迭代器,只能写出一个类型的,但如果把正向迭代器当作一个模板参数,就可以适配出所有的反向迭代器,只要是双向的,也就是支持++和–。
一个例子
void test_vector8()
{
bit::vector<int> v1;
v1.push_back(10);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(50);
bit::vector<int>::reverse_iterator rit = v1.rbegin();
while (rit != v1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
再拿出这个图来看,可以仔细想一想这个代码怎么走的。
反向迭代器封装正向迭代器,也就是用正向迭代器作为模板来实现自己,和正向迭代器封装指针,其实是一样的规则。正向迭代器有模板参数T*,const T*,也有引用,当然反向迭代器也有;编译器遇到这个指针是反向迭代器的,它就使用反向迭代器的方法,而这个方法是程序员自己定义的;我们可不可以重载内置类型的指针等的方法?可以,重载后编译器识别到相关类型就用对应的方法。它们都被放进库里,引用头文件后就可以使用,或者用自己重载的方法。
反向迭代器需要好好琢磨,这也是对C++类和泛型编程的一个深层次理解。
结束。