C++ 哈希桶和封装unordered_map和unordered_set

news2024/10/20 15:52:10

目录

哈希桶的概念

哈希桶的结构

哈希桶的结点

哈希桶的类 

Insert插入函数

 Find查找函数

Erase删除函数 

哈希的两种仿函数(int) 和(string)

哈希表的改造

​编辑

迭代器

 改造

unordered_map和unordered_set的封装


前言

上一篇文章讲的哈希表,属于闭散列。可以解决哈希冲突有两种方式:闭散列和开散列。现在要学习的哈希桶就是开散列。

哈希桶的概念

首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

下面即是哈希桶的简易图

​ 

哈希桶的结构

哈希桶的结点

由上图就可以看出来,每个结点必要存一个 pair 和一个指向下一个节点的指针 _next。

template<class T>
struct HashNode
{
	T _data;
	HashNode<T>* _next;
	HashNode(const T& data)
		:_data(data)
		, _next(nullptr)
	{}

};

哈希桶的类 

哈希桶类的构成和哈希表类似,都是一个由一个 vector 存放每个节点,但是这里与 HashTable 不同的是需要存放的是节点的指针。还有一个 _n 代表有效数据的个数:

template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{
    typedef HashNode Node;
private:
    vector<Node*> _ tables;
    size_t _n;
};

Insert插入函数

此时只需要使用 Key 模数组的大小来计算出该节点需要连接在 vector 上的位置,然后使用 new 得到储存 kv 的新节点,当 new 一个新节点时,节点的构造函数必不可少,下面先来看一下单个节点的构造函数以及类的构造函数: 

template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode* _next;
	HashNode(const pair<K, V>& kv):_kv(kv), _next(nullptr)
	{}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{    
    typedef HashNode<K, V> Node;
    HashBucket()
    {
        _tables.resize(10, nullptr);
        _n = 0;
    }
private:
    vector<Node*> tables;
    size_t _n;
}

这个仿函数HashFunc后面会为大家介绍

在插入的时候,我们是头插还是尾插呢?

很显然,头插的效率要更高;

size_t hashi = hf(kv.first) % _tables.size();
Node* newnode = new Node(kv);

// 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;

当插入的值很多的时候我们就要扩容了,这里一直没提到_n的作用是什么

其实_n是负载因子,当负载因子==_tables.size()时就要进行扩容了

扩容逻辑:定义一个新的指针数组,大小扩容到原指针数组大小的2倍,再逐个插入到新数组。

最后交换。

if (_n * 10 / _tables.size() == 1)
{
	vector<Node*> Newtables;
	Newtables.resize(_tables.size() * 2);
	for (int i = 0; i < _tables.size(); i++)
	{
		Node* cur = _tables[i];
		while (cur)
		{
			size_t hashi = hf(kot(cur->_data)) % Newtables.size();
			Node* next = cur->_next;
			cur->_next = Newtables[hashi];
			Newtables[hashi] = cur;
			cur = next;
		}
		_tables[i] = nullptr;

		_tables.swap(Newtables);
	}
}

 Find查找函数

Key 模数组的大小来计算出该节点需要连接在 vector 上的位置,找到在数组的位置通过while循环,查找桶内元素是否存在Key值。

Node* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}

				cur = cur->_next;
			}

			return NULL;
		}

Erase删除函数 

根据 Insert 函数中,可以得知, HashBucket 的每个节点都是 new 出来的,那删除的时候就要使用 delete ,又因为每个节点都是自定义类型,所以要为 HashBucket 写一个析构函数。
对类的析构就是遍历 vector 的每个节点,再从每个节点遍历每个链表,以此遍历全部节点。

	~HashTable()
	{
		for (int i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
	}

Erase删除思路

1.计算哈希值:使用哈希函数 hs 计算给定键 Key 的哈希值,并确定它在桶中的索引 index。

2.遍历链表:从索引 index 开始,遍历链表中的每个节点。

3.查找节点:检查当前节点的键是否等于 Key。

     如果找到匹配节点:

  1.          如果该节点是链表的第一个节点,将桶的头指针 _bucket[index] 指向下一个节点。
  2.          否则,将前一个节点的 _next 指针指向当前节点的下一个节点。
  3.          删除当前节点 cur,释放内存。
  4.          返回 true,表示删除成功。

如果没有找到匹配节点,继续遍历链表,更新 prev 和 cur。

  1. 返回结果:如果遍历完整个链表未找到匹配节点,返回 false,表示删除失败。

   

定义一个prev值 防止删除指定元素时,找不到上一个元素

bool Erase(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;

					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

哈希的两种仿函数(int) 和(string)

是为了直接获取key值

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		// BKDR
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}

		//cout << key << ":" << hash << endl;
		return hash;
	}
};

这里对string进行了特化,因为对string类的仿函数不同。

哈希表的改造

unordered_map和unordered_set这两个stl容器,从上图可以看到,他们底层似乎都是使用哈希来实现的。那么在理解了哈希的基础上,不妨尝试一下自己模拟实现一下简单的unordered_map和unordered_set也给之前的哈希结构加上迭代器操作。 

unordered_map和unordered_set的基本结构

从C++库中可以看出来,map和set一大区别就在于set只有一个关键字key,而map中存在key和value。因此他们的基本结构可以分别这样设计:其中在原来哈希的基础上又多了一个KeyOfT的仿函数,他的作用主要是确定需要进行比较的关键字,例如map中需要比较的是T中的first,而set就是key

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
  private:
	hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};

	template<class K, class Hash = HashFunc<K>>
	class My_Ordered_set
	{
		struct KeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
      private:
	   hash_bucket::HashTable<K, K, KeyOfT, Hash> _ht;

    };   

迭代器

基本框架

template<class K, class T,class Ref,class Ptr, class KeyOfT, class Hash>
struct HashIterator
{
	typedef HashNode<T> Node;
	Node* _node;
	typedef HashIterator<K, T,Ref,Ptr, KeyOfT, Hash> Self;
	const HashTable<K, T, KeyOfT, Hash>* _pht;
	size_t _hashi;
	HashIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
		:_node(node)
		, _pht(pht)
		, _hashi(hashi)
	{}

但在此之前要写一个前置声明,因为我们在基本框架里面定义了_pht,使用了哈希桶类,但代码只会向上查找,找不到哈希桶类 

编译器根本不认识_pht前面的代码

前置声明

	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;//前置声明

stl中的unordered_map和unordered_set内部实现了迭代器,可以使用迭代器访问其中的内容,这里我们也在哈希中实现一个迭代器操作,然后通过封装实现unordered_map和unordered_set的迭代器,实现代码如下,迭代器里面主要需要注意的是operator++操作。这个操作需要遍历哈希表以及每个点上所挂的链表。因此可以这样考虑:先判断当前链表是否走完,如果没走完就直接往下一个节点走,如果走完了就将hashi++,前往下一个哈希地址,然后开始遍历链表

迭代器完整代码

template<class K, class T, class KeyOfT, class Hash>
class HashTable;//前置声明

template<class K, class T,class Ref,class Ptr, class KeyOfT, class Hash>
struct HashIterator
{
	typedef HashNode<T> Node;
	Node* _node;
	typedef HashIterator<K, T,Ref,Ptr, KeyOfT, Hash> Self;
	const HashTable<K, T, KeyOfT, Hash>* _pht;
	size_t _hashi;
	HashIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
		:_node(node)
		, _pht(pht)
		, _hashi(hashi)
	{}
	HashIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
		:_node(node)
		, _pht(pht)
		, _hashi(hashi)
	{}

	Self& operator++()
	{
		if (_node->_next)
		{
			_node = _node->_next;
		}
		else
		{
			_hashi++;
			while (_hashi < _pht->_tables.size())
			{
				if (_pht->_tables[_hashi])
				{
					_node = _pht->_tables[_hashi];
					break;
				}
				_hashi++;
			}
			if (_hashi == _pht->_tables.size())
			{
				_node = nullptr;
			}
		}
		return *this;
	}
	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}
};

 改造

在完成迭代器之后,可以在原来的哈希表结构体中完善一下insert等参数的返回值,并加入新的begin,end接口函数,代码如下:

template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
	template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
	friend struct HashIterator;
	typedef HashNode<T> Node;
public:
	typedef HashIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
	typedef HashIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;
	iterator begin()
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i])
			{
				return iterator(_tables[i], this, i);
			}
		}

		return end();
	}
	iterator end()
	{
		return iterator(nullptr, this, -1);
	}
	const_iterator begin() const
	{
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i])
			{
				return const_iterator(_tables[i], this, i);
			}
		}

		return end();
	}

	// this-> const HashTable<K, T, KeyOfT, Hash>*
	const_iterator end() const
	{
		return const_iterator(nullptr, this, -1);
	}


	HashTable()
	{
		_tables.resize(10, nullptr);
	}

	// 哈希桶的销毁
	~HashTable()
	{
		for (int i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}
			_tables[i] = nullptr;
		}
	}
	pair<iterator, bool>  Insert(const T& data)
	{
		Hash hf;
		KeyOfT kot;

		iterator it = Find(kot(data));
		if (it != end())
			return make_pair(it, false);
		if (_n * 10 / _tables.size() == 1)
		{
			vector<Node*> Newtables;
			Newtables.resize(_tables.size() * 2);
			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					size_t hashi = hf(kot(cur->_data)) % Newtables.size();
					Node* next = cur->_next;
					cur->_next = Newtables[hashi];
					Newtables[hashi] = cur;
					cur = next;
				}
				_tables[i] = nullptr;

				_tables.swap(Newtables);
			}
		}
		size_t hashi = hf(kot(data)) % _tables.size();
		Node* newnode = new Node(data);
		newnode->_next = _tables[hashi];
		_tables[hashi] = newnode;

		_n++;
		return make_pair(iterator(newnode, this, hashi), true);
	}
	iterator Find(const K& key)
	{
		Hash hf;
		KeyOfT kot;

		size_t hashi = hf(key) % _tables.size();
		Node* cur = _tables[hashi];
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				return iterator(cur, this, hashi);
			}

			cur = cur->_next;
		}

		return end();
	}

	// 哈希桶中删除key的元素,删除成功返回true,否则返回false
	bool Erase(const K& key)
	{
		KeyOfT kot;
		Hash hf;
		size_t hashi = hf(kot(key)) % _tables.size();
		Node* cur = _tables[hashi];
		Node* prev = nullptr;
		while (cur)
		{
			if (kot(cur->_data) == key)
			{
				if (prev == nullptr)
				{
					_tables[hashi] = cur->_next;
				}
				else
				{
					prev->_next = cur->_next;
				}

				delete cur;
				--_n;
				return true;
			}

			prev = cur;
			cur = cur->_next;
		}

		return false;
	}

这里写了一个友元函数,是为什么呢?

当我们不写的时候报了一个这样的错误

具体意思就是 在迭代器里面我们要访问哈希桶类的私有成员变量,如果没有友元函数就没办法访问私有成员变量。

这里我们Insert返回是pair<iterator,bool>

find返回的是iterator

这也是为了和库里面保持一致 

完成了这些之后,接下来就是对unordered_map和unordered_set的封装了

unordered_map和unordered_set的封装

unordered_set的封装

template<class K, class Hash = HashFunc<K>>
class My_Ordered_set
{
	struct KeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:
	typedef typename hash_bucket::HashTable<K, K, KeyOfT, Hash>::const_iterator iterator;
	typedef typename hash_bucket::HashTable<K, K, KeyOfT, Hash>::const_iterator const_iterator;
	const_iterator begin()const
	{
		return _ht.begin();
	}
	const_iterator end()const
	{
		return _ht.end();
	}
	pair<const_iterator, bool> insert(const K& key)
	{
           auto ret= _ht.Insert(key);
		   return pair<const_iterator, bool>(const_iterator(ret.first._node, ret.first._pht, ret.first._hashi), ret.second);
	}
	iterator Find(const K& key)
	{
		return _ht.Find(key);
	}
	bool Erase(const K& key)
	{
		return _ht.Erase(key);
	}
private:
	hash_bucket::HashTable<K, K, KeyOfT, Hash> _ht;

这里需要注意的是,在封装插入函数的时候,我们为什么不写return _ht.insert呢?

 这里_ht返回的是iterator迭代器,而封装之后返回的是const_iterator迭代器

这里我们就要写一个将非const转换为const函数了

	HashIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
		:_node(node)
		, _pht(pht)
		, _hashi(hashi)
	{}

 这样就构建出const迭代器了

 unordered_map的封装

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
public:
	typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
	iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}
	pair<iterator, bool> insert(const pair<K, V>& kv)
	{
		return _ht.Insert(kv);
	}

	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
		return ret.first->second;
	}

	const V& operator[](const K& key) const
	{
		pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
		return ret.first->second;
	}
	iterator find(const K& key)
	{
		return _ht.Find(key);
	}

	bool erase(const K& key)
	{
		return _ht.Erase(key);
	}

private:
	hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;

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

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

相关文章

解决k8s集群中安装ks3.4.1开启日志失败问题

问题 安装kubesphere v3.4.1时&#xff0c;开启了日志功能&#xff0c;部署时有三个pod报错了 Failed to pull image “busybox:latest”: rpc error: code Unknown desc failed to pull and unpack image “docker.io/library/busybox:latest”: failed to copy: httpRead…

【D3.js in Action 3 精译_034】4.1 D3 中的坐标轴的创建(中篇):定义横纵坐标轴的比例尺

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…

京存助力北京某电力研究所数据采集

北京某电力研究所已建成了一套以光纤为主&#xff0c;卫星、载波、微波等多种通信方式共存&#xff0c;分层级的电力专用的网络通信架构体系。随着用电、配电对网络的要求提高&#xff0c;以及终端通信入网的迅速发展&#xff0c;迫切地需要高效的通信管理系统来应对大规模、复…

STM32传感器模块编程实践(七) MLX90614红外测温模块简介及驱动源码

文章目录 一.概要二.MLX90614主要技术指标三.模块参考原理图四.模块接线说明五.模块工作原理介绍六.模块通讯协议介绍七.STM32单片机与MLX90614模块实现体温测量实验1.硬件准备2.软件工程3.软件主要代码4.实验效果 八.小结 一.概要 一般来说&#xff0c;测温方式可分为接触式和…

大模型常见算子定义

本文将汇总大模型常用的算子定义&#xff0c;方便快速根据定义公式评估其计算量。 LayerNorm 这是在BERT、GPT等模型中广泛使用的LayerNorm&#xff1a; RMSNorm RMSNorm(root mean square)发现LayerNorm的中心偏移没什么用(减去均值等操作)。将其去掉之后&#xff0c;效果几乎…

51系列--人体身高体重BMI指数检测健康秤

本文主要介绍基于51单片机实现的人体身高体重BMI指数检测健康秤称设计&#xff08;程序、电路图、PCB以及文档说明书见文末链接&#xff09; 一、简介 本系统由STC89C52单片机、LCD1602液晶显示、按键、超声波测距、HX711称重传感器模块&#xff08;0-1000KG&#xff09;以及…

O(1)调度算法与CFS

目录 引言 linux内核的O&#xff08;1&#xff09;进程调度算法介绍 主要特点 工作原理 优点 缺点 运行队列 活动队列 过期队列 active指针和expired指针 O(1)调度器&#xff0c;两个队列的机制 两个队列的机制如下&#xff1a; 这个算法后期被CFS替代 CFS 工作原…

进阶篇-Redis集群算法详细介绍

目录 一 、集群是什么1.1 主从复制与集群的架构区别 二、Redis集群的作用三、集群算法3.1.分片-槽位slot3.2 分片是什么3.3如何找到找到给定的key值分片3.4分片的优势 四、槽位映射的三中国解决方案4.1 哈希取余分区算法4.2 哈希一致性算法4.2.1 背景以及概念4.2.2 算法的步骤4…

【Python加密与解密】深入了解Python中的数据加密技术!

Python加密与解密&#xff1a;深入了解Python中的数据加密技术 在现代信息时代&#xff0c;数据加密成为保障网络和通信安全的重要手段之一。无论是在保护个人隐私还是在保证企业数据的安全性方面&#xff0c;加密技术都发挥着关键作用。Python 作为一种流行的编程语言&#x…

(10) GTest c++单元测试(mac版)

文章目录 概要安装实现机制-断言&#xff08;简单、独立的测试&#xff09;实现机制-测试套件实现机制-Test Fixture和事件 概要 官方文档 https://google.github.io/googletest/ 安装 git clone https://github.com/google/googletestcd googletestmkdir build && c…

鸿蒙开发 四十五 鸿蒙状态管理(嵌套对象界面更新)

当运行时的状态变量变化&#xff0c;UI重新渲染&#xff0c;在ArkUI中称为状态管理机制&#xff0c;前提是变量必须被装饰器修饰。不是状态变量的所有更改都会引起刷新&#xff0c;只有可以被框架观测到的更改才会引起UI刷新。其中boolen、string、number类型&#xff0c;可观察…

PyQt 入门教程(3)基础知识 | 3.2、加载资源文件

文章目录 一、加载资源文件1、PyQt5加载资源文件2、PyQt6加载资源文件 一、加载资源文件 常见的资源文件有图像、图标、样式表&#xff0c;下面分别介绍下加载资源文件的常用方法 1、PyQt5加载资源文件 创建.qrc文件&#xff1a; 可以使用QtCreator或手动创建一个.qrc文件&…

注意LED亚型号区分

一. 前言 最近产品试产遇到一次批量事故&#xff1a;全部绿光LED的光功率不达标。最终定位到的原因就是未注意LED的细分型号&#xff0c;试产采用的批次与研发时的亚型号不一样&#xff0c;光功率范围不一致。在此记录下来&#xff0c;供大家做参考&#xff0c;避免走弯路。 …

桂林旅游一点通:SpringBoot平台应用

3系统分析 3.1可行性分析 通过对本桂林旅游景点导游平台实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本桂林旅游景点导游平台采用SSM框架&#xff0c;JAVA作…

uniapp结合uview-ui创建项目

准备工作&#xff1a; 下载HBuilderX;官网地址:HBuilderX-高效极客技巧 安装node.js;官网地址&#xff1a;Node.js — 在任何地方运行 JavaScript,下载所需版本&#xff0c;安装后配置好环境变量即可 方式一&#xff08;NPM安装方式&#xff09;&#xff1a; 1、打开开发者…

OpenRTP 乱序排包和差分抖动计算

OpenRTP 开源地址 OpenRTP 开源地址 暂时使用h264 aac 的音频去测试&#xff0c;点开配置去选择 1 音视频同步问题 先要解决一个音视频同步问题&#xff0c;否则包排不排序都不对&#xff0c;这是因为视频时间戳不一定能够对上音频&#xff0c;为什么呢&#xff1f;因为大部…

前端考试总结

1HTML标签 h标题标签 块级标签 独占一行 p段落标签 同上 br换行标签 单标签 img图片标签 内联标签:不独占一行(src图片地址 alt图片的替代文字 title鼠标悬停提示文字) a超链接标签 同上 (href跳转路径 target属性{_blank新窗口打开 _self在当前窗口打开}) 列表标签(ul无…

诺贝尔物理学奖与机器学习、神经网络:一场跨时代的融合与展望

诺贝尔物理学奖与机器学习、神经网络&#xff1a;一场跨时代的融合与展望 机器学习与神经网络的崛起 机器学习与神经网络的发展前景 机器学习和神经网络的研究与传统物理学的关系 总结 2024年&#xff0c;诺贝尔物理学奖颁给了机器学习与神经网络&#xff0c;这一具有里程碑…

JAVA就业笔记5——第二阶段(2)

课程须知 A类知识&#xff1a;工作和面试常用&#xff0c;代码必须要手敲&#xff0c;需要掌握。 B类知识&#xff1a;面试会问道&#xff0c;工作不常用&#xff0c;代码不需要手敲&#xff0c;理解能正确表达即可。 C类知识&#xff1a;工作和面试不常用&#xff0c;代码不…

房屋租赁管理系统|基于java和小程序的房屋租赁管理系统小程序设计与实现(源码+数据库+文档)

房屋租赁管理系统小程序 目录 基于java和小程序的房屋租赁管理系统小程序设计与实现 一、前言 二、系统功能设计 三、系统实现 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设…