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

文章目录

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

哈希表底层结构剖析

哈希概念

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;    //记录有效数据个数
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

同城跑腿APP开发需具备哪些功能?

移动互联网的飞速发展加上人们生活水平的提高&#xff0c;生活工作闲暇之余&#xff0c;人们不愿意为买药、送文件、取东西、送花、排队等小事浪费时间或者是根本没有时间去处理类似的事情。这个时候就想如果能够花钱请人来替我做这些事就好了&#xff0c;于是同城跑腿就在这样…

S/MIME电子邮件证书,符合FDA邮件安全要求

美国食品和药物管理局 &#xff08;FDA&#xff09;要求合作伙伴提交或接收电子监管信息时&#xff0c;必须使用数字证书保障通信安全。 01 为什么FDA使用数字证书保障通信安全&#xff1f; 为了维护数据完整性、准确性,有组织地管理文件,FDA为接受机构的电子监管提交设置了电子…

ajax的介绍及使用

ajax的介绍 开发流程 前端 ajax:前后端沟通的桥梁 后端 ajax介绍 ajax叫做异步的Javascript和xml ajax通过浏览器与服务器&#xff08;后端&#xff09;进行少量数据交互&#xff0c;进行页面异步更新&#xff08;网页不会重新加载&#xff09; 优点&#xff1a; 减轻服务器负…

C/C++中的数据结构对齐,#pragma pack() 和 __attribute__

C/C中的数据结构对齐 总览 数据结构对齐是指在计算机内存中排列和访问数据的方式。它包含三个独立但相关的问题&#xff1a;数据对齐&#xff08;data alignment&#xff09;&#xff0c;数据结构填充&#xff08; data structure padding&#xff09;和打包&#xff08;pack…

1.2和1.3、GCC

1.2和1.3、GCC 1.2和1.3、GCC1.2.1、什么是GCC1.2.2、编程语言的发展1.2.3、GCC工作流程1.2.4、gcc和g的区别1.2.5、GCC常用参数选项实际操作①接下来进行预处理操作&#xff08;test.c为需要预处理的源代码&#xff0c;test.i为要生成的目标代码&#xff09;②汇编操作&#x…

【Linux】二、Linux的基本指令

1、ls指令 语法 &#xff1a; ls [ 选项 ][ 目录或文件 ] 功能 &#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。 常用选项&#xff1a; &#xff08;1&#xff09;、-a 列出目录下的所有文件&#x…

按照条件向Spring容器中注册bean

1.Conditional注解概述 Conditional注解可以按照一定的条件进行判断&#xff0c;满足条件向容器中注册bean&#xff0c;不满足条件就不向容器中注册bean。 package org.springframework.context.annotation;import java.lang.annotation.Documented; import java.lang.annota…

基于 Python 的 Meta AI —— SAM

Segment Anything Model&#xff08;SAM&#xff09;是 Facebook 的一个 AI 模型&#xff0c;旨在推广分割技术。在我们之前的文章中&#xff0c;我们讨论了 SAM 的一般信息&#xff0c;现在让我们深入了解其技术细节。SAM 模型的结构如下图所示&#xff0c;图像经过编码器得到…

Leetcode-day4【88】【167】【125】【345】

文章目录 88. 合并两个有序数组题目解题思路解题思路【学习】尾插入法 167. 两数之和 II - 输入有序数组题目解题思路 125. 验证回文串题目解题思路 345. 反转字符串中的元音字母题目解题思路 88. 合并两个有序数组 题目 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums…

修改DaemonSet 的/args参数后多个pod重启的顺序

理论 当您修改了DaemonSet的/args参数时&#xff0c;DaemonSet控制器会自动触发Pod的滚动更新。滚动更新的过程是逐个将旧的Pod删除并创建新的Pod&#xff0c;以确保应用程序的高可用性和稳定性。 在进行滚动更新时&#xff0c;DaemonSet控制器会按照以下步骤逐个重启Pod&…

问卷中多选题如何分析?

一、案例与问卷 本研究选取大学生作为研究对象&#xff0c;旨在通过理财认知、理财现状、理财偏好三个方面&#xff0c;对大学生理财产品了解情况、使用需求进行调查。本次问卷共分为四个部分&#xff1a;第一部分共5道题&#xff0c;为基本信息题&#xff1b;第二部分共3道题…

【Linux高性能服务器编程】I/O复用的高级应用

文章目录 一、基于 select 的非阻塞 connect二、基于 poll 的聊天室程序2.1 客户端2.2 服务器 三、基于 epoll 实现同时处理 TCP 和 UDP 服务 一、基于 select 的非阻塞 connect connect系统调用的 man 手册中有如下一段内容&#xff1a; EINPROGERESS The socket is nonblock…

结构型模式-适配器模式

适配器模式 概述 如果去欧洲国家去旅游的话&#xff0c;他们的插座如下图最左边&#xff0c;是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑&#xff0c;手机在当地不能直接充电。所以就需要一个插座转换器&#xff0c;转换器第1面插入当地的插座&#x…

【java笔记】java多线程

目录 一、概念 1.1 什么是进程&#xff1f; 1.2 什么是线程&#xff1f; 1.3 什么事多线程&#xff1f; 1.4 进程和线程的关系 二、线程对象的生命周期 三、实现线程有两种方式 3.1 继承 java.lang.Thread&#xff0c;重写 run方法 3.2 实现 java.lang.Runnable 接口…

图像描述算法排位赛:SceneXplain 与 MiniGPT4 谁将夺得桂冠?

如果你对图像描述算法的未来感到好奇&#xff0c;本场“图像描述算法排位赛”绝对是你不能错过的&#xff01;在这场较量中&#xff0c;SceneXplain 和 MiniGPT-4 将会比试&#xff0c;谁将摘得这场比赛的桂冠&#xff1f; 背景介绍 在上篇文章中&#xff0c;我们介绍了图像描述…

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

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

Java设计模式-day02

4&#xff0c;创建型模式 4.2 工厂模式 4.2.1 概述 需求&#xff1a;设计一个咖啡店点餐系统。 设计一个咖啡类&#xff08;Coffee&#xff09;&#xff0c;并定义其两个子类&#xff08;美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】&#xff09;&#xff1b;再设…

Linux: 进程间通信机制

文章目录 1. 前言2. 进程间通信机制2.1 管道2.1.1 匿名管道2.1.2 popen() 和 pclose()2.1.3 命名管道 FIFO 2.2 消息队列2.3 共享内存2.4 信号量2.5 网络套接字2.6 UNIX套接字2.7 信号 3. 参考资料 1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给…

Javaee Spring的AOP简介

一.Spring的AOP简介 1.1 什么是AOP AOP 为 Aspect Oriented Programming 的缩写&#xff0c;意思为面向切面编程&#xff0c;是通过预编译方式和运行期动态代 理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续&#xff0c;是软件开发中的一个热点&#xff0c;也是…

shell脚本控制

shell脚本编程系列 处理信号 Linux利用信号与系统中的进程进行通信&#xff0c;通过对脚本进行编程&#xff0c;使其在收到特定信号时执行某些命令&#xff0c;从而控制shell脚本的操作。 Linux信号 shell脚本编程会遇到的最常见的Linux系统信号如下表所示&#xff1a; 在默…