C++_stack和queue和priority_queue(容器适配器)

目录

1.引言

2.容器适配器(重点)

2.1.什么是适配器

2.2.STL标准库中stack和queue的底层结构

2.3.deque的简单介绍

2.3.1.deque的原理介绍

2.3.2.deque的优缺点

2.4.为什么选择deque作为stack和queue的底层默认容器

3.stack的介绍和使用

3.1.stack介绍

3.2.stack的使用

3.3.stack模拟实现

拓展:按需实例化

4.queue的介绍和使用

4.1.queue的介绍

4.2.queue的使用

4.3.queue的模拟实现

5.priority_queue的介绍和使用

5.1.priority_queue的介绍

5.2.priority_queue的使用

5.3.priority_queue的模拟实现

push

top

pop

无仿函数版实现

仿函数(重点)

有仿函数版实现

6.结语


1.引言

这篇讲stack和queue的使用和模拟实现,在学这俩个容器前,需要掌握这些内容

C++_类和对象(上)-CSDN博客

C++_类和对象(中)-CSDN博客

C++_类和对象(下)-CSDN博客

C++_模板初阶-CSDN博客

C++_内存管理_c++内存管理要如何做-CSDN博客

虽然我们称stack,queue,priority_queue为容器,但更准确的说应该是容器适配器

具体什么是适配器也会在这篇文章中讲到,那么话不多说直接进入正文------------------>


2.容器适配器(重点)

2.1.什么是适配器

适配器是一种设计模式 (设计模式是一套被反复使用的,多数人知晓的,经过分类编目的,代码设计经验的总和),这种模式是将一个类的接口转换成客户希望的另外一个接口

就比如我们的充电器,又可以叫做电源适配器,因为可以对出来的电进行转换

2.2.STL标准库中stack和queue的底层结构

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器 ,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:

这个Container就是容器的意思,这个模板参数就是意味着用什么容器构建栈

2.3.deque的简单介绍

deque虽然叫双端队列,但其实deque跟queue没有关系,队列要求先进先出,deque不要求先进先出

我们可以把deque当成vector和list的缝合怪

2.3.1.deque的原理介绍

deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。(这也是为什么产生deque的原因,vector和list的优缺点过于明显,我这里汇总一下)

我们看图,可知deque用的是随机迭代器

我们从这就可以看出deque是个缝合怪,既支持下标访问,又支持头插头删

在讲deque底层结构前,我们先回顾下vector和list的结构,vector是一段连续的空间,list是一个一个节点作为空间,也就是很多小空间,然后通过指针连接在一起

deque为了解决vector频繁扩容的问题,采取的操作是开辟一块连续空间,如果连续空间满了,就开辟下一块同样大小的空间,不扩容,直接开辟新空间,也就是这样

这些空间连接deque是运用中控数组来实现,就是deque有一个中控数组,中控数组里面存的是指针,指向的就是对应空间,然后一开始的空间都会尽量往中控数组的中间位置靠,如下图

这样的话想要头插,尾插就会很方便,如下图

当然,这个也会要扩容,只是不会像vector一样拷贝的那么多,如果中控数组满了,就会扩容出一个更大的中控数组,然后把指针拷贝过去

那么假设一块空间buf的大小是N,那么如果想要获取第i个位置的数据怎么算呢,这个其实很简单,我们通过 i/N就能知道这个i是在第几个buf数组,然后再i%N就能知道这个i是在那个buf数组的第几个位置。那我们就可以通过中控数组先找到对应的buf数组,然后再进里面找对应位置的元素

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其"整体连续"以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

deque的迭代器里面有四个成员,first,last指向buf空间的开始和结束,cur指向的是想要访问的数据,node指向的是中控数组当前指向该buf空间的指针的位置,用图表示的话就是如下图

如果理解了迭代器,就理解了这个结构,从实现上来考量的话,重点要考量这个结构

这里我简单介绍下,deque主要的成员变量就俩个,start其实就是它的begin()返回的接待起,finish其实就是它的end()返回的迭代器

我们想要遍历依旧如此

cpp 复制代码
iterator it = begin();
while(it!=end())
{
    cout<<*it<<" ";
    ++it;
}

这边的it!=end()其实就是it.cur!=finish.cur。然后*it其实就是*(it.cur),然后++it就是这里迭代器最关键的地方,它内部先判断it.cur是否it.last,如果不相等,直接++it.cur就可以了,如果相等,那就说明这块空间遍历完了,此时就通过node找到对应的中控数组的位置,随后让it.node++,这个时候就找到了指向下一块空间的指针,然后更新cur,first和last即可

接下来我再来讲下deque的头插和尾插

尾插很简单,就看finish.cur是不是 等于finish.last,如果不等于,就相当于没满,直接插入随后cur++就可以了,如果满了,就开辟新空间,更新finish迭代器,随后插入即可

头插其实也简单,就看start.cur是不是等于start.first,因为是头插,所以开辟的空间其实是从后往前插入的,如果不等于相当于没满,直接插入随后cur--即可,如果满了就开辟新空间,更新start迭代器,随后插入即可(从后往前)

头删尾删也是同理,就不细讲了

讲了头插尾插后,其实上面我们所讲的访问第i个位置数据的方式也就要改进了,因为我们第一个buf的空间不能保证是满的,所以我们要先判断在不在第一个buf空间,如果不在,减去第一个buf的数据个数,再用/和%来找到对应位置

2.3.2.deque的优缺点

deque的头插尾插效率很高,甚至比vector和list更胜一筹,首先vector会有扩容方面问题,是这三个里效率最低的毋庸置疑,list和deque相比,deque每次开空间是一段,而list是一个一个开空间,前者的效率肯定是更高的,因为频繁申请空间效率肯定是比单次申请空间效率低的,deque虽然还多了个中控数组扩容,但是那个的代价非常低,所以不影响

inset和erase效率极低,甚至低于vector和list,因为deque和vector在增删的行为模式上很相似,但是他与vector相比还有判断是否跨buf

总结:

  1. deque头插尾插效率很高,更甚于vector和list
  2. 下标随机访问效率也不错,但是比不上vector
  3. inset和erase效率极低,甚至低于vector和list

致命缺陷不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多

2.4.为什么选择deque作为stack和queue的底层默认容器

我们了解deque后,可以知道,deque在头插头删尾插尾删方面的效率是极高的,而stack和queue都不需要访问中间的元素,只会对头部或者尾部的元素进行查找插入删除,这个时候,deque就非常适配了


3.stack的介绍和使用

3.1.stack介绍

stack就是栈,我们直接看图

stack的模板参数列表里就没有allocator了

3.2.stack的使用

我们接下来来看各个接口,可以发现其实和前面那些容器都差不多,基本上一眼懂

emplace效果和push是一样的,这里最主要的三个接口就是top(返回栈顶元素),push(入栈),pop(出栈)

要用需要加上#include <stack>头文件

这里直接用题来训练

155. 最小栈 - 力扣(LeetCode)

AC码

cpp 复制代码
#include <stack>
class MinStack {
public:
    MinStack() {
        
    }
    
    void push(int value) {
        st.push(value);
        if(stm.empty()||value<=stm.top())
            stm.push(value);
    }
    
    void pop() {
        if(st.top()==stm.top())
            stm.pop();
        st.pop();
    }
    
    int top() {
        return st.top();
    }
    
    int getMin() {
        return stm.top();
    }

    stack<int> st;
    stack<int> stm;
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack* obj = new MinStack();
 * obj->push(value);
 * obj->pop();
 * int param_3 = obj->top();
 * int param_4 = obj->getMin();
 */

栈的压入、弹出序列_牛客题霸_牛客网

AC码

cpp 复制代码
#include <stack>
class Solution {
  public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param pushV int整型vector
     * @param popV int整型vector
     * @return bool布尔型
     */
    bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
        stack<int> sti;
        stack<int> sto;
        for (int i = popV.size() - 1; i >= 0; i--)
            sto.push(popV[i]);
        for (size_t i = 0; i < pushV.size(); i++) {
            if (pushV[i] == sto.top()) {
                sto.pop();
                while (!sti.empty() && sti.top() == sto.top()) {
                    sti.pop();
                    sto.pop();
                }
            } else
                sti.push(pushV[i]);
        }
        while (!sti.empty() && sti.top() == sto.top()) {
            sti.pop();
            sto.pop();
        }
        return sti.empty();
    }
};

150. 逆波兰表达式求值 - 力扣(LeetCode)

AC码

cpp 复制代码
class Solution {
public:

    int evalRPN(vector<string>& tokens) {
        stack<int> st;
        int x = 0;
        for(int i = 0;i<tokens.size();i++)
        {
            int x1,y1;
            if(tokens[i]=="*")
            {
                x1 = st.top();
                st.pop();
                y1 = st.top();
                st.pop();
                st.push(y1*x1);
            }
            else if(tokens[i]=="/")
            {
                x1 = st.top();
                st.pop();
                y1 = st.top();
                st.pop();
                st.push(y1/x1);
            }
            else if(tokens[i]=="+")
            {
                x1 = st.top();
                st.pop();
                y1 = st.top();
                st.pop();
                st.push(y1+x1);
            }
            else if(tokens[i]=="-")
            {
                x1 = st.top();
                st.pop();
                y1 = st.top();
                st.pop();
                st.push(y1-x1);
            }
            else
            {
                x = 0;
                int j = 0;
                int xi=0;
                if(tokens[i][j]=='-')
                {
                    j++;
                    xi = 1;
                }
                for(j;j<tokens[i].size();j++)
                {
                    x = x*10+tokens[i][j]-'0';
                }
                if(xi)
                    x *= -1;
                st.push(x);
            }
        }
        return st.top();
    }

};

3.3.stack模拟实现

我们如果按照之前的思路来实现的话,大概率会实现成这样

cpp 复制代码
#pragma once
#include <iostream>

namespace qiu
{
	template<class T>
	class stack
	{
	private:
		T* _a;
		size_t _size;
		size_t _capacity;
	};
}

但我们学完适配器后其实完全不用这么写,因为stack这个东西完全可以用vector来进行封装转换,因为栈主要支持的就三个东西,一个入栈,一个出栈,一个查询栈顶元素,那入栈不就对应vector的尾插,出栈不就对应vector的尾删,查询栈顶元素不就是查询vector内部最后一个元素

那我们就可以写成如下样子

cpp 复制代码
#pragma once
#include <iostream>

namespace qiu
{
	template<class T>
	class stack
	{
	private:
		vector<T> _con;
	};
}

但是这个时候,其实我们相当于又把栈写死了,因为只能通过vector容器来构造栈,那么我们可以用一个模板参数来代表容器,这种方式也很贴近底层的实现方式,也就是如下代码

cpp 复制代码
#pragma once
#include <iostream>

namespace qiu
{
	template<class T,class Container>
	class stack
	{
	private:
		Container _con;
	};
}

这样,我们就可以通过传入不同的容器类型来形成不同的栈。

这就是container适配转换成stack。当然,我们也不用写构造函数了,因为Container肯定是个自定义类型,编译器默认生成的构造函数会调用自定义类型的构造,所以我们不用管

接下来,我们来实现一下核心接口

cpp 复制代码
#pragma once
#include <iostream>

namespace qiu
{
	template<class T,class Container>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		T& top()
		{
			return _con.back();
		}
		const T& top() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

至此,stack模拟实现就完成了,是不是超级简单

我们可以用一下这个栈

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "stack.h"
#include <vector>
#include <list>
using namespace qiu;

int main()
{
	stack<int, std::vector<int>> st1;
	stack<std::string, std::list<std::string>> st2;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	while (!st1.empty())
	{
		std::cout << st1.top() << " ";
		st1.pop();
	}
	std::cout << std::endl;

	st2.push("xixi");
	st2.push("haha");
	st2.push("aeiou");
	while (!st2.empty())
	{
		std::cout << st2.top() << " ";
		st2.pop();
	}
	return 0;
}

我们之前一般使用stack的时候,大多是这么使用的,stack<int> st;想要实现这种效果只要在第二个模板参数加个缺省参数即可,如下

cpp 复制代码
	template<class T,class Container = vector<T>>

STL底层赋的缺省参数是deque<T>

拓展:按需实例化

这里再拓展一个点,按需实例化,这个在前面章节也讲过,只是那个时候没有合适的样例,这里可以细致讲讲

我们可以先看这个代码

stack.h

cpp 复制代码
#pragma once
#include <iostream>

namespace qiu
{
	template<class T,class Container = vector<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		T& top()
		{
			return _con.back();
		}
		const T& top() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "stack.h"
#include <vector>
#include <list>
using namespace qiu;
using namespace std;

int main()
{
	stack<int> st1;
	st1.push(1);
	st1.push(2);
	st1.push(3);
	while (!st1.empty())
	{
		cout << st1.top() << " ";
		st1.pop();
	}
	cout << endl;
	return 0;
}

运行我们可以发现是没有问题的,但是我们.h头文件里并没有包vector,为什么不报错呢,因为我们实现的stack是模板,编译器检查是很宽泛的,这个模板实例化的时候是在main函数内部了,.h头文件经过编译链接已经合并了,这个时候向上找使能找到vector头文件的

当然,如果找不到就会报错了,就比如这种代码

cpp 复制代码
#pragma once
#include <iostream>

namespace qiu
{
	template<class T,class Container = vector<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_back();
		}
		T& top()
		{
			return _con.back();
		}
		const T& top() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};

	void func()
	{
		int x = 0;
		cout << x << endl;
	}
}

如果我们在 .cpp中头文件是这个样子的,就会编译报错,因为就算编译链接过去,也是替换在了"stack.h"那个位置,往上找找不到using namespace std;

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include "stack.h"
#include <vector>
#include <list>
using namespace qiu;
using namespace std;

但如果这个头文件在std之下,就不会报错了,如下

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <vector>
#include <list>
using namespace std;
#include "stack.h"
using namespace qiu;

接下来就是重点------按需实例化

我们先看代码

cpp 复制代码
#pragma once
#include <iostream>

namespace qiu
{
	template<class T,class Container = vector<T>>
	class stack
	{
	public:
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			_con.pop_front();//这里改了,vector没有pop_front接口
		}
		T& top()
		{
			return _con.back();
		}
		const T& top() const
		{
			return _con.back();
		}
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
	private:
		Container _con;
	};
}

如果我们不调用pop,就不会编译错误

但vector是没有pop_front接口的,这个时候我们调用pop就会发现编译报错了

这正是因为按需实例化的原因,没调用的函数,编译器不会实例化,只会检查大体有没有问题

所以有的时候,模板实现一直没问题,突然有一次调用出问题了,很可能就是这个接口之前一直没有使用过,然后这个接口内部有点问题


4.queue的介绍和使用

4.1.queue的介绍

和stack一样,如下图

4.2.queue的使用

还是一样,核心接口就这么些

要用需要加上#include <queue>头文件

这里用道题练练应用

102. 二叉树的层序遍历 - 力扣(LeetCode)

AC码

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> ans;
        if(root==nullptr)
            return ans;
        int _size = 1;
        queue<TreeNode*> q;
        q.push(root);

        while(!q.empty())
        {
            vector<int> v;
            while(_size--)
            {
                TreeNode* p = q.front();
                if(p->left!=nullptr)
                    q.push(p->left);
                if(p->right!=nullptr)
                    q.push(p->right);
                q.pop();
                v.push_back(p->val);
            }
            _size = q.size();
            ans.push_back(v);
        }
        return ans;
    }
};

4.3.queue的模拟实现

这个其实就和上面的stack一样,实现起来很简单,如下

cpp 复制代码
#pragma once
#include <iostream>
#include <cassert>
#include <deque>

namespace qiu
{
	template<class T,class Container = deque<T>>
	class queue
	{
	public:
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			assert(size());
			_con.pop_front();
		}
		T& front()
		{
			return _con.front();
		}
		const T& front() const
		{
			return _con.front();
		}
		T& back()
		{
			return _con.back();
		}
		const T& back() const
		{
			return _con.back();
		}

	private:
		Container _con;
	};
}

需要注意的是,队列而言就不能用vector了,因为队列是一端进一端出,如果用vector,出队列的时间复杂度是O(N)了

我们如果用vector来作为Container,一开始不会报错,直到我们调用pop才会报错,因为vector是没有pop_front这个操作的,这也体现了 前面我们所讲的按需实例化问题


5.priority_queue的介绍和使用

5.1.priority_queue的介绍

priority_queue我们平时常称优先队列,这个容器适配器相比于前面俩个而言会复杂一点

它的接口和stack的接口非常的像

push就是正常压入进去,这里最需要注意的是pop和top,它要选择优先级高的进行pop和top(priority_queue默认情况下是大的优先级高),如果想控制小的优先级高就得用到一个叫仿函数的东西。

priority_queue的底层就是我们之前的堆,堆的底层就是数组(用来表示完全二叉树),所以 priority_queue的默认适配容器是vector

可以用deque吗,可以,但是相比vector效率会低很多,因为堆操作会不断对中间部分的数据进行操作

5.2.priority_queue的使用

注意:用priority_queue只需要包queue头文件就行,priority_queue是包含在queue头文件里的

具体使用其实就和前面也一样,只是top和pop需要注意,不是删除头元素和输出头元素,是找优先级最高的元素操作

实例如下

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <queue>
using namespace std;

int main()
{
	priority_queue<int> pq;
	pq.push(2);
	pq.push(1);
	pq.push(3);
	pq.push(5);
	pq.push(4);
	while (pq.size())
	{
		cout << pq.top()<<" ";
		pq.pop();
	}
	return 0;
}

如果我们想让他变成小堆,就如下

cpp 复制代码
	priority_queue<int, vector<int>, greater<int>> pq;

写个题练练手

215. 数组中的第K个最大元素 - 力扣(LeetCode)

AC码

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int,vector<int>,greater<int>> pq;
        for(auto x:nums)
        {
            if(pq.size()==k&&x>pq.top())
            {
                pq.pop();
                pq.push(x);
            }
            else if(pq.size()<k)
                pq.push(x);
        }
        return pq.top();
    }
};

5.3.priority_queue的模拟实现

我们这里先不加仿函数,把基本结构写清楚

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
using namespace std;

namespace qiu
{
	template<class T,class Container = vector<T>>
	class  priority_queue
	{
	public:
		void push(const T& x)
		{

		}
		void pop()
		{

		}
		T& top()
		{

		}
		const T& top() const
		{

		}
		bool empty() const
		{
			return _con.empty();
		}
		size_t size() const
		{
			return _con.size();
		}

	private:
		Container _con;
	};

}

push

因为底层是堆,所以按照堆的数据结构,我们先对vector尾插,然后进行向上调整法成堆结构(因为priority_queue默认大的优先级高,所以我们现在就按如果大就向上调整来实现)

cpp 复制代码
		void push(const T& x)
		{
			_con.push_back(x);
			size_t i = _con.size() - 1;
			while (i)
			{
				if (_con[i] > _con[(i - 1) / 2])
				{
					swap(_con[i], _con[(i - 1) / 2]);
					i = (i - 1) / 2;
				}
				else
					break;
			}
		}

top

因为底层是堆,所以堆顶元素就是我们想要的元素,如下

cpp 复制代码
		T& top()
		{
			return _con[0];
		}
		const T& top() const
		{
			return _con[0];
		}

注意:如果容器为空 ,调用top访问_con0就属于未定义行为,因为STL库中的top也没有对这种情况处理,所以我们也不处理

pop

这个就是把堆顶元素和最后一个元素交换,然后对vector------pop_back,随后再对堆顶元素向下调整,重新调整为堆结构

cpp 复制代码
		void pop()
		{
			std::swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			size_t i = 0;
			size_t lin = i * 2 + 1;
			while (lin < _con.size())
			{
				if (lin + 1 < _con.size() && _con[lin] < _con[lin + 1])
				{
					++lin;
				}
				if (_con[i] < _con[lin])
				{
					std::swap(_con[i], _con[lin]);
					i = lin;
					lin = lin * 2 + 1;
				}
				else
					break;
			}

		}

无仿函数版实现

cpp 复制代码
#pragma once
#include <iostream>
#include <cassert>
#include <deque>
#include "priority_queue.h"

namespace qiu
{
	template<class T,class Container = deque<T>>
	class queue
	{
	public:
		size_t size() const
		{
			return _con.size();
		}
		bool empty() const
		{
			return _con.empty();
		}
		void push(const T& x)
		{
			_con.push_back(x);
		}
		void pop()
		{
			assert(size());
			_con.erase(_con.begin());
		}
		T& front()
		{
			return _con.front();
		}
		const T& front() const
		{
			return _con.front();
		}
		T& back()
		{
			return _con.back();
		}
		const T& back() const
		{
			return _con.back();
		}

	private:
		Container _con;
	};
}

我们测试一下

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "queue.h"
using namespace std;

int  main()
{
	qiu::priority_queue<int> pq;
	pq.push(3);
	pq.push(1);
	pq.push(2);
	pq.push(5);
	pq.push(4);
	while (pq.size())
	{
		cout << pq.top() << " ";
		pq.pop();
	}


	return 0;
}

但是上面的这种只实现了单一的排序,如果我想要小根堆,上述代码就无法实现了,这个时候,为了防止代码的冗余,写俩份基本没什么区别的代码,C++引入了一个叫仿函数的概念,我们先来看仿函数是什么

仿函数(重点)

首先,仿函数是一个类,这个类里面有()运算符重载(函数调用运算符重载)。就比如说,我们想要实现整型的比大小,实例如下,这个operator()重载的()是函数调用的时候参数列表的()。我们先前在前面讲过,类内如果没有成员变量的时候,类的大小为1,仿函数就普遍是这种情况,因为一般仿函数对应的类的内部基本就是只有一个operator()的重载

cpp 复制代码
	class Less
	{
    public:
		bool operator()(int x, int y)
		{
			return x < y;
		}
	};

我们也可以把仿函数改造成类模板

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

那么,仿函数怎么用呢,我们举个实例来看

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "queue.h"
using namespace std;

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

int  main()
{
	Less<int> ls;
	cout << ls(1, 2) << endl;

	return 0;
}

如果我们不看Less<int> ls,只看ls(1,2),我们就会以为ls是个函数,但其实ls是个对象,这也是我们为什么把这个取名为仿函数的原因

那我们除了可以写一个Less的还能写个Greater的

cpp 复制代码
template<class T>
class Greater
{
public:
	bool operator()(const T& x, const T& y)
	{
		return x > y;
	}
};

总结仿函数本质是一个类,这个类重载了operator(),他的对象可以像函数一样使用,所以仿函数的对象我们可以称为函数对象,但本质的底层其实还是调用了 operator()

目前来看,这个东西没什么用,我这里举个例子来带你们初步感受下这个的妙用

假设我要做一个冒泡排序,我们可以这么实现

cpp 复制代码
template<class T>
void Sort(int* a, size_t n, T cmp)
{
	for (int j = 0; j < n; j++)
	{
		int flag = 0;
		for (int i = 1; i < n - j; ++i)
		{
			if (cmp(a[i - 1], a[i]))
			{
				swap(a[i - 1], a[i]);
				flag = 1;
			}
		}
		if (!flag)
			break;
	}
}

我们可以把判断交给仿函数,这样我们就可以通过一个函数模板,实现出不同的排序效果,如下

那么,仿函数懂了后,接下来,我对priority_queue优化成有仿函数版的实现

有仿函数版实现

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>

namespace qiu
{

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

		void push(const T& x)
		{
			_con.push_back(x);
			Up();
		}
		void pop()
		{
			std::swap(_con[0], _con[_con.size() - 1]);
			_con.pop_back();
			Down();

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

	private:
		Container _con;
		Compare cmp;
		
		void Up()
		{
			size_t i = _con.size() - 1;
			while (i)
			{
				if (cmp(_con[(i - 1) / 2], _con[i]))
				{
					std::swap(_con[i], _con[(i - 1) / 2]);
					i = (i - 1) / 2;
				}
				else
					break;
			}
		}
		void Down()
		{
			size_t i = 0;
			size_t lin = i * 2 + 1;
			while (lin < _con.size())
			{
				if (lin + 1 < _con.size() && cmp(_con[lin], _con[lin + 1]))
				{
					++lin;
				}
				if (cmp(_con[i], _con[lin]))
				{
					std::swap(_con[i], _con[lin]);
					i = lin;
					lin = lin * 2 + 1;
				}
				else
					break;
			}
		}
	};
}

我们测试一下

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include "queue.h"
using namespace std;

int  main()
{
	qiu::priority_queue<int, vector<int>, greater<int>> pq;
	pq.push(1);
	pq.push(3);
	pq.push(2);
	pq.push(5);
	pq.push(7);
	pq.push(6);
	while (pq.size())
	{
		cout << pq.top() << " ";
		pq.pop();
	}
	cout << endl;

	qiu::priority_queue<int, vector<int>> pq1;
	pq1.push(1);
	pq1.push(3);
	pq1.push(2);
	pq1.push(5);
	pq1.push(7);
	pq1.push(6);
	while (pq1.size())
	{
		cout << pq1.top() << " ";
		pq1.pop();
	}

	

	return 0;
}

没有问题


6.结语

那么,C++STL部分stack,queue和priority_queue的内容就全部讲解完毕啦,希望以上内容对你有所帮助,感谢观看,若觉得写的还可以,可以分享给朋友一起来看哦,毕竟一起进步更有动力嘛,当然能关注一下就更好啦。

相关推荐
小保CPP1 小时前
OpenCV C++基于极值区域滤波算法的场景文本检测(OCR)
c++·人工智能·opencv·算法·计算机视觉·ocr
::呵呵哒::1 小时前
java中SseEmitter
java·开发语言
星恒随风1 小时前
C++ 多态底层原理:静态绑定、动态绑定、虚函数表与工程实践
开发语言·c++·笔记·学习
wuyk5551 小时前
68.嵌入式 C 语言避坑指南:volatile 关键字 —— 单片机中断、寄存器访问的核心技巧
c语言·开发语言·stm32·单片机·嵌入式硬件
qz_Serene1 小时前
C语言:编译和链接
c语言·开发语言
会编程的土豆2 小时前
功能详解 01 · 用户注册:从表单到数据库一行
开发语言·数据库·后端·mysql·golang
白色冰激凌2 小时前
[SECS/GEM研究] (四)HSMS 用TCP取代串口
网络·c++·网络协议·secs/gem
Ivanqhz2 小时前
Rust collect() 浅析
开发语言·后端·rust
Wang's Blog2 小时前
Go-Zero项目开发33: IM实时通信前后端对接与微服务基础设施实践
开发语言·微服务·golang·go-zero