【C++ 第八章】模拟实现 stack 与 queue 类 + 仿函数的介绍与使用(应用:模拟实现 priority_queue)

1. stack的介绍和使用

1.1 stack的介绍

stack 的官方文档 介绍

翻译:

1. stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行 元素的插入与提取操作。

2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定 的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。(容器适配器的人话:就是将其他 容器类封装,加入特定操作,创建出的新的类就是 容器适配器)

3. stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下 操作:

  • empty:判空操作
  • back:获取尾部元素操作
  • push_back:尾部插入元素操作
  • pop_back:尾部删除元素操作

4. 标准容器  vector、deque、list  均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器, 默认情况下使用 deque


1.2 stack 的 模拟实现

先前我们独立实现一个 栈:什么成员都要自己设计

template <class T>
class stack
{
private:
    T* _a;
    size_t top;
    size_t capacity;
};

现在,我们直接将 成型的 容器类 二次封装成 stack

可以是 vector 数组栈,可以是 list 链式栈

template <class T>
class stack
{
 
private:
    vector<T> _a;
    // 或者
    // list<T> _a;
    // deque<T> _a;
};

我们通过直接控制 vector 或 list 或 deque 的行为,间接的实现一个 栈的功能

因为 stack 可以由  vector 或 list  或 deque 多种容器作为内核实现 ,干脆将容器的选择 写成一个 模板参数 class Container ,让用户自己选择使用哪种容器作为内核

同时直接借助 这些容器自己的功能 实现 stack 的需求

template <class T, class Container>
class stack
{
public:
	void push(const T& x) {
		_con.push_back(x);
	}

	void pop() {
		_con.pop_back();
	}

	const T& top(){
		return _con.back();
	}

	bool empty() {
		return _con.empty();
	}
private:
	Container _con;
};

自己选择容器  使用样例

stack<int, vector<int>> st;
st.push(1);
st.push(2);

有人说:为什么这里还要自己 写一个 参数 vector< int >,好像使用 库里面的 stack 直接 stack< int > 就行

因为库里面的 stack 写了缺省参数:若不显式传 容器类型,则默认使用 vector< T > 作为 stack 底层容器

template <class T, class Container = vector<T>>

2. queue的介绍和使用

2.1 queue的介绍

queue 的官方文档

翻译:有序列表

1. 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端 提取元素。

2. 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的 成员函数来访问其元素。元素从队尾入队列,从队头出队列。

3. 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。

该底层容器应至少支持以下操 作:

  • empty:检测队列是否为空
  • size:返回队列中有效元素的个数
  • front:返回队头元素的引用
  • back:返回队尾元素的引用
  • push_back:在队列尾部入队列
  • pop_front:在队列头部出队列

4. 标准容器类 deque 和 list 满足了这些要求。默认情况下,如果没有为 queue 实例化指定容器类,则默认使用标准容器 deque

不能使用 vector ,因为 vector 不支持 头删(或者说效率较低)

如果硬要使用 vector ,则头删可以调用 vector 的 erase 

2.2 queue 的 模拟实现

queue 的实现和前面讲解的 stack 的模拟实现的原理一样,这里就不赘诉

template <class T, class Container = deque<T>>
class queue
{
public:
	void push(const T& x) {
		_con.push_back(x);
	}

	void pop() {
		_con.pop_front();
		// 这里换成 erase 就可以使用 vector 作为底层适配容器了(同时 list 和 deque 也可以使用),但是效率较低
		// _con.erase(begin())
	}

	const T& top() {
		return _con.front();
	}

	bool empty() {
		return _con.empty();
	}
private:
	Container _con;
};

3.1 priority_queue的介绍和使用

3.1 priority_queue的介绍

priority_queue 的官方文档

1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的

2. 类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。

3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特 定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。

4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。

容器应该可以通过随机访问迭 代器访问,并支持以下操作:

  • empty():检测容器是否为空
  • size():返回容器中有效元素个数
  • front():返回容器中第一个元素的引用
  • push_back():在容器尾部插入元素
  • pop_back():删除容器尾部元素

5. 标准容器类 vector和deque 满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指 定容器类,则 默认使用vector

6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数 make_heap、push_heap和pop_heap来自动完成此操作。(即存在 建堆算法、向上向下调整算法)

简单来说:

        优先级队列默认使用 vector 作为其底层存储数据的容器,在vector上又使用了堆算法将 vector中元素构造成 堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意: 默认情况下priority_queue是大堆。

3.2 priority_queue 的 模拟实现

其实也就是之前学过的 堆

namespace my
{
	template<class T, class Container = vector<T>>
	class priority_queue
	{
	public:

		// 插入数据,向上调整
		void adjust_up(size_t child) {
			size_t parent = (child - 1) / 2;
			while (child > 0) {
				if (_con[parent] < _con[child]) {      // 需要改变符号,大小堆转变
					std::swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else break;
			}
		}

		void push(const T& x) {
			_con.push_back(x);
			adjust_up(size() - 1);
		}

		void adjust_down(size_t parent) {
			size_t Lchild = parent * 2 + 1;
			size_t Rchild = parent * 2 + 2;

			// 选 Lchild 有两种情况:1、_con[Rchild] < _con[Lchild] ;2、右孩子 Rchild 越界,不得不选 左孩子
			// 因此这里是 条件或
			size_t child = (Rchild >= size() || _con[Rchild] < _con[Lchild]) ? Lchild : Rchild;   // 需要改变符号,大小堆转变

			while (child < size() && _con[parent] <  _con[child]) {  // 需要改变符号,大小堆转变
				std::swap(_con[parent], _con[child]);
				parent = child;

				Lchild = parent * 2 + 1;
				Rchild = parent * 2 + 2;
				child = (Rchild >= size() || _con[Rchild] < _con[Lchild]) ? Lchild : Rchild;
			}
		}

		void pop() {
			// 首尾交换 头向下调整,尾直接 pop 
			std::swap(_con[0], _con[size() - 1]);
			_con.pop_back();
			adjust_down(0);
		}

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


		// 迭代器拷贝:这种也是一种 构造函数,写了这个就不会生成 默认的构造(如果有需求就强制生成)
		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last) {
			while (first != last) {
				_con.push_back(*first);  // 这里确实可以使用 push 建堆,但是 push 里面使用的 向上调整算法,效率较低,因此需要改用 向下调整算法
				++first;
			}

			// 这里的 int 不要写成 size_t ,要注意 size_t 的 小于 0 的问题,会直接重置成一个很大的数,导致无限循环,或则 i 很大时 vector 就越界访问
			for (int i = (size() - 1 - 1) / 2; i >= 0; --i) {
				adjust_down(i);
			}
		}

		priority_queue() = default; // 强制生成一个 构造函数

	private:
		Container _con;
	};
}

我们这里实现的  优先队列默认大堆,即降序排序,当我们想要升序排序,还得自己将 源码中的某部分的 小于号改成大于号,较为麻烦

则 C++ 提出了 仿函数的概念

4.仿函数

4.1 仿函数的概念与使用

        仿函数(也叫做 函数对象)   是个类,其内部重载了 operator() 函数,可以使一个类的对象 当作一个 函数去使用,就和 正常函数一样,因此叫做仿函数(模仿函数

正常的函数使用:

swap(1, 2);  // swap函数传两个参数过去

仿函数的使用:

// 定义一个仿函数类,内部重载 operator() 
class Func
{
public:
    void operator()(const int& x = 0, const int& y = 0) {
        cout << "调用 Func" << '\n';
    }
};

// 使用仿函数样例
Func f1; // 定义一个仿函数类对象
f1(1, 1);  // 像函数一样使用
// 等价于
f1.operator()(1, 1);

对象 f1 也传参数过去,就像在使用一个函数一样,这就是 仿函数的概念与使用

4.2 特性

  • 仿函数仅仅是一种使用的理念方法,operator() 函数 内部想要实现什么功能没有限制

  • operator() 传几个参数都是不限定的,是根据需求确定的,不像 operator++ 及其他一些重载函数

    bool operator()(int& x = 0, int& y = 0, int& z = 0, ....)
  • 可以将这个仿函数类写成 模板,以适配更多类型

小结:仿函数中 operator() 的特点:参数个数、返回值 和 函数功能 根据需求确定,不固定,很灵活

比如我实现一个 比较大小的功能,传过来两个参数

class Func
{
public:
	bool operator()(const int& x = 0, const int& y = 0) {
		return x < y;
	}
};

// 使用仿函数样例
Func f1;
cout << f1(10, 20) << '\n';  // 打印出 1,表示 10 < 20

如果我想要比较两个 string 的大小,仅仅使用上面的 (const int& x = 0, const int& y = 0) 是不可以的,要写成模板

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

// 使用仿函数样例
Func<string> f1;
cout << f1("111", "222") << '\n';  // 打印出 1,表示 "111" <  "222"

5.优先队列结合仿函数

5.1 介绍与使用

实现两个仿函数类:

// 大堆降序
template<class T>
class LessCmp
{
public:
	bool operator()(const T& x = 0, const T& y = 0) {
		return x < y;
	}
};


// 小堆升序
template<class T>
class GreaterCmp
{
public:
	bool operator()(const T& x = 0, const T& y = 0) {
		return x > y;
	}
};

给 优先队列类的 模板增加一个 参数:仿函数 class Compare = LessCmp< T >

template<class T, class Container = vector<T>, class Compare = LessCmp<T>>

在需要比较两个数的代码处,通过定义一个 仿函数类对象,调用不同的仿函数类中的 不同 的 operator() ,实现控制 此处是需要  a < b  还是 a > b 的选择问题

在向上向下调整算法函数中 使用

Compare cmp; // 先定义 Compare 类对象 cmp

// 讲 需要两数比较的部分 改成 仿函数
//if (_con[parent] < _con[child])   
if(cmp(_con[parent], _con[child]))
    
// 等同于
if(cmp.operator()(_con[parent], _con[child]))

5.2 优先队列的最终版

namespace my
{
	// 大堆降序
	template<class T>
	class LessCmp
	{
	public:
		bool operator()(const T& x = 0, const T& y = 0) {
			return x < y;
		}
	};
	// 小堆升序
	template<class T>
	class GreaterCmp
	{
	public:
		bool operator()(const T& x = 0, const T& y = 0) {
			return x > y;
		}
	};



	template<class T, class Container = vector<T>, class Compare = LessCmp<T>>
	class priority_queue
	{
	public:

		// 插入数据,向上调整
		void adjust_up(size_t child) {
			Compare cmp;



			size_t parent = (child - 1) / 2;
			while (child > 0) {
				//if (_con[parent] > _con[child]) {      // 需要改变符号,大小堆转变
				if(cmp(_con[parent], _con[child])){
					std::swap(_con[parent], _con[child]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else break;
			}
		}

		void push(const T& x) {
			_con.push_back(x);
			adjust_up(size() - 1);
		}

		void adjust_down(size_t parent) {
			Compare cmp;


			size_t Lchild = parent * 2 + 1;
			size_t Rchild = parent * 2 + 2;

			// 选 Lchild 有两种情况:1、_con[Lchild] < _con[Rchild] ;2、右孩子 Rchild 越界,不得不选 左孩子
			// 因此这里是 条件或
			size_t child = (Rchild >= size() || cmp(_con[Rchild], _con[Lchild])) ? Lchild : Rchild;   // 需要改变符号,大小堆转变

			while (child < size() && cmp(_con[parent], _con[child])) {  // 需要改变符号,大小堆转变
				std::swap(_con[parent], _con[child]);
				parent = child;

				Lchild = parent * 2 + 1;
				Rchild = parent * 2 + 2;
				child = (Rchild >= size() || cmp(_con[Rchild], _con[Lchild])) ? Lchild : Rchild;
			}
		}

		void pop() {
			// 首尾交换 头向下调整,尾直接 pop 
			std::swap(_con[0], _con[size() - 1]);
			_con.pop_back();
			adjust_down(0);
		}

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

		// 迭代器拷贝:这种也是一种 构造函数,写了这个就不会生成 默认的构造(如果有需求就强制生成)
		template<class InputIterator>
		priority_queue(InputIterator first, InputIterator last) {
			while (first != last) {
				_con.push_back(*first);  // 这里确实可以使用 push 建堆,但是 push 里面使用的 向上调整算法,效率较低,因此需要改用 向下调整算法
				++first;
			}
			// 这里的 int 不要写成 size_t ,要注意 size_t 的 小于 0 的问题,直接重置成一个很大的数,导致无限循环,或则 i 很大时 vector 就越界访问
			for (int i = (size() - 1 - 1) / 2; i >= 0; --i) {
				adjust_down(i);
			}
		}

		priority_queue() = default;

	private:
		Container _con;
	};
}

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值