STL之stack、queue、priority_queue模拟实现

news2024/11/15 19:43:26

容器适配器

  容器适配器,简言之是可以用不同容器来快速实现自己的工具。像stack、queue、priority_queue都是容器适配器。

stack模拟实现

  • 主要接口定义如下:
namespace lz
{
	template<class T, class Container = deque<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();
		}
		//交换两个栈中的数据
		void swap(stack<T, Container>& st)
		{
			_con.swap(st._con);
		}
	private:
		Container _con;
	};
}
分析:
&emsp;&emsp;1. 因为是容器适配器,所以我们用容器模板参数即可。如果使用库中的原生stack,底层容器是deque(queue也一样)。

+ 构造函数和析构函数:
&emsp;&emsp;因为使用容器,所以内部不需要自己申请任何空间,扩容等一系列问题都不需要考虑。

+ push:
&emsp;&emsp;栈push需要给容器底部插,所以用push_back()。
```cpp
// push:const T& x:用_con做push,也就是deque,const保护,&防止改变
void push(const T& x)
{
	_con.push_back(x);	// _con.push_back();
}
  • pop:
    涉及出栈,要删最后一个,所以pop_back()。
// _con的pop()
void pop()
{
	_con.pop_back();
}
  • top:
// top:T& 是因为我们调用的deque使用了back(),back()可以改变,所以deque的back()返回值本身是个引用,所以这里可以使用引用,且我们要的就是得到top()也能做修改。
T& top()
{
	return _con.back();
}
  • size:
    _con的size。
size_t size() const
{
	return _con.size();
}
  • empty:
bool empty() const
{
	return _con.empty();
}

stack完整代码

//#pragma once
//#include<iostream>
//#include<deque>
//using namespace std;
//
//namespace lz
//{
//	template<class T, class Container = deque<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();
//		}
//		//交换两个栈中的数据
//		void swap(stack<T, Container>& st)
//		{
//			_con.swap(st._con);
//		}
//	private:
//		Container _con;
//	};
//}
 // cpp
//#include"l3stack.h"
//
//int main()
//{
//    lz::stack<int> st;
//    st.push(1);
//    st.push(2);
//    st.push(3);
//    st.push(4);
//    st.push(5);
//    cout << "st.size() = " << st.size() << endl;
//    while (!st.empty())
//    {
//        cout << st.top() << " ";
//        st.pop();
//    }
//    
//    return 0;
//}

queue模拟实现

  • queue的主要接口:
template<class T, class Con = deque<T>>

  class queue

  {

  public:

    queue();

    void push(const T& x);

    void pop();

    T& back();

    const T& back()const;

    T& front();

    const T& front()const;

    size_t size()const;

    bool empty()const;

  private:

    Con _c;

  };

};
  • queue和stack的区别
      区别在queue有front()、back()和queue的pop()必须出头。
  • pop()
void pop()
{
	_con.pop_front();
}
  • front()
T& front()
{
	return _con.front();
}
  • back()
T& back()
{
	return _con.back();
}

请添加图片描述

queue完整代码

//#pragma once
//#include<iostream>
//#include<deque>
//#include<list>
//using namespace std;
//
//namespace lz
//{
//	template<class T, class Container = deque<T>>
//	class queue
//	{
//	public:
//		//元素入栈
//		void push(const T& x)
//		{
//			_con.push_back(x);
//		}
//		//元素出栈
//		void pop()
//		{
//			_con.pop_front();
//		}
//
//		T& back()
//		{
//			return _con.back();
//		}
//
//		T& front()
//		{
//			return _con.front();
//		}
//
//		//获取栈顶元素
//		T& top()
//		{
//			return _con.back();
//		}
//		
//		//获取栈中有效元素个数
//		size_t size() const
//		{
//			return _con.size();
//		}
//		//判断栈是否为空
//		bool empty() const
//		{
//			return _con.empty();
//		}
//		//交换两个栈中的数据
//		void swap(queue<T, Container>& q)
//		{
//			_con.swap(q._con);
//		}
//	private:
//		Container _con;
//	};
//}

// queue.cpp
//#include"l3queue.h"
//
//int main()
//{
//    lz::queue<int, list<int>>q;
//    q.push(1);
//    q.push(2);
//    q.push(3);
//    q.push(4);
//    cout << "queue.size = " << q.size() << endl;
//    while (!q.empty())
//    {
//        cout << q.front() << endl;
//        q.pop();
//    }
//
//    return 0;
//}

priority_queue模拟实现

  • 关于原生priority_queue:
      STL原生底层实现使用:默认是大根堆,仿函数是:less<>。且底层数据结构是:vector,因为拿数组建堆方便。
    用法如下:
priority_queue<int, vector<int>, less<int>> q1;
  • 全部代码:
#pragma once
#include<iostream>
#include<vector>

using namespace std;

// 默认是大根堆,就写大根堆
namespace lz
{
	// 仿函数:
	template<class T>
	class less
	{
	public:
		bool operator()(const T& l, const T& r)const
		{
			return l < r;
		}
	};

	template<class T>
	class greater
	{
	public:
		bool operator()(const T& l, const T& r)const
		{
			return l > r;
		}
	};

	// compare:进行比较的仿函数
	template<class T, class Container = vector<T>, class Compare = std::less<T>>
	class priority_queue
	{
	public:
		void adjust_up(size_t child)
		{
			Compare com;
			int parent = (child-1)/2;
			// 孩子> 0也可
			while (parent >= 0)
			{
				if (com(_con[parent], _con[child]))	// 大根堆,	调 < :孩子>父亲,就换所以父亲在前面
				{
					std::swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
					break;
			}
		}

		// 向下调整
		void adjust_down(size_t parent)
		{
			Compare com;
			// 每次都用右孩子
			size_t child = parent * 2 + 1;
			// 允许孩子最多是 size-1 合理
			while (child < _con.size())
			{
				// 右孩子存在且更大 则child 变右孩
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))	// 大根  比如 less <  ,小的放前面 :
					child++;
				if (com(_con[parent], _con[child]))
				{
					std::swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
					break;
			}
		}

		priority_queue()
		{
			// 函数体不写就可以,
			// 会自动调用容器构造,
			// 但是不写不写,下面有迭代器构造函数
			// 编译器就不会自己生成pq的默认构造函数。
		}

		template<class InputIteartor>
		priority_queue(InputIteartor first, InputIteartor last)
		{
			// 迭代器模板中,会自动往后
			while (first != last)
			{
				_con.push_back(*first);
				++first;
			}
			// 数组已经好了,但是不符合堆, 向下调整规范堆:从倒数第一个父节点开始到堆顶做向下调整 
			for (int i = (_con.size()-1-1)/2; i >= 0; --i)
			{
				adjust_down(i);
			}
		}

		size_t empty()
		{
			return _con.size() == 0;
		}

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

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

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

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

	private:
		Container _con;
	};
}

代码分析

    1. 构造函数
        实现迭代器构造函数和无参的默认构造函数,因为我们写了迭代器构造函数,编译器发现你有自己写构造函数,就不会生成无参构造函数了,所以需要自己再写无参构造函数。
    1. 仿函数less、greater
  1. less用在大顶堆,使用<。
  2. greater用在小顶堆,使用>。
  3. 发现中文名字和大小顶堆的英文相反,而英文和符号又相符。

  仿函数都使用模板参数,重载()。重载()是规定,这个仿函数起比较作用,返回值即可。

    1. 向上向下调整算法
void adjust_up(size_t child)
{
	Compare com;
	int parent = (child-1)/2;
	// 孩子> 0也可
	while (parent >= 0)
	{
		if (com(_con[parent], _con[child]))	// 大根堆,	调 < :孩子>父亲,就换所以父亲在前面
		{
			std::swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

		// 向下调整
void adjust_down(size_t parent)
{
	Compare com;
	// 每次都用右孩子
	size_t child = parent * 2 + 1;
	// 允许孩子最多是 size-1 合理
	while (child < _con.size())
	{
		// 右孩子存在且更大 则child 变右孩
		if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))	// 大根  比如 less <  ,小的放前面 :
			child++;
		if (com(_con[parent], _con[child]))
		{
			std::swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;
	}
}

  向上调整是给当前孩子位置,不断求爹,向上调。因为开头从1个节点开始,所以后续用向上调整都能使得堆条件满足。
  向下调整是给当前爹位置,不断求儿子,向下调。

    1. 重要函数:
  • push:给堆底尾插,然后向上调整。
void push(const T& x)
{
	_con.push_back(x);
	adjust_up(_con.size()-1);
	//adjust_down(_)
}

  • pop:我们要做弹出堆顶,交换堆顶和最后一个元素,利用底层容器deque的pop_back(),相当于做了删尾,为什么用最后一个和堆顶交换,因为最后一个一定是比较小(大顶堆时,小顶堆相反),符合在叶子节点的条件,所以我们再做向下调整,
void pop()
{
	std::swap(_con[0], _con[_con.size() - 1]);
	_con.pop_back();
	_con,adjust_down(0);
}

全部代码

#pragma once
#include<iostream>
#include<vector>

using namespace std;

// 默认是大根堆,就写大根堆
namespace lz
{
	// 仿函数:
	template<class T>
	class less
	{
	public:
		bool operator()(const T& l, const T& r)const
		{
			return l < r;
		}
	};

	template<class T>
	class greater
	{
	public:
		bool operator()(const T& l, const T& r)const
		{
			return l > r;
		}
	};

	// compare:进行比较的仿函数
	template<class T, class Container = vector<T>, class Compare = std::less<T>>
	class priority_queue
	{
	public:
		void adjust_up(size_t child)
		{
			Compare com;
			int parent = (child-1)/2;
			// 孩子> 0也可
			while (parent >= 0)
			{
				if (com(_con[parent], _con[child]))	// 大根堆,	调 < :孩子>父亲,就换所以父亲在前面
				{
					std::swap(_con[child], _con[parent]);
					child = parent;
					parent = (child - 1) / 2;
				}
				else
					break;
			}
		}

		// 向下调整
		void adjust_down(size_t parent)
		{
			Compare com;
			// 每次都用右孩子
			size_t child = parent * 2 + 1;
			// 允许孩子最多是 size-1 合理
			while (child < _con.size())
			{
				// 右孩子存在且更大 则child 变右孩
				if (child + 1 < _con.size() && com(_con[child], _con[child + 1]))	// 大根  比如 less <  ,小的放前面 :
					child++;
				if (com(_con[parent], _con[child]))
				{
					std::swap(_con[child], _con[parent]);
					parent = child;
					child = parent * 2 + 1;
				}
				else
					break;
			}
		}

		priority_queue()
		{
			// 函数体不写就可以,
			// 会自动调用容器构造,
			// 但是不写不写,下面有迭代器构造函数
			// 编译器就不会自己生成pq的默认构造函数。
		}

		template<class InputIteartor>
		priority_queue(InputIteartor first, InputIteartor last)
		{
			// 迭代器模板中,会自动往后
			while (first != last)
			{
				_con.push_back(*first);
				++first;
			}
			// 数组已经好了,但是不符合堆, 向下调整规范堆:从倒数第一个父节点开始到堆顶做向下调整 
			for (int i = (_con.size()-1-1)/2; i >= 0; --i)
			{
				adjust_down(i);
			}
		}

		size_t empty()
		{
			return _con.size() == 0;
		}

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

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

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

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

	private:
		Container _con;
	};
}

==main.cpp==
#include"l4heap.h"
#include<queue>

void testpq()
{
    lz ::priority_queue<int, vector<int>, greater<int>> pq;
    //priority_queue<int> pq;
    pq.push(3);
    pq.push(1);
    pq.push(2);
    pq.push(5);
    pq.push(0);
    pq.push(1);
    while (!pq.empty())
    {
        cout << pq.top() << " ";
        pq.pop();
    }
    
    cout << endl << "排序 a" << endl;
    int a[] = { 3, 2 , 1 , 4 , 56, 7 , 8, 4, 0 ,5 };
    lz::priority_queue<int> pq2(a, a+ sizeof(a)/sizeof(int));
    while (!pq2.empty())
    {
        cout << pq2.top() <<  " ";
        pq2.pop();
    }


}


int main()
{
    testpq();
    return 0;
}

练习题

寻找topK
优先队列只能通过把vector遍历一次全放进来才行。
这个题快排最好。
记忆一下STL规律:
队列类的如priority_queue、queue都只有pop(),而没有pop_back(),而其它的如vector、list、deque(它是双端,所以有尾删很正常)都是pop_back(),可以想象,一个vector数组,删尾巴容易删,删顶不好删,所以也不留这个接口。
请添加图片描述

  • 选择题:
    下列代码的运行结果是( )
    int main()
    {
    priority_queue a;
    priority_queue<int, vector, greater > c;
    priority_queue b;
    for (int i = 0; i < 5; i++)
    {
    a.push(i);
    c.push(i);
    }
    while (!a.empty())
    {
    cout << a.top() << ’ ';
    a.pop();
    }
    cout << endl;
    while (!c.empty())
    {
    cout << c.top() << ’ ';
    c.pop();
    }
    cout << endl;
    b.push(“abc”);
    b.push(“abcd”);
    b.push(“cbd”);
    while (!b.empty())
    {
    cout << b.top() << ’ ';
    b.pop();
    }
    cout << endl;
    return 0;
    }
    A.4 3 2 1 0 0 1 2 3 4 cbd abcd abc
    B.0 1 2 3 4 0 1 2 3 4 cbd abcd abc
    C.4 3 2 1 0 4 3 2 1 0 abc abcd cbd
    D.0 1 2 3 4 4 3 2 1 0 cbd abcd abc
    其中,字典序:c>a,所以选ACD中一个,剩下都是大小堆判断,好说。

  • 仿函数比起一般函数具有很多优点,以下描述错误的是( )
    A.在同一时间里,由某个仿函数所代表的单一函数,可能有不同的状态
    B.仿函数即使定义相同,也可能有不同的类型
    C.仿函数通常比一般函数速度快
    D.仿函数使程序代码变简单

   仿函数是一种模板函数,因为使用了模板,要做匹配、实例化等,速度比一般函数满。此外,仿函数定义相同, 也可以有不同类型,因为实例化后可以存不同类型值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/108238.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【论文阅读总结】inception v2_v3总结

重新思考计算机视觉的Inception架构1.摘要2.简介2.1 以往模型问题2.2 问题缓解2.3 问题解决出现的问题2.4 有效的一般原则和优化思想3.一般设计原则3.1 设计原则1【避免代表性瓶颈(不能过度降维)】3.2 设计原则2【特征越多&#xff0c;收敛越快】3.3 设计原则3【卷积之前使用1*…

【java】opencv + Tesseract(tess4j) 实现图片处理验证码识别

1.opencv for java 环境搭建和测试 到OpenCV官网下载你需要的版本&#xff0c;运行安装&#xff0c;记住安装目录。打开上一步安装的位置&#xff0c;依次打开如下图位置&#xff0c;复制opencv-{version}.jar、x64包下对应的dll到项目里&#xff0c;放在同级 在maven里添加o…

JVS低代码首页功能介绍

首页介绍 首页操作演示 系统logo 系统logo是每个系统的名称标识&#xff0c;点击系统logo可以返回到首页&#xff0c;这里的系统logo是支持配置化的。 应用快捷导航 应用快捷导航是将登录用户有权限使用的应用展示出来&#xff0c;鼠标点击后&#xff0c;系统展示可见的应用于…

k8s-Pod的生命周期和调度

目录 主要运行周期 1 Pod创建和终止 2 初始化容器 3 钩子函数 4 容器探测 5 重启策略 Pod调度 1 定向调度 2 亲和性调度 3 污点和容忍 主要运行周期 我们一般将pod对象从创建至终的这段时间范围称为pod的生命周期&#xff0c;它主要包含下面的过程&#xff1a; pod创…

vscode python远程开发最佳实践

文章目录环境插件踩坑python类型提示不起作用配置PYTHONPATH前言 最近因为remote-ssh从pycharm转到vscode开发, 再删掉pycharm强制使用vscode摸索了一周熟练之后发现vscode其实使用起来也很爽&#xff0c;一些踩坑和最佳实践方案汇总 环境 插件 remote-sshpythonpylance(微软…

Compose 动画艺术探索之 Easing

本篇文章是此专栏的第六篇文章&#xff0c;前几篇文章大概将 Compose 中的动画都简单过了一遍&#xff0c;如果想阅读前几篇文章的话可以点击下方链接&#xff1a; Compose 动画艺术探索之瞅下 Compose 的动画Compose 动画艺术探索之可见性动画Compose 动画艺术探索之属性动画…

Mobtech 秒验应用介绍

一、传统APP手机注册登录验证的弊端 1、 注册过程输入的信息过多&#xff0c;耗费时间长。用户体验感较差。 2、 传统手机绑定需要通过验证码验证手机真实性&#xff0c;容易被批量注册。 3、 如果手机APP多&#xff0c;每个APP都注册&#xff0c;使用的用户名密码多&#x…

高薪资的IT行业,我们该不该转行

今年互联网各大厂秋招基本结束&#xff0c;校招薪资已经出炉了。可以从上图中看到&#xff0c;今年薪资仍然存在倒挂&#xff08;新员工工资高过老员工&#xff09;现象。各大厂人均 30w 的薪资在其它专业是难以想象的。大家无需置疑上述薪资的可靠性。作为今年的校招生&#x…

视频剪辑教程,批量将视频裁切为1:1比例的尺寸

视频太多&#xff0c;如何批量剪辑&#xff0c;比如将视频裁切为1:1的比例呢&#xff1f;那么今天小编给大家带来一个超简单的方法&#xff0c;可以同时将多段16:9的视频裁切为1:1的视频。 所需工具 多段16:9&#xff08;即1280*720&#xff09;的视频素材 操作步骤 第一步&…

C++那些事之高效率开发

1.神器 目前开发C/C用的比较多的当属Vim、VS code、CLion。 Vim配上插件编写C/C效率高的不少。 VSCode配上自定义配置及快捷键、vim插件效率跟vim旗鼓相当。 CLion因其独特的CMakeLists.txt管理方式及强大的代码补全等功能&#xff0c;编写本地代码绝对好于前两者。 但是对…

获B轮融资 官栈如何打破薛定谔式“中式滋补”

日前&#xff0c;滋补头部品牌官栈宣布完成B轮融资&#xff0c;这是其继去年9月完成Pre-B轮融资后&#xff0c;再度获得资本青睐。 近年来&#xff0c;乘国潮东风&#xff0c;中式滋补在沉寂多年后火热翻红&#xff0c;以官栈为代表的新品牌快速崛起&#xff0c;而老字号也紧跟…

非零基础自学Golang 第15章 Go命令行工具 15.5 代码测试(test) 15.5.2 基准测试 15.5.3 覆盖率测试

非零基础自学Golang 文章目录非零基础自学Golang第15章 Go命令行工具15.5 代码测试(test)15.5.2 基准测试15.5.3 覆盖率测试第15章 Go命令行工具 15.5 代码测试(test) 15.5.2 基准测试 基准测试提供可自定义的计时器和一套基准测试算法&#xff0c;能方便快速地分析一段代码…

P5 PyTorch 常用数学运算

前言&#xff1a; 这里主要介绍一下PyTorch 的常用数学运算 目录&#xff1a; 1&#xff1a; add|sub 加减法 2: mul/div 乘/除运算 3: 矩阵乘法 4 2D矩阵转置 5 其它常用数学运算 6 clamp 梯度剪裁 一 加减法 1.1 加法 可以直接通过符号 或者 torch.add # -*- co…

并发编程学习(五):设计模式~同步模式之保护性暂停

1、保护性暂停 模式的定义 保护性暂停 即Guarded Suspension&#xff0c;用于在一个线程等待另一个线程的执行结果。 要点&#xff1a; 有一个结果需要从一个线程传递到另一个线程&#xff0c;让它们关联同一个对象GuardedObject。如果有结果不断从一个线程到另一个线程&…

Redis架构演变之主从、Sentinel哨兵、Cluster(通信、分片、路由等机制)

一. 主从复制 1. 含义 在分布式系统中&#xff0c;为了解决单点问题&#xff0c;通常会把数据复制多个副本到其它机器&#xff0c;满足故障恢复和负载均衡等要求&#xff0c;Redis也是如此&#xff0c;提供了主从复制功能。&#xff08;redis第一代架构&#xff09; 实质&…

程序员35岁就失业了吗?就没有其他路可以选了吗?

前言 回到老家最近感到很迷茫&#xff0c;不知道该做什么&#xff0c;也不知道学习了更多的技术又能干什么。 有句话确实是很符合我现在的处境&#xff1a;时势造英雄&#xff01;虽然我不是英雄&#xff0c;但是我确实需要一个鞥一展所长的环境。 记得当初决定回到哈尔滨&a…

【大话设计模式】工厂+策略+装饰模式 hw01

背景 小李已经是一个工作一年的初级工程师了&#xff0c;他所在的公司是一家大型购物商场。随着各种网络购物软件兴起&#xff0c;老板也想做一个商场的购物 APP。分给小李的是一个一个订单结算模块&#xff0c;需要支持各种不同的结算策略。 需求 请帮小李写一个订单结算模…

vm2 <3.9.10 存在任意代码执行漏洞

漏洞描述 vm2 是一个基于 Node.js 的沙箱环境&#xff0c;可以使用列入白名单的 Node 内置模块运行不受信任的代码。 vm2 3.9.10之前版本中由于 WeakMap.prototype.set 方法使用原型查找从而存在任意代码执行漏洞&#xff0c;攻击者可利用此漏洞在沙箱内执行任意恶意代码&…

盲盒抽奖流程

盲盒模块的流程大致如下&#xff1a; 进入盲盒抽奖页面&#xff0c;需要初始化直接获取一些盲盒的信息&#xff0c;例如&#xff1a;盲盒活动id&#xff0c;开奖buff等。首先需要获取盲盒活动id&#xff0c;后面的所有请求都是基于盲盒活动id进行的。 初始化获取: 盲盒活动id…

Thymeleaf 下拉列表传值示例

参考资料 Spring Boot で Thymeleaf 使い方メモ 目录一. 前期准备二. 实体类.内部类设置下拉列表值2.1 form实体类2.2 Controller层2.3 Thymeleaf页面三. request.setAttribute()设置下拉列表值3.1 定义下拉列表存放类3.2 Controller层3.3 Thymeleaf页面一. 前期准备 枚举类 …