【数据结构】【C++】哈希表的模拟实现(哈希桶)

news2024/12/23 11:00:39

【数据结构】&&【C++】哈希表的模拟实现(哈希桶)

  • 一.哈希桶概念
  • 二.哈希桶模拟实现
    • ①.哈希结点的定义
    • ②.数据类型适配
    • ③.哈希表的插入
    • ④.哈希表的查找
    • ⑤.哈希表的删除
    • ⑥.哈希表的析构
  • 三.完整代码

一.哈希桶概念

哈希桶这种形式的方法本质上是开散列法,又叫链地址法。
首先对数据(关键码值集合)使用除留余数法,计算出对应的哈希地址,具有相同的地址的数据就归于同一个子集,每个子集称为一个桶,每个桶里的元素就用单链表的形式链接到一起。每个链表的头部存储在哈希表里。

除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,我们通常直接让p的值为哈希表的长度。
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
在这里插入图片描述
在这里插入图片描述

所以我们可以注意到哈希桶里的元素都是那些发生哈希冲突的元素。

我们要在模拟实现之前,分析一下哈希表的结构是如何的,首先哈希表底层存储的是一个数组指针,数组里存的都是指向哈希结点的指针。还需要存一个计算表里数据个数的变量。用来计算负载因子的大小。(负载因子就是表里数据的个数除以表的大小)
哈希结点里存的就是数据了,不过除了存储数据外,还应该存一个可以指向下一个位置的指针,用来链接哈希冲突的元素。

二.哈希桶模拟实现

①.哈希结点的定义

哈希结点里存储的是数据和指向下一个结点的指针。
存储的数据可以是pair<K,V>类型的数据。

//哈希结点
template <class K,class V>
struct HashNode
{
	pair<K, V> _kv;
	//存储的数据
	HashNode<K, V>* _next;
	//指向下一个结点的指针

	HashNode(const pair<K,V> kv)
		:_kv(kv)
		,_next(nullptr)
	{}
};

在插入之前我们要完成哈希表框架的搭建:首先给哈希表开辟10个大小的空间。

template<class K,class V >
//哈希表
class Hash_table
{
	typedef HashNode<K, V> Node;
public:
	Hash_table()
	{
		//构造
		_table.resize(10, nullptr);
		//首先开出十个空间,每个空间值为空
	}

	bool insert(const pair<K, V> _kv)
	{
		……………………
	}

private:
	//底层封装的是一个数组指针
	//数组里的指针指向的都是哈希结点
	vector<Node*> _table;
	//底层还封装一个大小n表示实际表中的数据个数
	size_t n=0;///用来计算负载因子
};

②.数据类型适配

我们这里采用除留余数法来计算数据的哈希地址。也就是用数据取模表的大小,计算出对应的哈希地址。但问题是,这里的数据不一定是int类型啊,如果是string类型呢?我们知道只有数值类型才可以支持取模,其他类型是不支持取模的。那怎么处理呢?

通常这种情况,我们就需要使用仿函数来处理了。根据不同情况处理:
对于数值类型我们可以直接强制类型转换成size_t类型,这样做的好处就是对于负数我们也可以进行取模了。


template <class K>
struct defaulthashfunc//默认的仿函数可以让数据模
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
		//将key强行类型转化
		//这样作的意义:可以负数也可以进行模了
	}
};

如果对于string类型,那我们可以将string类型的字符全部相加,这样就会得到一个数值。不过这样可能会存在大量的冲突,可能两个不同的字符串,最后得到的数值是一样大的。所以为了减少冲突。又大佬依据大量的数据处理,得出,每个字符都乘数131后再相加,这样就可以大大减少冲突概率。

template <class string>
struct defaulthashfunc
{
	size_t operator()(const string& str)
	{
		//为了减少冲突,我们将字符串的每个字符的值相加
		size_t hash = 0;
		for (auto& it : str)
		{
			hash *= 131;
			hash += it;
		}
		return hash;
	}
};

然后我们再使用一个模板的特化:
就可以根据传的模板类型来调用不同的仿函数了。

template <>
//模板的特化,当数据类型是int的时候就默认使用defaulthashfunc<int>,当数据类型是string类型时,就默认使用defaulthashfunc<string>
struct defaulthashfunc<string>
{
	size_t operator()(const string& str)
	{
		//为了减少冲突,我们将字符串的每个字符的值相加
		size_t hash = 0;
		for (auto& it : str)
		{
			hash *= 131;
			hash += it;
		}
		return hash;
	}
};

在这里插入图片描述

③.哈希表的插入

Ⅰ.将数据插入到哈希表中的逻辑很简单
第一步:根据除留余数法将数据对于的哈希地址求出来。
第二步:为插入的数据创建结点。
第三步:将创建好的新结点头插到哈希表里。
第四步:对应的n要++.
【问题】为什么是头插而不是尾插呢?
尾插也可以,但是尾插还需要找尾,头插直接插入到哈希表里即可,让新结点的尾部指向原来的新结点,而原来新结点的位置就变成了新结点。

       size_t hashi = hf(_kv.first) % _table.size();
        //计算哈希地址
		Node* newnode = new Node(_kv);
		//创建新结点
		newnode->_next = _table[hashi];
		_table[hashi] = newnode;
		//将新结点头插到哈希表里
		++n;
		//对应的n++

Ⅱ.难的是哈希表的扩容逻辑
【问题】:为什么要扩容,为什么要控制扩容逻辑?
有人可能有些迷惑,为什么要扩容啊?我们底层使用的不是vector吗?vector不是会自动扩容吗?确实,vector会自动扩容,但我们不能使用vector的自动扩容。为什么呢?
原因2:如果我们不扩容,当数据很多时,就可能会发生很多冲突,一旦冲突多了(哈希桶很长),那么跟单链表有什么区别呢?
原因1:因为我们的数值都是按照哈希表的大小进行取模获取的地址的,一旦哈希表扩容后,那么原来冲突的数值就又可能不冲突了,原来不冲突的数值就可能冲突了。所以我们一旦扩容完,还需要对这些数值进行重新定位,重新进行哈希插入。
【问题】:那什么时候应该扩容呢?
当哈希桶里平均存储一个数据时就进行扩容,也就是当负载因子是1时就进行扩容。


Ⅲ.扩容逻辑
①当负载因子到达1时就进行扩容
②按照二倍扩容开空间,开辟一个新的哈希表。
③遍历旧表,将旧表上的结点全部拿下来,按照插入逻辑迁到新表上。
④最后遍历完后,将新表和旧表交换一下,这样数据就到旧表上了。

bool insert(const pair<K, V> _kv)
	{

		 Hashfunc hf;
		//仿函数可以使数据模
	//在插入之前,确认一下表里是否已经有了这个值,如果有了就不用插入了 -->这个查找函数后面会写,这里直接先用
		if (find(_kv.first))
		{
			return false;
		}
		//我们自己控制扩容逻辑,虽然vector会自己扩容,但我们要控制。因为扩容完,有的key会冲突有的值又不冲突了。
	   //如果不扩容,那么冲突多了就根单链表一样了
		//当负载因子大约等于1时就要扩容,平均每个桶里有一个数据

		if (n == _table.size())//负载因子=1
		{
			//异地扩容,重新开空间
			size_t newSize = _table.size() * 2;
			//2倍空间大小
			vector<Node*> newtable;
			newtable.resize(newSize, nullptr);

			//不能再复用下面的方法,这样不好,因为就又重开空间,然后又要释放,
			//我们应该将原来的结点拿过来使用
			//所以我们遍历旧表,将旧表的结点拿过来,签到新表上
			for (size_t i = 0; i < _table.size(); i++)
			{
				//扩容后,空间size变大了,有的数据就可能会存到不同的桶里了
				//拿下来的结点要重新计算放进哪个位置
				Node* cur = _table[i];
				//cur只是哈希桶头结点,后面可能还有结点,所以需要将cur后面的结点全部拿走
				//cur后面可能还有链接的结点
				while (cur)
				{
					size_t hashi = hf(cur->_kv.first) % newtable.size();
					//按照上面的插入逻辑
					Node* next = cur->_next;
					//记录一下当前结点后面的位置,不然后面就被覆盖了
					
					//头插到新表
					cur->_next = newtable[hashi];
					//头插 这个结点的 接到插入结点的前面对吧
					//那么next就接到newtavle[i]
					newtable[hashi] = cur;

					//往后走接着拿走
					cur = next;
				}

				//当前桶里的结点被拿光后,就置为空
				_table[i] = nullptr;


			}
			//这个新表就是我们想要的表,那么我们利用vector、的交换,让旧表和新表交换

			_table.swap(newtable);
		}
      //插入逻辑
		size_t hashi = hf(_kv.first) % _table.size();

		Node* newnode = new Node(_kv);
		newnode->_next = _table[hashi];
		_table[hashi] = newnode;
		//将新结点头插到哈希桶里
		++n;
		return true;
	}

④.哈希表的查找

插入简单的很,就计算数值对应的哈希地址,看该哈希地址上是否有该值就可以了。不过由于哈希冲突,可能查找的值在哈希桶头结点的后面。所以需要遍历一下哈希桶。

	Node* find(const K& key)
	{

		Hashfunc hf;
		//仿函数,用来获取数据的数值大小
		
		size_t hashi = hf(key)% _table.size();

		Node* cur = _table[hashi];
		//用来遍历哈希桶
		while (cur)
		{
			if (cur->_kv.first == key)
				return cur;
			else
				cur = cur->_next;
		}
		return nullptr;
	}

⑤.哈希表的删除

删除的逻辑也很简单,就是找到key的位置,然后删除掉key所在的结点即可。那我们可不可以复用find函数来查找要删除的结点呢?
不能。因为删除逻辑需要找到所删除结点的前后位置,这样才可以删除结点,即让前面结点的next指向后面的结点。而find只找到要删除结点的位置,前后位置是找不到,所以不能直接使用,但我们可以使用其中的逻辑。

删除逻辑
①首先根据除留余数法计算数值的哈希地址。
②遍历所在的哈希桶,在遍历时,需要记录前面结点的位置。
③找到要删除结点后,就删除,没有找到就继续往后找,记录前面位置。
③注意要删除结点可能是头结点。

bool erase(const K& key)
	{

		Hashfunc hf;
		//可以复用find吗?先用find找到key然后删除key呢?4
		//不可以,因为删除一个结点需要找到这个结点的前面和后面的位置,但这里只有key的位置,所以不能直接复用find,但是复用其中的步骤
		size_t hashi = hf(key) % _table.size();

		Node* cur = _table[hashi];
		Node* prev = nullptr;
		while (cur)
		{
			if (cur->_kv.first == key)//找到要删除的结点后
			{
				
				//删除逻辑:将前面的结点的指针指向后面的前面

				//还有一种可能cur就是桶里的第一个,那么就是头删了,prev就是nullptr
				if (prev == nullptr)
				{
					_table[hashi] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
			
				delete cur;
				return true;
			}
			else
			{
				prev = cur;
				//每次先记录一下前面的结点位置
				cur = cur->_next;
			}
				
		}
		return false;

	}

⑥.哈希表的析构

为什么要写析构呢?我们使用的不是vector吗?vector不是会自动调用析构函数吗?
确实,vector会调用析构函数,但要注意vector存储的数据指针类型,是自定义类型,没有默认构造,所以我们开辟的结点的那些空间,需要我们自己释放,不然无法释放。
最简单的就是遍历哈希桶即可,将每个结点都释放。

~Hash_table()
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				//要先保存起来
				delete cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
	}

三.完整代码

#pragma once
using namespace std;
#include <vector>
#include<iostream>


//哈希桶

//哈希结点
template <class K,class V>
struct HashNode
{
	pair<K, V> _kv;
	//存储的数据
	HashNode<K, V>* _next;
	//指向下一个结点的指针

	HashNode(const pair<K,V> kv)
		:_kv(kv)
		,_next(nullptr)
	{}
};

template <class K>
struct defaulthashfunc//默认的仿函数可以让数据模
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
		//将key强行类型转化
		//这样作的意义:可以负数也可以进行模了
	}
};
template <>
//模板的特化,当数据类型是int的时候就默认使用defaulthashfunc<int>,当数据类型是string类型时,就默认使用defaulthashfunc<string>
struct defaulthashfunc<string>
{
	size_t operator()(const string& str)
	{
		//为了减少冲突,我们将字符串的每个字符的值相加
		size_t hash = 0;
		for (auto& it : str)
		{
			hash *= 131;
			hash += it;
		}
		return hash;
	}
};

//要写一个仿函数?  因为不是所有的数据类型都可以模的
// 一般整数是可以模的,string类型是无法模的
// 所以我们要写一个仿函数来达到传的数据可以模
//这样也就是增加了哈希表的一个模板参数l
template<class K,class V,class Hashfunc=defaulthashfunc<K>>
//哈希表
class Hash_table
{
	typedef HashNode<K, V> Node;


	//哈希需要将写析构函数,虽然自定义类型vector会自动调用默认析构,但它里面的成员是内置类型,没有默认构造,
	//所以需要我们自己析构每个结点
public:
	~Hash_table()
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				//要先保存起来
				delete cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
	}
	Hash_table()
	{
		//构造
		_table.resize(10, nullptr);
		//首先开出十个空间,每个空间值为空
	}

	bool insert(const pair<K, V> _kv)
	{

		 Hashfunc hf;
		//仿函数可以使数据模
		//在插入之前,确认一下表里是否已经有了这个值,如果有了就不用插入了
		if (find(_kv.first))
		{
			return false;
		}

		//我们自己控制扩容逻辑,虽然vector会自己扩容,但我们要控制。因为扩容完,有的key会冲突有的值又不冲突了。
	   //如果不扩容,那么冲突多了就根单链表一样了
		//当负载因子大约等于1时就要扩容,平均每个桶里有一个数据

		if (n == _table.size())
		{
			//异地扩容,重新开空间
			size_t newSize = _table.size() * 2;
			vector<Node*> newtable;
			newtable.resize(newSize, nullptr);

			//不能再复用下面的方法,这样不好,因为就又重开空间,然后又要释放,
			//我们应该将原来的结点拿过来使用
			//所以我们遍历旧表,将旧表的结点拿过来,签到新表上
			for (size_t i = 0; i < _table.size(); i++)
			{
				//扩容后,空间size变大了,有的数据就可能会存到不同的桶里了
				//拿下来的结点要重新计算放进哪个位置
				Node* cur = _table[i];
				//cur后面可能还有链接的结点
				while (cur)
				{
					size_t hashi = hf(cur->_kv.first) % newtable.size();
					

					Node* next = cur->_next;
					//头插到新表
					cur->_next = newtable[hashi];
					//头插 这个结点的 接到插入结点的前面对吧
					//那么next就接到newtavle[i]
					newtable[hashi] = cur;

					//往后走接着拿走
					cur = next;
				}

				//当前桶里的结点被拿光后,就置为空
				_table[i] = nullptr;


			}
			//这个新表就是我们想要的表,那么我们利用vector、的交换,让旧表和新表交换

			_table.swap(newtable);
		}

		size_t hashi = hf(_kv.first) % _table.size();

		Node* newnode = new Node(_kv);
		newnode->_next = _table[hashi];
		_table[hashi] = newnode;
		//将新结点头插到哈希桶里
		++n;
		return true;
	}
	Node* find(const K& key)
	{

		Hashfunc hf;
		size_t hashi = hf(key)% _table.size();

		Node* cur = _table[hashi];
		
		while (cur)
		{
			if (cur->_kv.first == key)
				return cur;
			else
				cur = cur->_next;
		}
		return nullptr;
	}
	void Print()
	{
		for (int i = 0; i < _table.size(); i++)
		{
			printf("[%d]", i);

			Node* cur = _table[i];
			while (cur)
			{
				cout << cur->_kv.first << " ";
				cur = cur->_next;
			}
			cout << endl;
			}

	}
	bool erase(const K& key)
	{

		Hashfunc hf;
		//可以复用find吗?先用find找到key然后删除key呢?4
		//不可以,因为删除一个结点需要找到这个结点的前面和后面的位置,但这里只有key的位置,所以不能直接复用find,但是复用其中的步骤
		size_t hashi = hf(key) % _table.size();

		Node* cur = _table[hashi];
		Node* prev = nullptr;
		while (cur)
		{
			if (cur->_kv.first == key)//找到要删除的结点后
			{
				
				//将前面的结点的指针指向后面的前面

				//还有一种可能cur就是桶里的第一个,那么就是头删了,prev就是nullptr
				if (prev == nullptr)
				{
					_table[hashi] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}
			
				delete cur;
				return true;
			}
			else
			{
				prev = cur;
				//每次先记录一下前面的结点位置
				cur = cur->_next;
			}
				
		}
		return false;

	}
private:
	//底层封装的是一个数组
	//数组里的指针指向的都是哈希结点
	vector<Node*> _table;
	//底层还封装一个大小n表示实际表中的数据个数
	size_t n=0;///用来计算负载因子
};

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

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

相关文章

常用压缩解压缩命令

在Linux中常见的压缩格式有.zip、.rar、.tar.gz.、tar.bz2等压缩格式。不同的压缩格式需要用不同的压缩命令和工具。须知&#xff0c;在Linux系统中.tar.gz为标准格式的压缩和解压缩格式&#xff0c;因此本文也会着重讲解tar.gz格式压缩包的压缩和解压缩命令。须知&#xff0c;…

26667-2021 电磁屏蔽材料术语 学习笔记

声明 本文是学习GB-T 26667-2021 电磁屏蔽材料术语. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件界定了0 Hz&#xff5e;500GHz 频率范围内具有电磁屏蔽作用的材料的术语和定义。 本文件适用于电磁屏蔽材料领域及相关的设备、人体和…

[C++ 网络协议] 异步通知I/O模型

1.什么是异步通知I/O模型 如图是同步I/O函数的调用时间流&#xff1a; 如图是异步I/O函数的调用时间流&#xff1a; 可以看出&#xff0c;同异步的差别主要是在时间流上的不一致。select属于同步I/O模型。epoll不确定是不是属于异步I/O模型&#xff0c;这个在概念上有些混乱&a…

数据安全:文件分析

什么是文件分析 文件分析是在组织的文件存储库中扫描、报告和处理文件安全性和存储的过程。文件安全性分析处理检查文件的位置、所有权和权限安全。文件存储分析包括检测结构化和非结构化数据&#xff0c;以及存储容量规划&#xff0c;由于对暗数据的担忧日益增加&#xff0c;…

基于Java的音乐网站管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&…

【MATLAB源码-第35期】matlab基于lms算法的陷波器仿真,估计误差,估计频率。

1、算法描述 1. LMS算法&#xff1a; LMS&#xff08;Least Mean Square&#xff09;算法是一种自适应滤波算法。其核心思想是通过最小化输入信号和期望响应之间的均方误差来调整滤波器的权重。 LMS算法的更新公式为&#xff1a;w(n1)w(n)μe(n)x(n) 其中&#xff0c; w(n) …

OSPF基础

OSPF&#xff1a;开放式最短路径优先协议 无类别IGP协议&#xff1a;链路状态型(LS) 基于LSA收敛&#xff0c;故更新量较大&#xff0c;在大中型网络正常工作&#xff0c;需要进行结构化部署---区域划分&#xff0c;IP地址规划 组播更新----224.0.0.5 224.0.0.6 支持等开销…

【学习笔记】CF1817F Entangled Substrings(基本子串结构)

前置知识&#xff1a;基本子串结构&#xff0c;SAM的结构和应用 学长博客 字符串理论比较抽象&#xff0c;建议直观的去理解它 子串 t t t的扩展串定义为 ext(t) : t ′ \text{ext(t)}:t ext(t):t′&#xff0c;满足 t t t是 t ′ t t′的子串&#xff0c;且 occ(t) occ(t…

【数据结构】堆,堆的实现,堆排序,TOP-K问题

大家好&#xff01;今天我们来学习数据结构中的堆及其应用 目录 1. 堆的概念及结构 2. 堆的实现 2.1 初始化堆 2.2 销毁堆 2.3 打印堆 2.4 交换函数 2.5 堆的向上调整 2.6 堆的向下调整 2.7 堆的插入 2.8 堆的删除 2.9 取堆顶的数据 2.10 堆的数据个数 2.11 堆的判…

【VUE复习·10】v-for 高级::key 作用和原理;尽量不要使用 index 来遍历

总览 1.:key 作用和原理 2.尽量不要使用 index 来遍历 一、:key 作用和原理 1.数据产生串位的原因 在我们使用 index 进行遍历的时候&#xff0c;会出现虚拟 DOM 和 真实 DOM 的渲染问题。 二、尽量不要使用 index 来遍历 详情见视频 1/3 处&#xff1a; https://www.bili…

复杂的连接如何破坏智能家居体验

智能家居网络复杂性的增加可能会导致客户体验不佳、回报增加以及品牌声誉挑战。如果不加以解决&#xff0c;这一趋势可能会影响智能家居市场的未来增长。 智能家居网络复杂性的增加可能会导致客户体验不佳、回报增加以及品牌声誉挑战。如果不加以解决&#xff0c;这一趋势可能会…

【数据库——MySQL】(12)过程式对象程序设计——存储过程

目录 1. 存储过程2. 局部变量3. 条件分支3.1 IF 语句3.2 CASE 语句 4. 循环语句4.1 WHILE 语句4.2 REPEAT 语句4.3 LOOP和LEAVE语句4.4 LOOP和ITERATE语句 5. 存储过程应用示例参考书籍 1. 存储过程 要创建存储过程&#xff0c;需要用到 CREATE 语句&#xff1a; CREATE PROCED…

卸载无用Mac电脑软件应用程序方法教程

如何在Mac电脑卸载应用程序&#xff1f;Mac OS系统的用户卸载软件时&#xff0c;大部分会选择直接将软件图标拖进废纸篓清倒。这种操作会留下大量程序残余文件占据磁盘空间&#xff0c;手动清理又怕误删文件&#xff0c;有时还会遇到无法移除的恶意/流氓软件。小编今天分享3种可…

端口被占用怎么解决

第一步&#xff1a;WinR 打开命令提示符&#xff0c;输入netstat -ano|findstr 端口号 找到占用端口的进程 第二步&#xff1a; 杀死使用该端口的进程&#xff0c;输入taskkill /t /f /im 进程号&#xff08; &#xff01;&#xff01;&#xff01;注意是进程号&#xff0c;不…

分布式文件系统FastDFS实战

1. 分布式文件系统应用场景 互联网海量非结构化数据的存储需求&#xff1a; 电商网站&#xff1a;海量商品图片视频网站&#xff1a;海量视频文件网盘&#xff1a;海量文件社交网站&#xff1a;海量图片 2.FastDFS介绍 https://github.com/happyfish100/fastdfs 2.1简介 …

Spring Boot中配置文件介绍及其使用教程

目录 一、配置文件介绍 二、配置简单数据 三、配置对象数据 四、配置集合数据 五、读取配置文件数据 六、占位符的使用 一、配置文件介绍 SpringBoot项目中&#xff0c;大部分配置都有默认值&#xff0c;但如果想替换默认配置的话&#xff0c;就可以使用application.prop…

Unity如何实现TreeView

前言 最近有一个需求,需要实现一个TreeView的试图显示,开始我一直觉得这么通用的结构,肯定有现成的UI组件或者插件可以使用,结果,找了好久,都没有找到合适的插件,有两个效果差强人意。 最后在回家的路上突然灵光一闪,想到了一种简单的实现方式,什么插件都不用,仅使用…

react create-react-app v5配置 px2rem (暴露 eject方式)

环境信息&#xff1a; create-react-app v5 “react”: “^18.2.0” “postcss-plugin-px2rem”: “^0.8.1” 配置步骤&#xff1a; 我这个方式是 npm run eject 暴露 webpack配置的方法 1.安装 postcss-plugin-px2rem 和 lib-flexible cnpm install postcss-plugin-px2rem…

RV1126笔记四十一:RV1126移植LIVE555

若该文为原创文章,转载请注明原文出处。 RV1126的SDK有提供了一个librtsp.a封装好的RTSP推流库,但不开源,还有个确定延时长,所以想自己写一个RTSP的推流,但不想太麻烦,所以使用Live555。 记录下移植过程和测试结果。 live555需要用到的包有 openssl 和live555 一、 编…

基于SpringBoot的服装生产管理系统的设计与实现

目录 前言 一、技术栈 二、系统功能介绍 登录界面的实现 系统主界面的实现 用户管理模块的实现 人事安排管理模块的实现 工资管理模块的实现 考勤管理模块的实现 样板管理模块的实现 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 本协力服装厂服装生…