所谓仿函数,其实它本身不是一个函数,而是一个类,在这个类当中重载了 operator() 这个操作符,那么在外部使用这个类的 operator() 这个成员函数的时候,使用的形式就像是在使用一个函数一样,仿函数(函数对象)这个类的对象可以像函数一样使用。如下就是定义的一个仿函数:
// 简单仿函数的定义
class Less
{
public:
bool operator()(int x, int y)
{
return x < y;
}
};
像上述 less 类当中 就定义个一个 operator() 运算符重载函数,在外面使用这个函数的时候如下所示:
Less newLess;
cout << newLess(1 , 2) << endl;
由上述例子我们发现,如果不看多定义的 newLess 对象的话,在 流插入当中使用 形式就像是在使用函数一样,其实上述的代码等价于下面这个代码:
cout << newLess.operator()(1 , 2) << endl;
C++ 当中定义 仿函数只要是想要替代C当中哈数指针 。
使用方式:因为仿函数的使用方式是类似于调用函数,那么我们就可以定义多个仿函数(类),然后再使用模版来使用这些类(因为模版是需要用类型的,模版参数是类型直接使用函数不能套用模版),那么也就相当于一个位置可以使用多个函数。也就是把某一个位置写活了,而不是单纯的写死。
比如:
func(bool x)
{
if(x)
{
cout << "yes" << endl;
}
}
class less
{
bool operator()(int x , int y)
{
return x > y;
}
}
class greater
{
bool operator()(int x , int y)
{
return x < y;
}
}
template>
class My_class
{
public:
void My_class_func()
{
Comapre com;
int x = 10, y = 20;
//func(x < y); 不用这样写死了
func(com(x , y)); // 这样写看按照传入的Comapre 是什么函数模版来给func函数传入不同值
}
}
如上所示,只要在外部创建 My_class 的对象的时候,给的第二个模版参数是 less 那么,给func函数传入的就是 x > y 的bool值;如果传入的参数是 greater,那么给 func 函数传入的就是 x < y 的bool值。
这里就解决了,模版参数当中只能传入类型,如果我们想用模版来套用不同的函数,从而实现某一些代码的变化。
头文件
优先级队列本质也是一个容器适配器,它默认的容器是 vector 容器:
容器主要功能如下:
函数声明 | 接口说明 |
priority_queue()/priority_queue(first, last) |
构造一个空的优先级队列 |
empty( ) | 检测优先级队列是否为空,是返回true,否则返回 false |
top( ) | 返回优先级队列中最大(最小元素),即堆顶元素 |
push(x) | 在优先级队列中插入元素x |
pop() | 删除优先级队列中最大(最小)元素,即堆顶元素 |
我们发现,功能函数的实现和 堆 类似,其实 优先级队列的底层实现就是 二叉树当中的 堆。
根据堆的特性,优先级队列也是不支持遍历的,也就不支持迭代器,也是需要像 队列 和 栈一样 pop()掉元素才能取到所有的数据。 如下代码演示:
void text1()
{
priority_queue pq;
pq.push(5);
pq.push(3);
pq.push(1);
pq.push(2);
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
输出:
5 3 2 1
priority_queue 默认是大堆,在上述的模版参数当中也可以看出:
class Compare = less
这个参数控制了 这个 优先级队列是大堆还是小堆,这里给点缺省参数是 小堆。只不过这个缺省参数是给的不太一样,上述 less 是小于的意思,但是这里表达的意思是 大堆的意思,可以说是一个开发过程化当中的一个失误。
对于 小堆的 和 大堆的控制如下所示:
priority_queue , less> // 大堆的创建
priority_queue , greater> // 小堆的创建
这里控制小堆和大堆,使用仿函数来实现的。
对于堆的使用,看下面这个例题:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题目要求:
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
这里就可以用堆来解决,那么建大堆还是小堆呢?
本来排升序是需要建大堆的,但是这里数据量少,所以建小堆和大堆都是可以的。
建大堆代码;
int findKthLargest(vector& nums, int k) {
priority_queue pq(nums.begin(), nums.end());
while(--k)
{
pq.pop();
}
return pq.top();
}
建小堆代码:
int findKthLargest(vector& nums, int k) {
priority_queue , greater> 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();
}
关于容器适配器的介绍请看下面这博客,这里不再多讲:
#pragma once
#include
namespace My_priority_queue
{
template>
class priority_queue
{
private:
private:
Container _con;
};
}
private:
void Adjustdown(int parent)
{
int child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && _con[child] < _con[child + 1])
{
++child;
}
if (_con[child] > _con[parent])
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
public:
template
priority_queue(priority_queue first, priority_queue last)
{
while (first != last)
{
_con.push_back(*first);
++first;
}
// 建堆 向下调整建大堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--)
{
Adjustdown(i);
}
}
这里主要实现就是, 先把数据从其他容器当中拷贝到 存储 堆 的容器当中,然后进行向下调整建大堆。堆的具体实现过程可以参考下面博客当中对于堆排序的介绍:(1条消息) 数据结构(c语言版)-5_为这个查询提供一个最大化p ai 5的排序实例_chihiro1122的博客-CSDN博客
堆的删除不能直接对 堆顶 的元素进行删除,然后再把元素按照数组的方式向前移动。这样是不行的,如果这样做的话,整个 堆 的父亲 和 孩子 的关系,全乱了。
我们需要把 堆顶元素和 数组最后一个元素进行交换,然后删除数组最后一个元素,在把堆顶的元素进行向下调整建堆。
void pop()
{
swap(_con[0], _con[_con, size() - 1]);
_con.pop_back();
Adjustdown(0);
}
往数组最后一个位置插入,然后对数组最后一个位置的元素进行向上调整即可:
// 向上调整
void adjustup(int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (_con[child] > _con[parent])
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 插入元素
void push(const T& x)
{
_con.push_back(x);
adjustup(_con.size());
}
包括top()函数 size() 函数 empty()函数。
// 取堆顶的元素
const T& top()
{
return _con[0];
}
// 堆元素个数
size_t size()
{
return _con.size();
}
// 堆的判空
bool empty()
{
return _con.empty();
}
我们上述已经说过了 仿函数的用法,那么这里就直接用到 priority_queue 当中,因为建大堆和建小堆的区别,就在与向上调整和向下调整两个函数的 孩子结点和 双亲结点的大小比较关系,所以中替换这个两个函数即可:
两个仿函数,Less类实现的是大堆;Greater类实现的是小堆:
template
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template
class Greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
下面是对 向上调整和向下调整的改进:
// 向下调整
void Adjustdown(int parent)
{
Comapre com;
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[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 向上调整
void adjustup(int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
//_con[parent] < _con[child]
if (com(_con[parent] < _con[child]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
注意:因为我们上述实现的仿函数只是单纯的比较大小,如果是内置类型那么没有问题;但如果是自定义类型,那么这个类型必须要重载 operator< 和 operator> 这两个重载函数,不然会编译报错。
除了实现 operator< 和 operator> 这两个重载函数,像上述实现的仿函数其实在库当中已经实现了的,在库当中也是用 " < " " > " 来计算的,所以,其实这个仿函数我们可以不自己实现,直接用库当中的仿函数。
但是使用库当中的仿函数还是弊端,假设使用的自定义类没有实现 operator< 和 operator> 这两个重载函数,那么我们需要自己在仿函数当中去实现;其实这种情况是可以避免的,一般来说 operator< 和 operator> 这两个重载函数 在自定义类型当中就应该实现。
除了上述说的一种情况,还有一种情况,如下代码所示:
My_priority_queue::priority_queue pq;
pq.push(new Date(2023, 7, 1));
pq.push(new Date(2023, 6, 2));
pq.push(new Date(2023, 8, 3));
在 上述的 priority_queue 容器当中存储不是一个 Date 自定义类型对象,而是存储的是对象的指针,在 push()的时候新开辟的空间。
这种情况的输出结果就非常的奇怪,我们不改变代码,多次运行 可执行程序,每一次的输出结果都是不一样的:
这时候,传入的模版参数的类似是 一个指针,那么走到仿函数当中去 比较的时候,是按照地址的大小去排序的,而每一次在空间当中开辟空间的,这个空间的位置是不确定的,所以导致了结果不一样。
如果在这种情况下,想要按照日历去排序的话,就不能按照 库当中的仿函数去走了,要自己在仿函数当中进行日期的判断。
注意:不能直接使用类当中的重载运算符函数,只能自己控制的仿函数当中去实现/使用。因为指针属于是内置类型,内置类型是不能重载运算符的,Date类当中实现的 重载运算符函数是 属于这个自定义类型的,不属于指针。如下代码所示:
struct LessPDate
{
bool operator() (const Date* d1, const Date* d2)
{
return *d1 < *d2
}
}
就要像如上方式去调用 Date类 当中重载运算符函数。
之前对 list 的正向迭代器进行了实现,list 的迭代器不再像 vector 和 string一样使用原生指针来做作为迭代器,而是把指针 用一个类 进行了包装/封装,让这个迭代器使用起来 和 vector 一样,使用 解引用(*),向后迭代(++),等等操作来使用。
反向迭代器 相比于 正向迭代器,不同的就是 (++)是向前迭代器,开始位置 和 终止位置 反过来而已,那么,我们大可以直接拷贝一份 正向迭代器的 代码,然后对 operator++ 和 operator-- 两个函数换一下,然后在完善一些 typedef 的工作等等,可以实现,但是冗余。
在库 当中 list 的反向迭代器不是这样实现的:库当中的实现简单来说,在list环境下,用一个反向迭代器的类,对正向迭代器的类进行封装,(++)调用 正向迭代器当中的 (--),(--)调用 (++);
但是其实没有这么简单。库当中是实现了一个反向迭代器的类模版(反向迭代器的适配器),只要我使用某一个类的反向迭代器,就会去调用这个 反向迭代器的类模板;也就是说,库当中用一个类模板实现了全部容器的反向迭代器。
它并不是针对某一个类去实现的 反向迭代器,针对的是全部的容器。
在 STL 当中,反向迭代器的代码在 stl_iterator.h 这个文件当中:
大概是这样的;
而且,对于rend(), rbegin()的实现是 和 end (),rbegin()镜像对称的,所以我们看见了,反向迭代器当中的 解引用操作是 先 (--)后解引用的,也就是访问的是 当前位置的前一个位置:
迭代器关系如下:
由此可见是完全镜像的,反向迭代器和正向迭代器的位置是对称的。
根据上述的描述,我们下述在 list 当中的反向迭代器的实现,就参照库当中的逻辑去实现。
问题:operator*的返回值问题,因为 反向迭代器不想正向迭代器是实现在 各个 容器当中的,反向迭代器没有 这个容器存储的数据类型,正向迭代器使用模版参数来知道 容器存储数据的类型的。
在官方库当中,是用 萃取的 方式把容器当中存储的数据类型套出来的,但是这个方式非常的复杂。
另一种方式相对简单一些,就是让用的人,把容器的 数据类型 用模版参数传过来。
代码实现:
#pragma once
#include
using namespace std;
namespace reverse_iterator
{
// 迭代器类型 T* T&
template
class ReverseIterator
{
typedef ReverseIterator< iterator, Ref, ptr> self;
iterator _it;
ReverseIterator(it)
:_it(it)
{}
Ref operator*()
{
iterator tmp = it;
return *(--tmp);
}
ptr operator->()
{
return &(operator*());
}
self operator++()
{
--_it;
return _it;
}
self operator--()
{
++_it;
return _it;
}
bool operator!= (const T& x) const
{
return _it != x._it;
}
};
}
对于在类当中的实现,typedef 一下就行:
template
class xxx
{
public:
// 非 const迭代器
typedef ReverseIterator reverse_iterator;
// const 迭代器
typedef ReverseIterator const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
const_reverse_iteratorrbegin()
{
return const_reverse_iterator(end());
}
const_reverse_iteratorrend()
{
return const_reverse_iterator(begin());
}
}
这个反向迭代器,根据给的是哪一个容器的正向迭代器,就适配哪一个的反向迭代器。