C++ - 优先级队列(priority_queue)的介绍和模拟实现 - 反向迭代器的适配器实现

 仿函数

 所谓仿函数,其实它本身不是一个函数,而是一个类,在这个类当中重载了  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 T, class Comapre = less<T>>
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值。

这里就解决了,模版参数当中只能传入类型,如果我们想用模版来套用不同的函数,从而实现某一些代码的变化。 

优先级队列

 头文件 <queue>  

 优先级队列本质也是一个容器适配器,它默认的容器是 vector 容器:

 容器主要功能如下:

函数声明 接口说明
priority_queue()/priority_queue(first,
last)
构造一个空的优先级队列
empty( ) 检测优先级队列是否为空,是返回true,否则返回
false
top( ) 返回优先级队列中最大(最小元素),即堆顶元素
push(x) 在优先级队列中插入元素x
pop() 删除优先级队列中最大(最小)元素,即堆顶元素

 我们发现,功能函数的实现和 堆 类似,其实 优先级队列的底层实现就是 二叉树当中的 堆。

 根据堆的特性,优先级队列也是不支持遍历的,也就不支持迭代器,也是需要像 队列 和 栈一样 pop()掉元素才能取到所有的数据。 如下代码演示:

void text1()
{
	priority_queue<int> 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<typename Container::value_type>

这个参数控制了 这个 优先级队列是大堆还是小堆,这里给点缺省参数是 小堆。只不过这个缺省参数是给的不太一样,上述 less 是小于的意思,但是这里表达的意思是 大堆的意思,可以说是一个开发过程化当中的一个失误。

对于 小堆的 和 大堆的控制如下所示:

priority_queue<int , vector<int> , less<int>>      // 大堆的创建
priority_queue<int , vector<int> , greater<int>>  //  小堆的创建

这里控制小堆和大堆,使用仿函数来实现的

对于堆的使用,看下面这个例题:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

 题目要求:

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

 这里就可以用堆来解决,那么建大堆还是小堆呢?

本来排升序是需要建大堆的,但是这里数据量少,所以建小堆和大堆都是可以的。

建大堆代码;

    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int> pq(nums.begin(), nums.end());

        while(--k)
        {
            pq.pop();
        }

        return pq.top();

    }

建小堆代码:

          int findKthLargest(vector<int>& nums, int k) {

    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();
    }

priority_queue 优先级队列的模拟实现

 关于容器适配器的介绍请看下面这博客,这里不再多讲:

 大体框架

#pragma once
#include<vector>

namespace My_priority_queue
{
	template<class T , class Container = vector<T>>
	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<class Inputiterator>
		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博客

 增删查改

 pop()

 堆的删除不能直接对 堆顶 的元素进行删除,然后再把元素按照数组的方式向前移动。这样是不行的,如果这样做的话,整个 堆 的父亲 和  孩子 的关系,全乱了。

我们需要把 堆顶元素和 数组最后一个元素进行交换,然后删除数组最后一个元素,在把堆顶的元素进行向下调整建堆

		void pop()
		{
			swap(_con[0], _con[_con, size() - 1]);
			_con.pop_back();
			Adjustdown(0);
		}

push()

 往数组最后一个位置插入,然后对数组最后一个位置的元素进行向上调整即可:

// 向上调整
		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 的大堆和小堆的选择

 我们上述已经说过了 仿函数的用法,那么这里就直接用到 priority_queue 当中,因为建大堆和建小堆的区别,就在与向上调整和向下调整两个函数的 孩子结点和 双亲结点的大小比较关系,所以中替换这个两个函数即可:

两个仿函数,Less类实现的是大堆;Greater类实现的是小堆:

	template<class T>
	class Less
	{
	public:
		bool operator()(const T& x, const T& y)
		{
			return x < y;
		}
	};

	template<class T>
	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<Date*> 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<iostream>

using namespace std;

namespace reverse_iterator
{
	//			迭代器类型     T*         T&
	template<class iterator, class Ref, class ptr>
	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 T, class Ref, class ptr>
class xxx
{
public:
    // 非 const迭代器
    typedef ReverseIterator<T, T* , T&> reverse_iterator;
    // const 迭代器
    typedef ReverseIterator<T, const T* , const T&> 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());
    }

}

这个反向迭代器,根据给的是哪一个容器的正向迭代器,就适配哪一个的反向迭代器。

猜你喜欢

转载自blog.csdn.net/chihiro1122/article/details/131954198
今日推荐