哈希表(底层结构剖析-- 上)

news2024/11/19 7:49:31

文章目录

  • 哈希表底层结构剖析
    • 哈希概念
    • 哈希冲突
    • 哈希函数
  • 哈希冲突解决办法
    • 闭散列( 线性探测 + 二次探测)
    • 开散列
  • 哈希表闭散列方法的模拟实现
    • 基本框架
    • 有关哈希数据的类
    • 插入函数
    • 删除函数
    • 查找函数
    • 增加仿函数将所有数据类型转换为整型
  • 哈希表开散列方法的模拟实现(增加仿函数版)

哈希表底层结构剖析

哈希概念

1: 在顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系.因此,在查找一个元素时,必须要经过关键码的多次比较.我们知道顺序表查找的时间复杂度为0(N),平衡树中的查找的时间复杂度则为树的高度,即O(log2N),此时,搜索的效率取决于搜索过程中元素的比较次数.

2: 那么理想的搜索方法为:可以不经过比较,一次直接从表中得到要搜索的元素,如果构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素.

3:哈希表中插入,查找函数方法:
插入元素 : 根据待插入元素的关键码,以此函数计算该元素的存储位置并按照此位置进行存放.

搜索元素:
对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在此存储结构中按此位置取元素关键码比较,如果关键码相等,则查找成功.

综合以上说法: 其中哈希方法中使用的某种转换函数为哈希(散列)函数,构造出来的结构称为哈希表(又称为散列表)

例如;数据集合{ 1,7,6,4,5,9 };
哈希函数设置为: hash(key) = key % capacity. 其中capacity为存储元素底层的空间总的大小.
图示如下:
在这里插入图片描述

哈希冲突

哈希冲突即: 不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为哈希冲突. 把具有不同关键码而具有相同哈希地址的数据元素称为"同义词.

哈希函数

引起哈希冲突的一个原因可能是: 哈希函数的设计不够合理.
常见哈希函数:
1: 直接定址法(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
在这里插入图片描述

2:除留余数法(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址.(其中p等于容器大小):
图示如下:
在这里插入图片描述
但是,以上方法也容易引起哈希冲突.

哈希冲突解决办法

解决哈希冲突的两种常见方法为: 闭散列和开散列

.

闭散列( 线性探测 + 二次探测)

线性探测:从发生冲突的位置,依次向后探测,直到寻找到下一个空位置为止.

插入函数:
(1): 通过哈希函数获取待插入元素在哈希表中的位置.
(2): 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,则使用线性探测找到下一个空位置,插入新元素.
例如:
在下列数据集合 : { 1,4,5,6,7,44,9) 中,元素44与4的位置发生哈希冲突,那么便从4的位置后面依次寻找空的位置,直到位置8中.
在这里插入图片描述
线性探测缺点:
线性探测在某个位置冲突很多的情况下,数据之间会互相占用位置,从而导致冲突一片.
例如:
在以下数据集合中,11,21,31与1发生哈希冲突,只能在1的后续空位置中插入元素,但是11占用了2的位置,又导致11与2之间发生哈希冲突,而2又是占用了其他元素的位置,有可能引发别的元素之间的哈希冲突.
在这里插入图片描述

面对线性探测的缺点,有人提出了二次探测:
二次探测是由冲突位置开始,每次累加计算i^2(i>=0),这样就会将连续冲突互相占用位置的情况减弱,和线性探测相比,数据的位置有效分开了.

在这里插入图片描述

但是,如果当我们在2的后面添加上12,此时 hash = 12 % 10 = 2,
12经过线性探测后在下标为6的位置,hash +i = 2 + 4 = 6.
经过二次探测后在 hash + i^2 = 2 + 2^2 = 6,
所以此时12应该在下标为6的位置.

在这里插入图片描述

开散列

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

哈希桶的产生正是为了有效解决闭散列的占用相互影响导致连续冲突的问题.

在这里插入图片描述

哈希表闭散列方法的模拟实现

基本框架

哈希表主要由2个内置成员:
1:存储有关哈希结点类的vector.
2: 记录哈希表中的有效元素个数_size;


//闭散列哈希表
template<class K, class V>
class HashTable
{
public:
	//...
private:
	vector<Data> _table; //哈希表
	size_t _n = 0; //哈希表中的有效元素个数
};

vector _table中就有一个_size,为什么哈希表中还额外需要一个_size?

因为删除为伪删除,并非将数据真正删除了,所以vector中的_size还包括了删除过的数据,而哈希表中的size实际上才真正代表哈希表中存储的有效数据个数.

有关哈希数据的类

哈希表中存储的正式该数据类型,主要存储2个内置类型:
1: KV结构的键值对.
2: 标明状态的枚举类型 (状态标志位)

enum State
{
	EXIST,
	DELETE,
	EMPTY,
};
template <class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

为什么要额外添加标志位呢?

例如:
当我们删除3,再去寻找20时,通过线性探测由下标为2的位置开始寻找,
如果找的有数据但是 不是20,就从下一个位置继续寻找.
如果找到的位置为0,此时说明该位置为空,就说明找不到了,因为根据线性探测是根据哈希函数找到对应位置,如果哈希冲突,就从对应位置开始寻找到空位置插入的.

综合以上情况 : 当我们删除3,该位置数据就变成了0或者-1(删除一般为伪删除),此时寻找的时候根据哈希函数正好是从下标为2的位置开始寻找,此时便会直接返回空,进而导致3的后面明明有20,却找不到20的情况,我们也可以从开始全部找一遍,可是这也失去了哈希表的优势.

在这里插入图片描述
有了标志位,当我们将3删除的时候,就将元素3的位置状态标为DELETE,当我们要查找20的时候,从3的位置开始查找时碰到DELETE就不停下来,继续往后查找.并且再次插入22时还可以在3的位置插入.

标志位的意义:
EMPTY的标志标明查找时停止的标志,插入时可以在该位置处放值.
DELETE的标志标明查找时不停止,插入时可以在该位置处放值.
EXIST的标志标明查找时不停止,插入时不可以在该位置放值.

插入函数

插入函数主要分为3个步骤:
1: 复用find函数查找数据是否冗余,如果哈希表中有相同数据就不用插入了.

2:如果没有超过负载因子,查找空位置插入数据.

3: 扩容.

下面根据2,3两个个步骤主要展开描述:
2: 如果没有超过负载因子,查找空位置插入新数据.
不扩容插入新数据分为2个步骤:
1: 根据哈希函数计算对应位置,必须%_table.size(),不能除以_table.capacity(),因为在顺序表中[]中只能在size范围内访问,%size()能够保证hashi而在size范围之内.
如果%_table.capacity(),那么hashi的位置有可能在size范围之外,这样在之后的[操作中会导致assert断言错误.

2: 线性探测删除标志或者空标志的位置,为了如果从标志位开始找到哈希表的尾端都没找到,为了防止在size范围之外,我们可以将hashi % size(),让它能够归零寻找.
如果找到,则将数据插入,并将_size+1,状态改为存在.

template < class K, class V>
class HashTable
{
public:
	bool Insert(const pair<K, V>& kv)
	{
size_t start = kv.first % _table.size(); 

		while ( _table[hashi]._state == EXIST )   
		{
			++hashi;
			hashi %= _table.size();    
		}
		_table[hashi]._kv = kv;              //目标位置的存储对象的_kv就等于要插入kv的键值.
		_table[hashi]._state = EXIST;        //将目标位置存储对象的状态修改为EXIST.
		++_size;
		return true;

3: 扩容.

哈希表什么情况下扩容? 如何扩容?

负载因子: 散列表的荷载因子(负载因子)定义为: a = 填入表中的元素个数 / 散列表的长度.

其中,a代表散列表装满程度的标志因子,由于表长是定值.
a越大,表明填入表中的元素越多,产生哈希冲突的可能性就越大.
a越小,表明填入表中的元素越少,产生哈希冲突的可能性就越小.

对于开放定址法,负载因子是特别重要元素,应严格限制在0.7-0.8以下.
所以,在扩容时我们将负载因子定为0.7,一旦大于等于0.7就扩容.

扩容主要分为3个步骤:
1: 确定新表长度,创建新表.

2: 将旧表数据填入到新表当中.

3: 调用vector中的swap,将旧表与新表交换.(现代写法)

插入函数线性探测版:

bool Insert(const pair<K, V>& kv)
	{
		Hash hash;
		if ( Find(kv.first ))         
		{
			return true;
		}
		
		//如果分子为7,分母为10,结果却等于0,所以为了避免这种情况,我们可以让分子乘以10,结果再乘以10.
		if (_table.size() == 0 || 10 * _size / _table.size() >= 7) //1:		
		{

			size_t newSize = _table.size() == 0 ? 10 : 2 * _table.size();
			HashTable<K, V> newTable;
			newTable._table.resize(newSize);
			for (auto& e : _table)        //遍历旧表,依次将旧表的数据放入到新表当中.  
			{
				if (e._state == EXIST)
				{
					newTable.Insert(e._kv);  
				}
			}
			_table.swap(newTable._table);  //现代写法.

		}
		size_t hashi = hash(kv.first) % _table.size(); 

		while ( _table[hashi]._state == EXIST )  
		{                    
			
			hashi++;

			hashi %= _table.size();          //此时不能超过size范围,需要将hashi回到起点重新开始寻找,直到找到空位置.
		}
		_table[hashi]._kv = kv;           
		_table[hashi]._state = EXIST;      
		++_size;
		return true;
	}

插入函数二次探测版:
针对于二次探测,我们要提前记录哈希映射位置,在探测时都是在这个位置的基础上+i*i的.

bool Insert(const pair<K, V>& kv)
	{
		Hash hash;
		if ( Find(kv.first ))         
		{
			return true;
		}
		
	
		if (_table.size() == 0 || 10 * _size / _table.size() >= 7) 
		{
			size_t newSize = _table.size() == 0 ? 10 : 2 * _table.size();
			HashTable<K, V> newTable;
			newTable._table.resize(newSize);
			for (auto& e : _table)        
			{
				if (e._state == EXIST)
				{
					newTable.Insert(e._kv);   
				}
			}
			_table.swap(newTable._table);  //现代写法.

		}
		size_t start = hash(kv.first) % _table.size(); 

		size_t hashi = start;    //二次查找,记录哈希映射位置.

		size_t i = 0;


		while ( _table[hashi]._state == EXIST )  
		{
			++i;                                     
			
			hashi = start + i * i;     //每次都是从哈希映射位置处+i*i.

			hashi %= _table.size();          
		}
		_table[hashi]._kv = kv;              
		_table[hashi]._state = EXIST;        
		++_size;
		return true;
	}

注意:
1:负载因子超过0.7就扩容,这样就说明哈希表永远只能存储百分之70的位置,这样就不会在hashi归零时寻找一直找不到位置,防止造成死循环问题.
2: 如果最开始表的数据个数为0的,也要进行扩容,否则刚开始哈希表_size为0,防止发生除0错误.
3:将旧表数据填入新表时,我们可以调用Insert函数过程中,因为哈希表的容量已经开辟好了,肯定不会扩容.所以编译器只会执行if之后的语句,重新计算目标数据要插入的位置,这样就服用了insert部分代码.
如果重新创建_table,然后将旧表的数据放入到新表的化,因为新表的长度原来在旧表中冲突的数据在新表中就不冲突了,原来在旧表中不冲突的位置,在新表中反而冲突,
这样在每次将旧表数据放入新表中都要重新计算插入位置.

删除函数

哈希表中的删除实际上是伪删除,主要分为3个步骤:
1: 查找哈希表中是否有目标数据.

2: 将要删除的数据中的状态改为DELETE.

3: 如果删除成功就将哈希表的数据个数-1,返回true,如果失败返回false;

bool Erase(const K& key)                  
	{
		HashData<K,V>* ret = Find(key);   //查找哈希表中是否有删除数据.
		if (ret)
		{
			ret->_state = DELETE;
			--_size;                     
			return true;
		}
		return false;
	}

查找函数

哈希表中查找函数主要分为2个步骤:
1: 判断表是否为空,防止哈希函数计算时发生除零错误.

2: 通过哈希函数计算数据查找开始的位置,我们要查找的数据必须符合状态为EXIST并且存储数据等于目标数据条件,如果找到空位置就说明找不到了,就退出循环.

在这里插入图片描述


	HashData<K,V>* Find(const K& key)                  
	{
		if (_table.size() == 0)                //如果表为空,就会发发生除零错误,所以必须要先判断.
		{
			return nullptr;
		}
		size_t hashi = key % _table.size();   //左边是无符号整数,右边是有符号整数,相除时右边的类型会发生整型提升,提升为无符号整数,然后算出的这个位置自然就为4了.
		                           


		size_t start = hashi;

		while (_table[hashi]._state != EMPTY )  //哈希映射的那个位置不为空就继续查找.
		{
			if (_table[hashi]._state != DELETE && _table[hashi]._kv.first == key)  //当映射的那个位置不为空并且状态也不能是删除,_kv.first等于那个值就找到了.
			{
				return &_table[hashi];
			}
			++hashi;                                 //哈希映射的那个位置不为空就继续查找.
			hashi %= _table.size();              //归零,防止越界.
			
			if (hashi == start)
			{
				break;
			}
		}
		return nullptr;
		
	}

注意:
1: 考虑到哈希表有可能经过多次删除插入,造成哈希表中没有空的位置,所以当查了一圈,还没有找到时,应该退出循环,表明找不到了.

增加仿函数将所有数据类型转换为整型

如果当我们向哈希表中插入如string数据类型时,插入函数使用哈希函数计算插入位置时string数据类型是不能取模的.并且凡是使用哈希函数的地方我们都要使用仿函数.
为此,为了哈希表支持更多数据类型,我们增加了仿函数.
1: 对于数据类型为K(指针,整数),我们把它转换为无符号整数.

2: 对于字符类型,我们将字符串中每个字母转换为ASCII码相加.得到这个字符串的ASCII码之和,此时字符串类型就转换成了无符号整数类型.

可是,如果当我们所传的字符串不同,但是字符串所组成的ASCII码却相同,此时也容易导致哈希冲突.

例如字符串 : “abcd” 和 “bcad”.
在这里插入图片描述
此时:
我们便可以采用BKDR算法,在val每次累加之前都乘以固定值131.

仿函数代码如下:

template < class K >                   //仿函数的默认值,如果不显示传就会默认调用.
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template< >                                 //1:对于常见类型,为了方便,我们可以对类模板进行特化.并且根据实参所传类型,优先走特化版本.
struct HashFunc <string>                 //2: 函数特化针对的是第一个类模板.,所以类模板名及其模板参数类型必须相同,只不过后面将模板参数显示写出来并使用了而已.
	                                     
{
	size_t operator()(const string& key)
	{
		size_t  val = 0;
		for ( auto& ch : key ) //遍历string,将一个个字母替换成ASCI码进行相加.
		{ 
		    val *= 131;        //相加之前,val每次乘以131.
			val += ch;
		}
		return val;
	}
};
//哈希表
template < class K, class V,class Hash = HashFunc<K> >  //1:仿函数实则是一个类调用operator()重载,实现函数的功能.
	                                      // 2:仿函数模板跟平常类模板一样,需要那个仿函数,使用时就将哪个仿函数带上模板参数类型传过去.

class HashTable
{
public:
	//...
private:
	vector<Data> _table; //哈希表
	size_t _n = 0; //哈希表中的有效元素个数
};

注意:

哈希表开散列方法的模拟实现(增加仿函数版)

include <stdio.h>
#include <iostream>
#include <map>
#include <set>
#include <vector>
using namespace std;
enum State
{
	EXIST,
	DELETE,
	EMPTY,
};
template <class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

template < class K >                   //仿函数的默认值,如果不显示传就会默认调用.
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template< >                                 //1:对于常见类型,为了方便,我们可以对类模板进行特化.
struct HashFunc <string>                    //并且根据实参所传类型,优先走特化版本.
	                                        //2: 函数特化针对的是第一个类模板.,所以类模板名及其模板参数类型必须相同,只不过后面将
	                                        //模板参数显示写出来并使用了而已.
	                                        
	                                       //2: 仿函数实则是一个类调用operator()重载,实现函数的功能.
	                                      // 仿函数模板跟平常类模板一样,需要那个仿函数,使用时就将哪个仿函数带上模板参数类型传过去.
	                                     
{
	size_t operator()(const string& key)
	{
		size_t  val = 0;
		for ( auto& ch : key ) //遍历string,将一个个字母替换成ASCI码进行相加.
		{ 
		    val *= 131;
			val += ch;
		}
		return val;
	}
};

template < class K, class V,class Hash = HashFunc<K> >
class HashTable
{
public:
	bool Insert(const pair<K, V>& kv)
	{
		Hash hash;
		if ( Find(kv.first ))         //防止数据冗余,如果哈希表中有数据已经存在的话就不需要再插入了.
		{
			return true;
		}
		
		//2:如果分子为7,分母为10,结果却等于0,所以为了避免这种情况,我们可以让分子乘以10,结果再乘以10.
		if (_table.size() == 0 || 10 * _size / _table.size() >= 7) //1:负载因子超过0.7就扩容,这样就说明哈希表永远只能存储百分之70的位置,这样就不会在hashi归零时一直找不到位置,防止造成死循环问题.
		//3:如果最开始表的数据个数为0的,也要进行扩容.
		{

			size_t newSize = _table.size() == 0 ? 10 : 2 * _table.size();
			HashTable<K, V> newTable;
			newTable._table.resize(newSize);
			for (auto& e : _table)        //遍历旧表,依次将旧表的数据放入到新表当中.  
			{
				if (e._state == EXIST)
				{
					newTable.Insert(e._kv);   //1:调用Insert函数过程中,因为哈希表的容量已经开辟好了,肯定不会扩容.
										  //所以编译器只会执行if之后的语句,重新计算目标数据要插入的位置, 
										  //并采用线性探测方式寻找到新位置进行插入

										  //2:如果重新创建_table,然后将旧表的数据放入到新表的化,因为新表的长度
										 //不同,原来在旧表中冲突的数据在新表中就不冲突了,原来在旧表中不冲突的位置
										 //反而会在星表中冲突,这样每次将旧表数据放入到新表中都要重新计算对应位置,
										 //这样反而就会显得很麻烦.
				}
			}
			_table.swap(newTable._table);  //现代写法.

		}
		size_t start = hash(kv.first) % _table.size(); //必须%size(),因为在顺序表中[]只能在size范围内访问,如果%capacity,
												 //那么访问时则在capacity范围访问,这样有可能[]时大于size范围造成断言错误.

		size_t hashi = start;

		size_t i = 0;


		while ( _table[hashi]._state == EXIST )   //线性探测,如果这个位置存在,那么就需要继续从这个位置遍历. 如果这个位置                                         //为空或者删除状态,那么就可以将数据插入了.
		{
			++i;                              
			
			hashi = start + i * i;

			hashi %= _table.size();          //如果等于size的位置,就说明从这个位置开始找到后面还没有找到,此时不能超过size范围,需要
											  //将hashi回到起点重新开始寻找,直到找到空位置.
		}
		_table[hashi]._kv = kv;              //目标位置的存储对象的_kv就等于要插入kv的键值.
		_table[hashi]._state = EXIST;        //将目标位置存储对象的状态修改为EXIST.
		++_size;
		return true;
	}

	HashData<K,V>* Find(const K& key)                  
	{
		Hash hash;
		if (_table.size() == 0)                //如果表为空,就会发发生除零错误,所以必须要先判断.
		{
			return nullptr;
		}
		size_t hashi = hash(key) % _table.size();   //左边是无符号整数,右边是有符号整数,相除时右边的类型会发生整型提升,提升为无符号整数,然后算出的这个位置自然就为4了.
		                                       //所以就不需要关注负数了,因为它能存入这个表中.


		size_t start = hashi;

		while (_table[hashi]._state != EMPTY )  //哈希映射的那个位置不为空就继续查找.
		{
			if (_table[hashi]._state != DELETE && _table[hashi]._kv.first == key)  //当映射的那个位置不为空并且状态也不能是删除,_kv.first等于那个值就找到了.
			{
				return &_table[hashi];
			}
			++hashi;                                 //哈希映射的那个位置不为空就继续查找.
			hashi %= _table.size();              //归零,防止越界.
			
			if (hashi == start)
			{
				break;
			}
		}
		return nullptr;
		
	}
	

bool Erase(const K& key)                  //删除一个值,直接改哈希表中存储对应key地Data对象中的状态就行了.
	{
		HashData<K,V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_size;
			return true;
		}
		return false;
	}
void Print()
{
	for (int i = 0; i < _table.size(); ++i)
	{
		if (_table[i]._state == EXIST)
		{
			cout <<_table[i]._kv.first << "->" << _table[i]._kv.second<<" ";
		}
		
	}
	
}
private:

	vector<HashData<K, V>> _table;
	size_t _size;    //记录有效数据个数
};

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

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

相关文章

Visual Studio的安装注意

本文目前包含的内容&#xff1a; Visual Studio版本选择工作负载组建的选择安装后的环境配置 目录 1. 安装前1. 安装过程中2. 安装后注意 1. 安装前 安装版本选择&#xff1a; 一定不要选最新的版本&#xff0c;选3-5年前的稳定版本&#xff01;新版本会出很多bug。 1. 安装过…

银行数字化转型导师坚鹏:银行数字化转型创新与应用前沿

银行数字化转型创新与应用前沿 ——金融科技如何赋能银行数字化转型 课程背景&#xff1a; 数字化背景下&#xff0c;很多银行存在以下问题&#xff1a; 不清楚5G如何赋能银行数字化转型&#xff1f; 不清楚最新金融科技如何赋能银行数字化转型&#xff1f; 不了解银行…

自动驾驶落地如何降本?这家头部公司有自己的独特之处

一直以来&#xff0c;商用车智能化与乘用车有很多差异化&#xff0c;比如&#xff0c;B端客户对于规模车队的采购成本&#xff0c;智能化应用场景的适配性以及对自动驾驶更为务实的认知。 4月18日&#xff0c;2023上海车展&#xff0c;作为场景化新能源的自动驾驶全球领导者&am…

直播软件app开发:如何开发一个可以免费打扑克的直播应用?

作为一个技术开发人员&#xff0c;我深知直播软件app开发的重要性。在这个数字化时代&#xff0c;越来越多的人选择使用直播软件来分享自己的生活和与朋友互动。而随着技术的发展&#xff0c;直播软件也不断更新和改进&#xff0c;为用户提供更好的体验和功能。 对于开发者来说…

git merge、git pull和git fetch

git merge 合并分支&#xff0c;将目标分支合并到当前分支git fetch 更新远端分支&#xff0c;但不会merge到本地git pull 更新远端分支并merge到本地git pull git fecth git merge merge的意思为“合并”&#xff0c;git merge命令是用于将分支合并在一起的操作&#xff…

kafka_2.13-2.8.1环境搭建

本次kafka环境主要针对kafka2.x版本&#xff0c;运行kafka服务之前&#xff0c;需要先搭建zookeeper服务&#xff0c;因为kafka服务依赖zookeeper&#xff0c;kafka3.x版本后可以不需要手动搭建zookeeper了。 本文主要是介绍怎样搭建kafka2.8.1&#xff0c;关于kafka的操作&am…

matlab求解整数规划

一、整数规划 定义&#xff1a;数学规划中的变量&#xff08;部分或全部&#xff09;限制为整数时&#xff0c;称为整数规划。 若在线性规划模型中&#xff0c;变量限制为整数&#xff0c;则称为整数线性规划。 分类&#xff1a;&#xff08;1&#xff09;变量全部限制为整数时…

JUC多并发编程 内存模型

计算机硬件存储系统 因为有多级的缓存(CPU 和 物理主内存的速度不一致的)&#xff0c; CPU 的运行并不是直接操作内存耳饰先把内存里边的数据读到缓存&#xff0c;而内存的读和写操作的时候就会造成不一致的问题JVM 规范中试图定义一种 Java 内存模型(Java Memory Model, 简称…

【UE】倒计时归零时结束游戏

上一篇博客&#xff08;【UE】一个简易的游戏计时器&#xff09;完成了游戏时间每秒1的功能&#xff0c;本篇博客在此基础上完成倒计归零时结束游戏的功能 效果 步骤 1. 打开“ThirdPersonGameMode”&#xff0c;将剩余的秒数和分钟数的默认值分别设置为1和59 在事件图表中添…

全球首款车规级彩色激光大灯,这家中国供应商引领下一代显示交互

随着汽车智能化进程进入深水区&#xff0c;基于显示、照明的新技术正在成为新的聚焦点。无论是AR HUD&#xff0c;还是舱内多模态人机交互都在成为新的增量。而汽车独有的智能移动终端以及第三生活空间的未来属性&#xff0c;也在创造新的市场机会。 4月18日&#xff0c;全球领…

法规标准-ISO 16787标准解读

ISO 16787是做什么的&#xff1f; ISO 16787全称为智能运输系统-辅助泊车系统&#xff08;APS&#xff09;-性能要求和测试程序&#xff0c;其中主要描述了对APS系统的功能要求及测试规范 APS类型 根据目标停车位类型将APS系统分为两类&#xff1a; 1&#xff09;APS类型I&a…

北大POJ 1000 ~ 1009

1. AB &#x1f351; POJ1000 ab &#x1f354; 签到题 import java.io.*; import java.util.*; public class Main {public static void main(String args[]) throws Exception{Scanner cinnew Scanner(System.in);int acin.nextInt(),bcin.nextInt();System.out.println(…

开发常用的 Linux 命令3(文本处理、打包和压缩)

开发常用的 Linux 命令3&#xff08;文本处理、打包和压缩&#xff09; 作为开发者&#xff0c;Linux是我们必须掌握的操作系统之一。因此&#xff0c;在编写代码和部署应用程序时&#xff0c;熟练使用Linux命令非常重要。这些常用命令不得不会&#xff0c;掌握这些命令&#…

c提高学习——指针作为函数参数的输入特性

输入特性 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include<stdlib.h> //在主调函数分配内存 被调函数使用 void func(char *p) {strcpy(p, "hello world"); } void test01() {//在栈上分配内存char buf[1024] …

《使用循环做一些练习》---C语言

目录 前言&#xff1a; 1.练习专题 1.1打印1-100之间的奇数 1.2计算n的阶乘 1.3计算到达n阶乘的前m个阶乘相加 1.4演示字符串动态变化的效果 2.goto语句 ❤博主CSDN:啊苏要学习 ▶专栏分类&#xff1a;C语言◀ C语言的学习&#xff0c;是为我们今后学习其它语言打好基础…

扬声器,打印机2键触摸VK3602K SOP8 2键2通道触摸检测芯片技术资料直接输出自动校准功能

型号&#xff1a;VK3602K 封装形式&#xff1a;SOP8 KPP2620 VK3602K具有2个触摸按键&#xff0c;可用来检测外部触摸按键上人手的触摸动作。该芯片具有较高的集成度&#xff0c;仅需极少的外部组件便可实现触摸按键的检测。 提供了2路直接输出功能,可通过IO脚选择输出电平。…

基于单片机的家庭防盗报警系统的设计与实现_kaic

基于单片机的家庭防盗报警系统 专业&#xff1a;物联网工程 摘要&#xff1a;本论文研究的是将AT89C52单片机芯片作为核心元器件的防盗报警系统,该系统除了具有直接报警的功能外,还额外增加了布防和红外感应的功能。和市场上的其他各类防盗报警器相比,该设计的不同之处在于它所…

KubeSphere 助力提升研发效能的应用实践分享

作者&#xff1a;卢运强&#xff0c;主要从事 Java、Python 和 Golang 相关的开发工作。热爱学习和使用新技术&#xff1b;有着十分强烈的代码洁癖&#xff1b;喜欢重构代码&#xff0c;善于分析和解决问题。原文链接。 我司从 2022 年 6 月开始使用 KubeSphere&#xff0c;到目…

开放原子训练营(第一季)铜锁探密:基于铜锁构建在线在线加密工具箱

基于铜锁构建Web在线加密工具库 搭建运行环境 实验⼿册中的实验都是以 docker 和 docker-compose 环境为主&#xff0c;基于 Ubuntu 20.04 容器镜像。 初始化项目 首先利用 IDE 创建一个 tongsuo_web 的空项目&#xff0c;接下来我们所有的文件都会创建在该项目中&#xff0…

AI如何帮助人类进而验证直觉的可靠性?

AI可以通过以下方式帮助人类寻找反例进行否定或寻找特别的架构&#xff1a; 1. 利用机器学习算法进行反例搜索。AI可以训练一个分类器&#xff0c;用于判断某个假设是否成立。通过反复训练和测试&#xff0c;AI可以识别出一些不合理的假设&#xff0c;并帮助人类进行进一步的验…