【数据结构】散列(哈希)表简单介绍

news2024/9/23 22:26:26

        散列表也叫做哈希表(Hash table),散列表通过关键码和存储地址建立唯一确定的映射关系,能够快速查找到对应的元素,排序算法中的计数排序就是一种利用哈希进行排序的算法。

一、散列表的概念

        散列表(Hash table)是表示集合和字典的另一种有效方法,它提供了一种完全不同的存储和搜索方式,通过将关键码映射到表中的某个位置来存储元素,然后根据关键码用同样的方式直接访问。

二、散列方法

        在散列表中搜索元素可以不进行任何大小的比较,只需要支持比较相等即可,因为通过映射关系建立的散列表可以一次从中得到要搜索的元素的集合。

(一)散列函数(Hash function)

        元素在散列表中的存储地址和关键码之间的映射关系通过散列函数Hash()来唯一确定,使得每个关键码在散列表中都有一个唯一确定的存储位置相对应。Address为散列表中距离散列表中第一个存储位置的地址偏移量,key为元素的关键码,那么所确定的存储位置Address和关键码key之间的关系为

                                                Address = Hash(key)

        一般情况,关键码的集合比散列表地址的集合大得多,所以会出现不同元素的关键码通过同一个散列函数计算出了同一个存储位置,这些不同的关键码称之为同义词(synonym)。这些同义词元素在存储时就会发生哈希冲突,冲突往往是不能够避免的,只能尽量减小,所以我们必须有解决冲突的方法,我们也需要选择合适的散列函数,尽量让散列表中的每一个位置都能有相同的概率被不同关键码映射到,减少冲突的发生。

(二)、散列函数的选择

所有的散列函数需要遵循下面三个规定:

1、散列函数的定义域必须包含全部的关键码,并且如果散列表允许存储的地址个数为size个,那么散列函数计算出的映射地址的值域必须为[0,size - 1]。

2、通过散列函数计算出的地址应能够均匀分布在整个地址空间中,让散列表中的每一个位置都能有相同的概率被不同关键码映射到,减少冲突的发生。

3、散列函数应是简单的,能够在较短时间内计算出结果。

         下面来讲一个常用的散列函数,除留余数法其他的散列函数有很多:数学分析法、平方取中法、折叠法等等,这里不着重介绍。

        假设散列表中的存储地址个数为size个,某元素的关键码为key(key需要是整数,如果不是整数类型,则还需要通过某种规则将key转化为整数,例如将字符串按照某种规则转化为整数)。

除留余数法(division)

        取一个质数pp \leqslant size,则可以利用下面的公式将关键码转化为散列地址:

                                                        hash(key) = key % p

       其中,“%”是运算符中的整数除法取余的操作。当p接近于size并且为质数时,散列函数计算出的映射值冲突最小,为更进一步的减小冲突,p也应当避免为2或10的次幂。当然这不是硬性要求,如果不注重极致的效率,自己设计实现的散列表的p可以不为质数,也不管是否为2或10的次幂。为了简单介绍,下面出现的size就等于并代表p。

        举例: size = 10, key = 2233,那么2233通过散列函数映射到的地址offset = hash(2233) = 2233 % 10 = 3。

三、处理冲突的方法

(一)、闭散列(开地址法)

        闭散列也叫做开地址法开一段数组空间作为散列表(hash table),数组的元素类型即需要存储的元素的类型,该数组有size个存储地址,并将该数组头尾相连作为环状结构使用。当新插入一个元素时,用该元素的关键码key计算其存储的位置hashi,然后将该元素定位到散列表中的第hashi个位置上,如果hashi位置尚未存储有其他元素,那么直接将新的元素存储到hashi的位置。若hashi位置已经被其他元素占取了,此时发生了冲突,需要制定某种方案将新插入的元素存储到“下一个”空的位置中。

线性探查法解决哈希冲突

        下面详细讲闭散列中的一种解决哈希冲突的方法:线性探查法(linear probing)。还有其他的解决冲突方法:如二次探查法(quadratic probing)和双散列法,这里不着重介绍

插入元素       

        给出一组元素,它们的关键码分别为37,25,15,36,49,68,59,11,表的大小size = 10。散列函数使用除留余数法,hash(key) = key % 10。

这样可得每一个关键码对应的映射地址为:

关键码(key)

映射地址(hash(key))

37

7

25

5

15

5

36

6

49

9

68

8

59

9

11

1

将上述元素的关键码(为简单起见,键就是要存储的元素值)依次存储到散列表中,散列表中的变化如下: 

        需要加入一个新元素时,通过散列函数计算该元素存储的位置,如果存储的位置存在其他的元素, 则查看紧随其后的下一个位置,如果没有被占用,则将新元素存储到该位置,否则继续查看紧随其后的下一个位置,直到找到没有被占用的位置为止(若饶了数组一圈都没法找到空的位置,说明散列表满了),则插入失败(如果在满之前扩容就可以避免发生因为容量不足而插入失败的情况了)

散列情况统计:

关键码

37

25

15

36

49

68

59

11

映射位置

7

5

5

6

9

8

9

1

冲突位置

5

6,7

8,9

9,0

1

最后存入的位置

7

5

6

8

9

0

1

2

探查次数

1

1

2

3

1

3

3

2

查找元素

        将这些关键码存储到散列表中后,后续查找某元素时的查找次数和存入该元素时的探查次数相同,方式也一样。例如在散列表中搜索关键码为36的元素,先通过散列函数定位到6的位置,发现6号位置为15,不相等,则线性探测到下一号7,7号位置为37,不相等,则线性探测到下一号8,发现相等,此时查找到了关键码为36的元素。若探测到的位置存储的元素为空,则查找失败,该元素不在散列表中。

删除元素 

        需要注意,在闭散列的情形下不能随便物理删除散列表中已有的元素。因为若删除元素会影响其他元素的搜索。例如在上面查找关键码为36的元素例子前,把关键码为37的元素给物理删除了,7号位置为空,那么在查找36时,探测到7号位置,发现为空,此时程序为认为关键码为36的元素不在该散列表中,就发生了漏查的情况,解决方法是对删除的7号位置标记为deleted状态,当查找探测到deleted状态的位置时,继续往下探测,这样就不会发生漏查了

负载因子α

        引入负载因子α来讨论当前散列表是否需要进行扩容,负载因子α的计算方法为:设当前散列表存储的元素个数为n,散列表的大小为size,那么α = n / size。若用线性探查法解决冲突当α = 1时,说明此时散列表满了,不论如何都要进行扩容操作了。采用线性探查法的散列表一般在α 小于等于0.75前就应该扩容了,因为采用线性探查法的散列表存储过多元素会产生太多的“堆积”会导致该散列表的搜索效率过低。

搜索效率

        元素通过关键码可以映射定位到一个接近散列表中存储该元素的位置,能够从接近该元素存储位置的地方开始线性探测,而线性探查法容易产生“堆积(cluster)”问题,即某元素本该映射到的位置被其他的非映射到该位置的元素给占用了,某元素映射到的位置和实际存储的位置不相同,导致插入和搜索时所需要探测的序列长度变长,搜索时间增加。

简单代码实现(C++)

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

using namespace std;

//-----------------------------HashFunc仿函数----------------------------------------
// 描述:将键值转化为哈希值(就是将某类型K的关键码转化为整数的方法)
//-----------------------------------------------------------------------------------
template<class K>
class HashFunc
{
public:
	size_t operator() (const K& key)
	{
		return (size_t)key;	//对于可以转化为整形的数据直接强转为整形
	}
};

//对常用的string类型进行特化处理,转化为哈希值
template<>
class HashFunc<std::string>
{
public:
	size_t operator() (const std::string& str)
	{
		//进行特殊运算将字符串转化为整形映射
		//其他的转化方法参考https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html
		size_t hash = 0;
		for (auto& e : str)
		{
			hash += e;
			hash *= 31;	//每次加上一个ASCII码值再乘上31
		}
		return hash;
	}
};


//闭散列开放地址法
//解决哈希冲突的方法:线性探查法
namespace MySpace1
{
	//-----------------------------存储状态和存储节点的类型的定义------------------------
	// 
	//-----------------------------------------------------------------------------------
		//哈希表中每个位置的存储状态
	enum KindOfState
	{
		EMPTY,	//代表该位置为空
		EXIST,	//代表该位置存在着元素
		DELETED,
	};

	//哈希表的结点,数组的每一个元素为一个结点,里面存储了要存储的元素和该位置状态
	template<class T>
	struct HashNode
	{
	public:
		T _data;	//结点存储的数据
		KindOfState _state;	//该节点的状态

		HashNode(T data = T(), KindOfState state = EMPTY) :_data(data), _state(state) {  }
	};


	//----------------------------------哈希表的具体实现---------------------------------
	// 描述:博客上的描述和代码实现上可能有些出入。
	//-----------------------------------------------------------------------------------
	template<class T, class Hash = HashFunc<T>>
	class HashTable
	{
	private:
		typedef HashNode<T> Node;
		vector<Node> _table;	//哈希表
		size_t currentSize;	//当前哈希表实际存储的数据个数
	public:
		HashTable() :currentSize(0)
		{
			_table.resize(10);	//默认开十个数据大小
		}

		//插入
		bool insert(const T& key)
		{
			if (_table[find(key)]._state == EXIST)	//判断要插入的值是否已经存在,如果是则插入失败
				return false;

			if (currentSize * 100 / _table.size() >= 75)	//负载超过75%就扩容
			{
				//扩容和重新映射
				//两倍扩容
				HashTable newTable;
				newTable._table.resize(2 * _table.size());

				for (auto& e : _table)	//将原有数据插入到新表中重新计算映射位置
					newTable.insert(e._data);

				_table.swap(newTable._table);	//将新表交换给旧表,完成扩容工作

				//cout << "扩容到" << _table.size() << "个数据大小" << endl;
			}

			Hash hash;

			//除留余数法计算哈希值
			size_t hashi = hash(key) % _table.size();

			//找到空位存储key值
			while (_table[hashi]._state == EXIST)
			{
				hashi = (hashi + 1) % _table.size();
			}

			_table[hashi]._data = key;
			_table[hashi]._state = EXIST;

			currentSize++;	//存储的有效数据个数+1
			return true;

		}

		//删除
		bool erase(const T& key)
		{
			size_t hashi = find(key);
			if (_table[hashi]._state == EXIST)
			{
				_table[hashi]._state = DELETED;
                currentSize--;
				return true;
			}

			
			return false;
		}

		//查找,返回对应关键码的映射位置
		size_t find(const T& key) const
		{
			Hash hash;

			size_t hashi = hash(key) % _table.size();

			while (_table[hashi]._state != EMPTY)	//不为空则往后找
			{
				if (_table[hashi]._state == EXIST && _table[hashi]._data == key)	//存在且与key相等
					return hashi;

				hashi = (hashi + 1) % _table.size();
			}

			//找不到
			return hashi;
		}
	};

	//--------------------------------测试代码------------------------------------
	// 描述:需要在调试窗口看具体的存储情况
	//----------------------------------------------------------------------------
	void test1()
	{
		MySpace1::HashTable<int> hashTable;

		int a[] = { 11,21,4,14,24,15,9 ,55,66,99,87,15,56,61,32,14,14,16 };

		for (auto e : a)
		{
			hashTable.insert(e);
		}

		hashTable.erase(15);

		size_t hash = hashTable.find(55);

        for (auto e : a)
        {
        	hashTable.erase(e);
        }
	}

	void test2()
	{
		HashTable<string> hashTable;

		hashTable.insert("insert");
		hashTable.insert("sort");
		hashTable.insert("good");
		hashTable.insert("oodg");
		hashTable.insert("rtos");
		hashTable.insert("sertin");
		hashTable.insert("dog");
		hashTable.insert("cat");
		hashTable.insert("bird");

		hashTable.erase("good");
		size_t i = hashTable.find("good");
	}
};

(二)、开散列 (链地址法)

        开散列也叫做链地址法,开一段数组空间作为散列表(hash table),数组的元素类型为指向一个链表头结点的指针类型(若链表为空,则置空)。散列表中的每一个元素都是一个指针,指向不同的链表,每一个链表都为一个,该结构又叫做哈希桶。当不同的关键码通过同一个散列函数计算出相同的存储位置hashi时,只需将这些互为同义词的元素存储到第hashi位置的桶上,也就是存储在同一个链表上,每一个桶为同义词的集合

插入元素

        给出一组元素,它们的关键码分别为37,25,15,36,49,68,59,11表的大小size = 10。散列函数使用除留余数法,hash(key) = key % 10。

这样可得每一个关键码对应的映射地址为:

关键码(key)

映射地址(hash(key))

37

7

25

5

15

5

36

6

49

9

68

8

59

9

11

1

将上述元素的关键码(为简单起见,键就是要存储的元素值)依次存储到散列表中,散列表中的变化如下: 

查找和删除元素

        查找:待查找的元素x的关键码通过散列函数找到对应的桶号,然后在桶内通过遍历链表的方式依次将遍历的元素和待查找的元素x进行比对即可。

        删除:待删除的元素x的关键码通过散列函数找到对应的桶号,执行像链表一样删除某结点的操作即可。

负载因子α

        引入负载因子α来讨论当前散列表是否需要进行扩容,负载因子α的计算方法为:设当前散列表存储的元素个数为n,散列表的大小为size,那么α = n / size。若用开散列的哈希桶结构解决冲突当α = 1时,建议进行扩容操作,因为当α = 1时,说明n = size,在非常理想的情况下,每个桶的结点都均等分,即哈希表中的所有桶都存储有结点,而且每个桶只有一个结点,这个时候对哈希表进行扩容,可以控制每个桶的长度不会过长,保证搜索效率。当存储的元素非常多时,要追求更高效率,可以在桶的长度超过8时,考虑使用红黑树结构存储同义词的集合,而不是桶结构,保证搜索的时间复杂度为常数级别

搜索效率

        在冲突不多,每个桶存储结点的平均长度不长的时候(必要时可考虑使用红黑树结构代替桶),搜索效率高,时间复杂度可以达到常数级O(1)

        在这个例子中,搜索成功的平均搜索长度为(1 + 1 + 2 + 1 + 1 + 1 + 1 + 2 ) / 8 = 1.25

搜索失败的平均搜索长度为(1 + 2 + 1 + 1 + 1 + 3 + 2 + 2 + 2 + 3) / 10 = 1.8

简单代码实现(C++)

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

using namespace std;

//-----------------------------HashFunc仿函数----------------------------------------
// 描述:将键值转化为哈希值(就是将某类型K的关键码转化为整数的方法)
//-----------------------------------------------------------------------------------
template<class K>
class HashFunc
{
public:
	size_t operator() (const K& key)
	{
		return (size_t)key;	//对于可以转化为整形的数据直接强转为整形
	}
};

//对常用的string类型进行特化处理,转化为哈希值
template<>
class HashFunc<std::string>
{
public:
	size_t operator() (const std::string& str)
	{
		//其他的转化方法参考字符串Hash函数
		size_t hash = 0;
		for (auto& e : str)
		{
			hash += e;
			hash *= 31;	//每次加上一个ASCII码值再乘上31
		}
		return hash;
	}
};

namespace MySpace2
{
	//桶的结点定义
	template<class T>
	struct HashBucketNode
	{
		T _data;
		HashBucketNode* next;	//指向下一个结点,如果为空,则代表尾结点

		HashBucketNode(const T& data) :_data(data), next(nullptr) {  }
	};

	//散列表的具体实现
	template<class T, class Hash = HashFunc<T>>
	class HashTable
	{
	private:
		typedef HashBucketNode<T> Node;	
		vector<Node*> _table;	//散列表为指针数组,每个指针指向一个桶
		size_t currentSize;	//存储的有效数据个数
	public:
		HashTable() :currentSize(0)
		{
			_table.resize(10, nullptr);	//默认开10个桶的大小,都为空
		}

		~HashTable()	//析构函数,释放所有桶的结点
		{
			for (auto& node : _table)
			{
				Node* pCur = node;
				Node* pNext = nullptr;
				while (pCur)
				{
					pNext = pCur->next;
					delete pCur;
					pCur = pNext;
				}
				node = nullptr;
			}
		}

		//插入
		bool insert(const T& value)
		{

			if (find(value))	//如果要插入的元素已经存在,插入失败				
				return false;

			if (currentSize >= _table.size())	//如果当前数据个数等于桶的个数,就扩容
			{
				vector<Node*> newTable(_table.size() * 2, nullptr);

				for (auto& node : _table)	//将每个桶里的结点重新映射,将现有节点移动到新的桶中,而不应是复制结点。
				{
					if (node)	//该桶不为空
					{
						Node* pNext = nullptr;
						Node* pCur = node;
						size_t hashi;
						Hash hash;

						while (pCur)
						{
							pNext = pCur->next;

							hashi = hash(pCur->_data) % newTable.size();	//计算映射值

							//头插到新的桶中
							pCur->next = newTable[hashi];
							newTable[hashi] = pCur;

							pCur = pNext;
						}
					}
				}
				//旧表和新表交换
				_table.swap(newTable);
			}

			Hash hash;
			size_t hashi = hash(value) % _table.size();

			//将元素头插到桶上
			Node* newNode = new Node(value);
			newNode->next = _table[hashi];
			_table[hashi] = newNode;	//成为新的头结点

			currentSize++;	//结点的数量增加
			return true;
		}

		//删除
		bool erase(const T& key)
		{
			Hash hash;
			size_t hashi = hash(key) % _table.size();

			Node* pPrev = nullptr;	//待删除节点的上一个结点
			Node* pCur = _table[hashi];	//到对应的桶中找对应结点

			while (pCur)
			{
				if (pCur->_data == key)
					break;

				pPrev = pCur;
				pCur = pCur->next;
			}

			//如果不存在该结点
			if (!pCur)
				return false;

			//如果删除的是头结点
			if (pPrev == nullptr)
			{
				_table[hashi] = pCur->next;
				delete pCur;

			}
			else
			{
				pPrev->next = pCur->next;
				delete pCur;
			}

			currentSize--;
			return true;
		}

		//查找
		Node* find(const T& key)
		{
			Hash hash;
			size_t hashi = hash(key) % _table.size();

			Node* pCur = _table[hashi];	//到对应的桶中找对应结点

			while (pCur)
			{
				if (pCur->_data == key)
					return pCur;

				pCur = pCur->next;
			}

			//找不到
			return	nullptr;
		}
	};

	void test1()
	{
		HashTable<int> hsTable;

		int a[] = { 37,25,15,36,49,68,59,25,15,36,49};

		
		for (auto e : a)	//依次添加案例元素
		{
			hsTable.insert(e);
		}

		for (auto e : a)	//测试依次删除所有元素
		{
			hsTable.erase(e);
		}
	}
};

       

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

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

相关文章

【优选算法之前缀和】No.6--- 经典前缀和算法

文章目录 前言一、前缀和例题模板&#xff1a;1.1 【模板】前缀和1.2 【模板】⼆维前缀和1.3 寻找数组的中⼼下标1.4 除⾃⾝以外数组的乘积1.5 和为 K 的⼦数组1.6 和可被 K 整除的⼦数组1.7 连续数组1.8 矩阵区域和 前言 &#x1f467;个人主页&#xff1a;小沈YO. &#x1f6…

提升晶振电路抗扰性:优化方案解析

在现代电子设备中&#xff0c;晶振作为提供稳定时钟信号的核心组件&#xff0c;其稳定性对整个系统的运行至关重要。然而&#xff0c;电路抗扰性不良往往会导致晶振失效&#xff0c;进而影响设备的整体性能。晶发电子针对这一问题&#xff0c;提出了以下关于晶振电路抗扰性及优…

【C++】拆分详解 - string类

文章目录 一、为什么学习string类&#xff1f;二、标准库中的string类  1. 定义  2. 常用接口说明     2.1 构造     2.2 容量操作     2.3 访问及遍历操作     2.4 修改操作     2.5 非成员函数 三、OJ练习自测  [1. 仅仅反转字母](https://leetcod…

9.23 My_string.cpp

my_string.h #ifndef MY_STRING_H #define MY_STRING_H#include <iostream> #include <cstring>using namespace std;class My_string { private:char *ptr; //指向字符数组的指针int size; //字符串的最大容量int len; //字符串当前…

华为三折叠一拆,苹果脸被打肿了!

文&#xff5c;琥珀食酒社 作者 | 随风 哎呀 苹果这次脸真是被华为狠狠打肿了 那些高高兴兴买iPhone 16的 东西一收到&#xff0c;脸马上就绿了啊 各种意想不到的问题啊 拆开手机后发现有两处掉漆咱就不说了 第一次滑动iPhone 16 Pro屏幕有响应 再滑动就没有响应了咱也…

【27】C++项目练习

练习1 题目如下 代码如下 .h #pragma once #include <string> using namespace std;class Toy { public:Toy();Toy(string name,int price,string place);~Toy();string getName() const;int getPrice() const;string getPlace() const;void changePrice(float count)…

自己开发的windows服务在虚拟机上不能正常启用

最近开发了个数据采集系统&#xff0c;在我本机上发布、安装是没有问题的&#xff1b;但是在虚拟机上进行安装部署的时候&#xff0c;反馈的错误码是1053&#xff0c;服务不能正常启动。 网上搜索可能的原因&#xff0c;如图&#xff1a; 能引起1053的问题比较多&#xff0c;首…

springboot实战学习笔记(4)(Spring Validation参数校验框架、全局异常处理器)

接着上篇博客学习。上篇博客是已经基本完成用户模块的注册接口的开发。springboot实战学习笔记&#xff08;3&#xff09;(Lombok插件、postman测试工具、MD5加密算法、post请求、接口文档、注解、如何在IDEA中设置层级显示包结构、显示接口中的方法)-CSDN博客本篇博客主要是关…

最新版Visual Studio安装教程(超详细,新手必看)

一、官网下载 这里奉上Visual Studio官方下载地址&#xff1a; https://visualstudio.microsoft.com/zh-hans/downloads/https://visualstudio.microsoft.com/zh-hans/downloads/ 对于我们学习来说&#xff0c;下载第一个社区免费版即可&#xff0c;点击下载。 下载完成以后是…

Kubernetes Pod调度基础(kubernetes)

实验环境依旧是k8s快照&#xff0c;拉取本次实验所需的镜像文件&#xff1b; 然后在master节点上传已经编写好的yaml文件&#xff1b; 然后同步会话&#xff0c;导入镜像&#xff1b; pod控制器&#xff1a; 标签选择器--》标签&#xff1a; 标签&#xff1a; 在Kubernetes&…

还在用windows自带录屏?试试这三款录屏工具

作为一名办公室文员&#xff0c;我经常需要录制电脑屏幕来制作教程或者记录工作流程。在众多的录屏工具中&#xff0c;我尝试了四款不同的录屏工具&#xff0c;包括Windows自带录屏工具。今天&#xff0c;我就来跟大家分享一下我的使用体验&#xff0c;希望能帮助到和我有同样需…

利用代码,玩转腾讯云脱敏服务:Java、Python、PHP案例集

腾讯云数据脱敏服务-数据管理的优势是什么&#xff1f; 腾讯云数据脱敏服务-数据管理 提供了一种高效且灵活的方式来保护敏感数据。其核心优势在于可以在数据处理和传输过程中自动化地执行数据脱敏操作。无论是脱敏信用卡号、身份证号还是其他个人信息&#xff0c;该服务都能精…

Games101笔记-二维Transform变换(二)

1、什么是Transform Transform就是通过一个矩阵&#xff0c;进行缩放、旋转、平移等变换 2、缩放、旋转、切变、平移等基础变换 缩放变换&#xff1a; 反射变换&#xff1a; 切变&#xff1a; 绕原点旋转&#xff1a; 以上都是线性变换&#xff1a; 平移变换&#xf…

线程同步:消费者模型(非常重要的模型)

一.线程同步的概念 线程同步&#xff1a;是指在互斥的基础上&#xff0c;通过其它机制实现访问者对 资源的有序访问。条件变量&#xff1a;线程库提供的专门针对线程同步的机制线程同步比较典型的应用场合就是 生产者与消费者 二、生产者与消费者模型原理 在这个模型中&…

中文文本分词-技术实现

当做语音&文本相关的技术时&#xff0c;经常会涉及到文本的分词实现。以下是对中文的文本简单实现。 一、单个中文句子的分词 import jiebatext_ "我爱我的祖国&#xff01;" # 精确模式 seg_list jieba.cut(text_, cut_allFalse) print("精确模式: &qu…

【51实物与仿真】基于51单片机设计的波形/函数发生器(正弦波、锯齿波、三角波、矩形波,设定频率步进值,改变振幅,LCD显示)——文末完整资料链接

基于51单片机设计的波形函数发生器 演示视频: 功能简介: 1.本设计基于STC89C51/52(与AT89S51/52、AT89C51/52通用,可任选)单片机。 2.LCD1602液晶显示波形种类和频率值(10-100HZ)。 3.按键设置波形种类和设定频率步进值。 4.电位器器改变振幅(0V-3.5V稳定)。 5…

医院预约|基于springBoot的医院预约挂号系统设计与实现(附项目源码+论文+数据库)

私信或留言即免费送开题报告和任务书&#xff08;可指定任意题目&#xff09; 目录 一、摘要 二、相关技术 三、系统设计 四、数据库设计 五、核心代码 六、论文参考 七、源码获取 一、摘要 近年来&#xff0c;信息化管理行业的不断兴起&#xff0c;使得人们的日…

集合根据上下级关系转树结构

1、创建实体对象 public class TreeNode {private String id;private String pid;private String name;private List<TreeNode> children;public TreeNode(String id,String pid,String name){this.id id;this.pid pid;this.name name;}public String getId() {retur…

独立游戏《Project:Survival》UE5C++开发日志0——游戏介绍

该游戏是《星尘异变》团队的下一款作品&#xff0c;太空科幻题材的生存游戏&#xff0c;我将负责使用C、蓝图实现游戏的基础框架和核心功能&#xff0c;其中还包含使用人工智能算法助力游戏开发或帮助玩家运营 目前已有功能&#xff1a; 1.3D库存系统&#xff1a;所有库存中的物…

Python练习宝典:Day 3 - 选择题 - 字符串与正则表达式、基础练习补充

目录 一、基础练习补充二、字符串与正则表达式 一、基础练习补充 1.下列能返回变量s的数据类型的是: A.print(type(s)) B.print(s) C.print(int(s)) D.print(str(s))2.如果想要换行,可以使用: A.\ B.\n C.\t D.\f3.合法的标识符是: A.M-N B.and C.Dior_Ysl D.6_friends4.在…