[C++]——哈希(附源码)

news2024/11/25 16:36:08

目录

​编辑

​编辑

一、前言

二、正文

2.1 unorder系列关联式容器

2.1.1 unordered_map 

2.1.1.1 unorderer_map的介绍

        ①unordered_map的构造

        ②unordered_map的容量

        ③unordered_map的迭代器

        ④unordered_map的元素访问

        ⑤unordered_map的查询

        ⑥unordered_map的修改操作

        ⑦unordered_map的桶操作

2.1.2 unordered_set

2.2 底层结构 

2.2.1 哈希概念 

2.2.2 哈希冲突

 2.2.3 哈希函数

2.3 哈希冲突解决

2.3.1 闭散列 

2.3.2 闭散列模拟实现

2.3.2.1 插入

2.3.2.2  删除

2.3.2.3 查找

 2.3.3 开散列及其模拟实现

2.3.3.1 开散列概念

2.3.3.2 插入

2.3.3.3 删除

2.3.3.3 查找

2.4 unordered_map与unordered_set的模拟实现

2.4.1 哈希表的改造

 ①模版参数列表的改造

 ②增加迭代器操作

2.4.2 unordered_set模拟实现

2.4.3 unordered_map模拟实现

三、全部代码

1.闭散列的哈希表

2.开散列的哈希表

3.改造的哈希表

4.unordered_set

5.unoredered_map

四、结语


一、前言

      在上一篇博客中,为小伙伴们进行了红黑树的讲解,但是为了进一步提高数据的插入效率,便出现了哈希表,接下来就为大家带来哈希表的介绍,如有不足之处,欢迎各位大佬们给予指正!

二、正文

2.1 unorder系列关联式容器

        在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到log N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍,至于剩下的unordered_multimap和unordered_multiset大家可自行了解。

2.1.1 unordered_map 

2.1.1.1 unorderer_map的介绍

        1. unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与其对应的value

        2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。

        3. 在内部,unordered_map没有按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。

        4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低

        5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问 value。

        6. 它的迭代器至少是前向迭代器。        

        大家若想详细了解,可移步至其在线文档说明:

https://cplusplus.com/reference/unordered_map/unordered_map/?kw=unordered_map

2.1.1.2 unordered_map的接口说明

        ①unordered_map的构造
函数说明功能介绍
unordered_map构造不同格式的unordered_map对象
        ②unordered_map的容量
函数说明功能介绍
bool empty() const检测unordered_map是否为空
size_t size() const获取unordered_map的有效元素个数
        ③unordered_map的迭代器
函数说明功能介绍
begin返回unordered_map第一个元素的迭代器
end返回unordered_map最后一个元素下一个位置的迭代器
cbegin返回unordered_map第一个元素的const迭代器
cend返回unordered_map最后一个元素下一个位置的const迭代器
        ④unordered_map的元素访问
函数说明功能介绍
operator[]返回与key对应的value,设有一个默认值

注:        

        对于map中的operator[],其于我们之前所了解的vector,string的[]有所不同。对于之前的容器的[],我们是用于访问容器中存储的数据,但是对于map而言,若是查询的key不在容器中,便会先执行插入的操作,然后再进行对应位置的访问

        在后面我们会认识到unordered_map一般是对哈希桶进行封装,所以该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中, 将key对应的value返回。

        ⑤unordered_map的查询
函数说明功能介绍
iterator find(const K& key)返回key在哈希桶中的位置
size_t count(const K& key)返回哈希桶中关键码为key的键值对的个数

注:

         unordered_map中key是不能重复的,因此count函数的返回值最大为1

        ⑥unordered_map的修改操作
函数声明功能介绍
insert向容器中插入键值对
erase删除容器中的键值对
void clear()清空容器中有效元素个数
void swap(unorderde map&)交换两个容器中的元素
        ⑦unordered_map的桶操作
函数说明功能介绍
size_t bucket_count() const返回哈希桶中桶的总个数
size_t bucket_size ( size_t n ) const返回n号桶中有效元素的总个数
size_t bucket ( const key_t& k ) const返回元素key所在的桶号

2.1.2 unordered_set

        unordered_set的功能与unordered_map类似,只是后者比前者多存储一个数据,就像之前笔者所学过的set与map,具体功能可见下面文档:

unordered_set在线文档说明

2.2 底层结构 

        在上一节红黑树的学习中,我们知道其插入的效率几乎可以达到logN的程度,已经蛮厉害了,那么unordered系列的关联式容器效率还能更高,其实是因为其底层使用了哈希结构

2.2.1 哈希概念 

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

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

       

 当向该结构中:

        ●插入元素 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

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

   

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

        

例如:数据集合{1,7,6,4,5,9};

         

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

 

        用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。但是其也存在一个问题,也是当我们当们插入的元素中,如果存在两者及其以上的元素通过哈希函数算出来的哈希地址相同,就会导致我们到底该如何存储这些哈希地址相同的元素,这个问题也就是我们下面会讲到的哈希冲突。

2.2.2 哈希冲突

      对于两个数据元素的关键字$k_i$和 $k_j$(i != j),有$k_i$ != $k_j$,但有:Hash($k_i$) == Hash($k_j$),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。  

        把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。那么发生哈希冲突,我们该如何处理呢?

 2.2.3 哈希函数

  引起哈希冲突的一个原因可能是:哈希函数设计不够合理。      

  哈希函数设计原则:

        ●哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间

        ●哈希函数计算出来的地址能均匀分布在整个空间中

        ● 哈希函数应该比较简单

                

常见哈希函数:

        1. 直接定址法--(常用)

                取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B         

                优点:简单、均匀

                缺点:需要事先知道关键字的分布情况

                使用场景:适合查找比较小且连续的情况

        

        2. 除留余数法--(常用)

                设散列表中允许的地址数为m,

                取一个不大于m,但最接近或者等于m的质数p作为除数,

                按照哈希函数:Hash(key) = key% p(p将关键码转换成哈希地址

        

        3. 平方取中法--(了解)

                假设关键字为1234,平方就是1522756,抽中间的3位227作为哈希地址;                              再如关键字为4321,平方就是18671041,抽中间的3位671(或710)作为哈希地址

                平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

        

        4. 折叠法--(了解)

                折叠法是将关键字从左到右分割成位数相等的几部分(末尾部分可短些),

                然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

                折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

       

        5. 随机数法--(了解)

                选择一个随机函数,取关键字的随机函数值为它的哈希地址,

                即H(key) = random(key),其中 random为随机数函数。

                通常应用于关键字长度不等时采用此法

       

         6. 数学分析法--(了解)

                设有n个d位数,每一位可能有r种不同的符号,

                这r种不同的符号在各位上出现的频率不一定相同,

                可能在某些位上分布比较均匀,每种符号出现的机会均等,

                在某些位上分布不均匀只 有某几种符号经常出现。

                可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

                例如:

        假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同 的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还 可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移 位、前两数与后两数叠加(如1234改成12+34=46)等方法。

        数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的 若干位分布较均匀的情况

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

2.3 哈希冲突解决

        虽然采取合适哈希函数可以适当的减少哈希冲突的产生,但是想要真正的解决哈希冲突,两种常见的方法有:闭散列开散列 

2.3.1 闭散列 

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

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

                 比如2.2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

             

        ★插入

                ●通过哈希函数获取待插入元素在哈希表中的位置

                ●如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

 

         2.二次探测

                线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位 置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:$H_i$ = ($H_0$ + $i^2$ )% m, 或者:$H_i$ = ($H_0$ - $i^2$ )% m。其中:i = 1,2,3…, $H_0$是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表 的大小。 对于2.2.1中如果要插入44,产生冲突,第一次算的的地址为4,由于地址为4的表内已经填入4;下一次地址加2^1,得地址为6,依旧有值;下下一次地址加2^2得地址为8,表内为空,可以进行数据的插入。使用解决后的情况为:

 

        ★删除

        采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。 

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
    enum STATE
	{
		EXIST,
		EMPTY,
		DELETE
	};

         ★扩容

         当我们不断往哈希表中插入数据,随着数据量的增大,剩余空间不断减少,就愈发容易地出现哈希冲突,这势必会增大我们插入数据的消耗,那么我们就需要对哈希表进行扩容,那么我们到底要在什么时候开始扩容呢?这时候就需要引入载荷因子(又称负载因子)这一概念来帮助我们判断什么时候应当对当前容器进行扩容。

        具体介绍见下图:

 

2.3.2 闭散列模拟实现

        接下来我们为大家进行闭散列的哈希模拟实现

        对于这个闭散列的哈希表,它的成员主要有两个,一个是用来存储有效数据的容器,另一个是用记录当前表内有效数据的,我们用一个无符号整型接受即可。

        具体代码见下:

template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		typedef HashTableNode<K, V> Node;
	private:
		vector<Node> _table;	//哈希表
		size_t _n;				//载荷因子
	};
}
2.3.2.1 插入

        首先是插入操作。

        第一步是封装所需数据,在本文中我们采取存储数据pair的形式来实现,由于我们实现的是闭散列的形式,当我们在进行数据删除的时候,不能直接将数据删除,这样会影响后面元素的查找,这时候我们采取是改变状态值的方式,因此我们除了存储pair还需要存储数据状态,在明确这两点之后我们就可以将其封装方便我们的插入。

        第二步是确定哈希函数,由于我们插入的数据是随机的,并不是连续的,因此我们采取除留余数法来实现我们的哈希函数。为了实现简单,对于整型我们就直接将其对容器的size进行取摩,得到哈希地址,要注意这里不能对容器的capacity进行取摩。这是因为由于我们采取vector的容器来存储我们封装后的数据,得到哈希地址后我们需要通过【】来进行数据的写入,而vector的【】是有越界检查的,所以我们需要对容器的size进行取摩。由于我们插入的不总是整型,随着不同数据使用需求,直接将数据对容器的size进行取摩是不行的,这时候我们就需要随着不同的数据类型进行哈希函数的重载,由于本文主要进行整型和string的插入,因此对于后者string我们就进行了重载,主要通过ASCII码来进行对容器的size进行取摩,又由于string像整型一样,使用的比较频繁,于是我们将重载string的哈希函数进行特化就不需要频繁传参。

        第三步就是往表内插入数据,在进行数据的插入之前,我们需要先计算下容器载荷因子的大小,若是太大,则说明当前容器存储的有效数据较多,容易出现哈希冲突,这时候我们就需要对当前的哈希表进行扩容。由于我们定义的一个无符号整型来记录表内存储的有效数据个数,于是我们只需要将其除以容器的size的就可以获得载荷因子的数据,由于size和_n都是无符号整型,在除后结果小于0,会转化成0,于是我们将结果扩大了十倍,方便进行载荷因子的比较。

        如果载荷因子较大,当前表确实需要扩容,我们就重新创建一个新表,并将其的大小扩大到原表的二倍,并逐一取出原表的元素,将其插入新表并重新进行哈希的映射。

        如果载荷因子较小,说明当前表内的数据还不是很多,我们只需要对元素进行哈希映射插入到表中即可。通过哈希表对应的哈希函数我们可以得到映射的vector下标,若是对应下标的数据为空或是删除,我们就直接插入即可;若是不为空或是删除,我们就进行线性探测,往哈希地址的下一个进行判断,直至找到空或者删除的位置,将元素进行插入。

        具体代码如下:

        bool Insert(const pair<K, V>& kv)
		{
			//扩容
			if (_n * 10 / _table.size() > 7)
			{
				size_t newsize = _table.size() * 2;
				HashTable<K, V> newHT;
				newHT._table.resize(newsize);
				//插入数据并重新映射
				for (const auto& e : _table)
				{
					newHT.Insert(e._kv);
				}
				_table.swap(newHT._table);
			}
			//根据哈希函数得到对应位置下标
			HashFunc HF;
			size_t Hashi = HF(kv.first) % _table.size();

			//根据下标插入数据,若有冲突则线性探测
			//位置状态为空或者删除则插入
			while (_table[Hashi]._state == EXIST)
			{
				if (_table[Hashi]._kv.first == kv.first) return false;
				++Hashi;
			}
			_table[Hashi] = kv;
			_table[Hashi]._state = EXIST;
			++_n;
			return true;
		}
2.3.2.2  删除

        对于闭散列的哈希表,其元素的删除是较为简单的。

        先对要删除的元素通过哈希函数进行哈希映射找到对应的地址,与表内的数据进行比对,若是找到了,无需将其删除,只需将其状态更改为Delete即可;若是没找到,就直接返回false就可以了。

        具体代码如下:

        bool Erase(const K& key)
		{
			HashFunc HF;
			size_t Hashi = HF(key) % _table.size();
			//找到了
			while (_table[Hashi]._state != EMPTY)
			{
				if (_table[Hashi]._kv.first == key && _table[Hashi]._state == EXIST)
				{
					_table[Hashi]._state = DELETE;
					return true;
				}
				++Hashi;
				Hashi %= _table.size();
			}
			//没找到
			return false;
		}
2.3.2.3 查找

        查找的逻辑与删除类似,都是现根据哈希函数来进行哈希映射来找到哈希地址,再根据拿到的哈希地址在表内进行数据的比对,若是数据不匹配或者数据状态为删除,我们就将哈希地址加加,若是一直找到空都没有找到,说该元素并未插入该表内,返回false,找到就返回对应的数据。

        具体代码如下:

        HashTableNode<const K, V>* Find(const K& key)
		{
			HashFunc HF;
			size_t Hashi = HF(key) % _table.size();

			//找到了
			while (_table[Hashi]._state != EMPTY)
			{
				if (_table[Hashi]._kv.first == key && _table[Hashi]._state == EXIST)
				{
					return (HashTableNode<const K, V>*) & _table[Hashi]._kv;
				}
				++Hashi;
				Hashi %= _table.size();
			}
			//没找到
			return nullptr;
		}

        研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出 必须考虑增容。

         因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

 2.3.3 开散列及其模拟实现

        由于闭散列中一旦载荷因子数值过大,就需要进行扩容,因此其空间利用率比较低的同时开辟新表并插入数据的消耗也比较大,因此接下来我们来讲解另一种实现哈希表的方式——闭散列,而这种方式也是下面我们封装unordered_map和unordered_set的底层容器。

         

2.3.3.1 开散列概念

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

 

         从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

2.3.3.2 插入

        闭散列的哈希表插入与开散列的哈希表插入类似,都需要先将插入的元素通过哈希函数进行映射,得到哈希映射,不同的地方在于对哈希冲突的处理以及扩容的判断。

        首先我们先来讲讲扩容。对于闭散列的哈希表来说,它的扩容并不需要像开散列那样进行重新创建一个新表,只需要创建一个新的vector,将原vector中的一个个结点拿下来即可。然后是扩容时机的选择,由于闭散列采取链式的方式将一个个元素链接起来,对于空间的利用率相较于开散列而言会较高,因此其荷载因子可以达到1再进行扩容,也就是平均每个哈希地址下挂一个元素

        再而是哈希冲突的处理,我们无需像闭散列那样采取线性探测或是二次探测的方式来挤占其他哈希地址对应的空间,而是采取链式的方式,将冲突元素头插到当前的哈希地址,将这些冲突的元素像水桶一样挂在对应的哈希地址

        具体代码如下:

        bool Insert(const pair<K, V>& kv)
		{
			if(Find(kv.first))
			{
				return false;
			}

			HashFunc hf;

			// 负载因子到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++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插到新表
						size_t hashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

					_table[i] = nullptr;
				}

				_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;
		}
2.3.3.3 删除

        对于删除这个接口,我们要做的第一步就是查找表内是否有对应的元素,可以复用查找的接口,如果没有直接返回false即可;如果找到了,这时候我们就需要将要删除元素的上下两个节点进行链接,但是由于我们的节点中只存储了下一个节点的指针,因此我们还需要额外定义一个节点指针来记录所删除元素的上一个节点。当然还有一个需要注意的点,就是如果要删除的元素如果是哈希地址的第一个结点,就不需要链接,只需要将其下一个节点给哈希地址就可以了

        具体代码如下: 

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

					delete cur;	
					return true;
				}

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

			return false;
		}
2.3.3.3 查找

        查找这一接口也不难,就是通过哈希函数拿到对应的哈希地址,然后到对应的哈希桶进行元素的遍历,如果找不到则说明当前表内不存在该元素。

        具体代码如下:

        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;
				}

				cur = cur->_next;
			}

			return nullptr;
		}

2.4 unordered_map与unordered_set的模拟实现

2.4.1 哈希表的改造

         在本文中我们对unordered_map与unordered_set的模拟实现采取对闭散列的哈希表进行封装的方式,但是在上面中我们实现的哈希表还并不足以满足我们使用它们两者的需求,因此在封装之前我们还需要对其进行改造。

 ①模版参数列表的改造

        我们改造后的哈希表一共有四个模版参数。

●K:关键码类型——方便我们我们后续查找接口的实现

●T:结点数据——由于我们并不知道是使用这个哈希表的类是set还是map,所以采取泛型的方式,对于set而言它就是K,对于map而言就是pair<K,V>

●KeyOfT:由于我们需要通过结点数据进行哈希函数来取得哈希地址,对于set而言我们可以直接传给哈希函数,但是map的T是pair<K,V>的类型,显然不能直接传给哈希函数,因此我们就需要一个仿函数来帮助我们取出T中的关键码,方便我们取得哈希地址

●HashFunc:哈希函数,帮助我们获得哈希地址进行哈希映射

    template<class K,class T,class Ptr,class Ref,class KeyOfT,class HashFunc>
	struct HTIterator
    {

    };
 ②增加迭代器操作

         对于哈希表的迭代器而言,与以往我们其他容器诸如list,二叉搜索树的实现类似,都是对对节点的指针进行封装,来实现迭代器的各种功能,不过也有其不同的一点。由于闭散列的哈希表是由一个个哈希桶组成的,因此如果只是通过一个节点的指针我们并不能将整个哈希表遍历,于是我们还需要一个哈希表的指针来帮助我们进行各个哈希桶之间的切换

        因此对于哈希表的迭代器而言,我们需要两个成员变量,一个是节点的指针,另一个就是哈希表的指针。由于我们对迭代器进行初始化时需要访问哈希表的指针,但是其为哈希表private成员,因此我们需要将迭代器设为哈希表的友元,这样就能够正常的初始化了。除此之外,由于有要用到哈希表类型的指针,我们还需要对哈希表进行一个前置声明,避免迭代器找不到该类型。

        具体代码如下:

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

	template<class K,class T,class Ptr,class Ref,class KeyOfT,class HashFunc>
	struct HTIterator
	{
		typedef HashNode<T> Node;
		typedef HTIterator<K, T,Ptr, Ref, KeyOfT,  HashFunc> Self;
		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;

		
		HTIterator(Node* node, HashTable<K, T, KeyOfT,  HashFunc>* pht)
			:_node(node)
			,_pht(pht)
		{}

		HTIterator(const iterator& it)
			:_node(it._node)
			, _pht(it._pht)
		{}

		Self& operator ++()
		{
			//当前桶不为空
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				KeyOfT Key;
				HashFunc HF;
				size_t hashi = (HF(Key(_node->_data))% _pht->_table.size())+1;
				while (hashi < _pht->_table.size()&&_pht->_table[hashi] == nullptr)
				{
					++hashi;

				}
				//找到不为空或者走到end
				if (hashi == _pht->_table.size()) _node = nullptr;
				else _node = _pht->_table[hashi];
			}
			return *this;
		}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &(_node->_data);
		}
		bool operator==(const Self& it) const
		{
			return this->_node == it._node;
		}

		bool operator!=(const Self& it)const
		{
			return this->_node != it._node;
		}
		
		Node* _node;
		const HashTable<K, T, KeyOfT, HashFunc>* _pht;
	};

2.4.2 unordered_set模拟实现

2.4.3 unordered_map模拟实现

        对于上面两者的封装我们只需复用哈希表的接口即可,具体实现可见下面代码

三、全部代码

1.闭散列的哈希表

template<class T>
struct DefaultHashFunc
{
	size_t operator()(T data)
	{
		return data;
	}
};

template<>
struct DefaultHashFunc<string>
{
	size_t operator()(const string& str)
	{
		// BKDR
		size_t hash = 0;
		for (auto ch : str)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

namespace open_address
{
	enum STATE
	{
		EXIST,
		EMPTY,
		DELETE
	};



	template<class K, class V>
	struct HashTableNode
	{
		HashTableNode()
			:_state(EMPTY)
		{}

		HashTableNode(const pair<K, V>& kv)
			:_kv(kv)
			, _state(EXIST)
		{
		}

		pair<K, V> _kv;	//存储数据
		STATE _state;  //状态
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		typedef HashTableNode<K, V> Node;

		HashTable()
			:_n(0)
		{
			_table.resize(10);
		}


		bool Insert(const pair<K, V>& kv)
		{
			//扩容
			if (_n * 10 / _table.size() > 7)
			{
				size_t newsize = _table.size() * 2;
				HashTable<K, V> newHT;
				newHT._table.resize(newsize);
				//插入数据并重新映射
				for (const auto& e : _table)
				{
					newHT.Insert(e._kv);
				}
				_table.swap(newHT._table);
			}
			//根据哈希函数得到对应位置下标
			HashFunc HF;
			size_t Hashi = HF(kv.first) % _table.size();

			//根据下标插入数据,若有冲突则线性探测
			//位置状态为空或者删除则插入
			while (_table[Hashi]._state == EXIST)
			{
				if (_table[Hashi]._kv.first == kv.first) return false;
				++Hashi;
			}
			_table[Hashi] = kv;
			_table[Hashi]._state = EXIST;
			++_n;
			return true;
		}

		HashTableNode<const K, V>* Find(const K& key)
		{
			HashFunc HF;
			size_t Hashi = HF(key) % _table.size();

			//找到了
			while (_table[Hashi]._state != EMPTY)
			{
				if (_table[Hashi]._kv.first == key && _table[Hashi]._state == EXIST)
				{
					return (HashTableNode<const K, V>*) & _table[Hashi]._kv;
				}
				++Hashi;
				Hashi %= _table.size();
			}
			//没找到
			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashFunc HF;
			size_t Hashi = HF(key) % _table.size();
			//找到了
			while (_table[Hashi]._state != EMPTY)
			{
				if (_table[Hashi]._kv.first == key && _table[Hashi]._state == EXIST)
				{
					_table[Hashi]._state = DELETE;
					return true;
				}
				++Hashi;
				Hashi %= _table.size();
			}
			//没找到
			return false;
		}


	private:
		vector<Node> _table;	//哈希表
		size_t _n;				//载荷因子
	};
}

2.开散列的哈希表

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, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_table.resize(10, nullptr);
		}

		~HashTable()
		{
			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;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			if(Find(kv.first))
			{
				return false;
			}

			HashFunc hf;

			// 负载因子到1就扩容
			if (_n == _table.size())
			{
				// 16:03继续
				size_t newSize = _table.size()*2;
				vector<Node*> newTable;
				newTable.resize(newSize, nullptr);

				// 遍历旧表,顺手牵羊,把节点牵下来挂到新表
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插到新表
						size_t hashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}

					_table[i] = nullptr;
				}

				_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;
				}

				cur = cur->_next;
			}

			return nullptr;
		}

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

					delete cur;	
					return true;
				}

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

			return false;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_kv.first <<":"<< cur->_kv.second<< "->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
			cout << endl;
		}

	private:
		vector<Node*> _table; // 指针数组
		size_t _n = 0; // 存储了多少个有效数据
	};
}

3.改造的哈希表

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

		T _data;
		HashNode* _next;
	};

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

	template<class K,class T,class Ptr,class Ref,class KeyOfT,class HashFunc>
	struct HTIterator
	{
		typedef HashNode<T> Node;
		typedef HTIterator<K, T,Ptr, Ref, KeyOfT,  HashFunc> Self;
		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;

		
		HTIterator(Node* node, HashTable<K, T, KeyOfT,  HashFunc>* pht)
			:_node(node)
			,_pht(pht)
		{}

		HTIterator(const iterator& it)
			:_node(it._node)
			, _pht(it._pht)
		{}

		Self& operator ++()
		{
			//当前桶不为空
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				KeyOfT Key;
				HashFunc HF;
				size_t hashi = (HF(Key(_node->_data))% _pht->_table.size())+1;
				while (hashi < _pht->_table.size()&&_pht->_table[hashi] == nullptr)
				{
					++hashi;

				}
				//找到不为空或者走到end
				if (hashi == _pht->_table.size()) _node = nullptr;
				else _node = _pht->_table[hashi];
			}
			return *this;
		}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &(_node->_data);
		}
		bool operator==(const Self& it) const
		{
			return this->_node == it._node;
		}

		bool operator!=(const Self& it)const
		{
			return this->_node != it._node;
		}
		
		Node* _node;
		const HashTable<K, T, KeyOfT, HashFunc>* _pht;
	};

	template<class K, class T, class KeyOfT,class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		//友元声明
		template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
		friend struct HTIterator;
	public:
		typedef HashNode<T> Node;
		typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;
		typedef HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator;

		HashTable()
			:_n(0)
		{
			_table.resize(10, nullptr);
		}


		iterator begin()
		{
			size_t hashi = 0;
			while(_table[hashi] == nullptr && hashi < _table.size())
			{
				++hashi;
			}
			if (hashi == _table.size()) return iterator(nullptr, this);
			else return iterator(_table[hashi], this);
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}

		const_iterator begin() const
		{
			size_t hashi = 0;
			while(_table[hashi] == nullptr && hashi < _table.size())
			{
				++hashi;
			}
			if (hashi == _table.size()) return const_iterator(nullptr, this);
			else return const_iterator(_table[hashi], this);
		}

		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}


		iterator Find(const K& key)
		{
			HashFunc HF;
			KeyOfT Key;
			size_t Hashi = HF(key) % _table.size();
			Node* cur = _table[Hashi];

			//找到了
			while (cur)
			{
				if (Key(cur->_data) == key)
					return iterator(cur,this);
				else
					cur = cur->_next;
			}
			//没找到
			return iterator(nullptr,this);
		}

		pair<iterator,bool> Insert(const T& data)
		{
			//表内若已存在,则返回
			KeyOfT Key;
			iterator it = Find(Key(data));
			if (it._node!= nullptr) return make_pair(it, false);

			//扩容
			if (_n / _table.size() == 1)
			{
				//建立新表
				size_t newsize = _table.size() * 2;
				vector<Node*> newtable;
				newtable.resize(newsize, nullptr);

				//遍历旧表并将数据移动到新表
				for (auto& node : _table)
				{
					HashFunc HF;
					Node* cur = node;
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = HF(Key(cur->_data)) % newtable.size();
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
					node = nullptr;
				}
				//将旧表与新表交换
				_table.swap(newtable);
			}

			//哈希函数查找对应位置并进行数据的头插
			HashFunc HF;
			size_t hashi = HF(Key(data)) % _table.size();

			Node* cur = new Node(data);
			cur->_next = _table[hashi];
			_table[hashi] = cur;
			++_n;

			return make_pair(iterator(cur,this),true);
		}

		void Print()
		{
			//遍历表并将打印数据
			for (auto& node : _table)
			{
				HashFunc HF;
				KeyOfT Key;
				Node* cur = node;
				if (node)
				{
					size_t hashi = HF(Key(cur->_data)) % _table.size();
					printf("[%d]->", hashi);
					while (cur)
					{

						cout << cur->_kv.first << "->";
						cur = cur->_next;
					}
					cout << endl;
				}

			}


		}

		bool Erase(const K& key)
		{
			if (Find(key) == nullptr) return false;

			HashFunc HF;
			KeyOfT Key;
			size_t hashi = HF(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				//找到了
				if (Key(cur->_data) == key)
				{
					//cur位于第一个数据
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					cur = nullptr;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}
			//没找到
			return false;

		}

		~HashTable()
		{
			//遍历表并将清理数据
			for (auto& node : _table)
			{
				Node* cur = node;
				Node* next = nullptr;
				while (cur)
				{
					next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}
	private:
		vector<Node*> _table;
		size_t _n;
	};

}

4.unordered_set

namespace mine
{
	template <class K>
	class unordered_set
	{
	public:
		struct KeyOfSet
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};

		typedef typename hash_bucket::HashTable<K, K, KeyOfSet>::const_iterator iterator;
		typedef typename hash_bucket::HashTable<K, K, KeyOfSet>::const_iterator const_iterator;


		pair<iterator,bool> Insert(const K& key)
		{
			return _set.Insert(key); 
		}

		iterator begin() 
		{
			return _set.begin();
		}

		iterator end() 
		{
			return _set.end();
		}

		const_iterator begin() const
		{
			return _set.begin();
		}

		const_iterator end() const
		{
			return _set.end();
		}


		

	private:
		hash_bucket::HashTable<K, K, KeyOfSet> _set;

	};
}

5.unoredered_map

template <class K,class V>
	class unordered_map
	{
		
	public:
		struct KeyOfMap
		{
			const K& operator()(const pair<K,V>& kv)
			{
				return kv.first;
			}
		};

		typedef typename hash_bucket::HashTable<K, pair<const K, V>, KeyOfMap>::iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, KeyOfMap>::const_iterator const_iterator;


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

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

		iterator begin()
		{
			return _map.begin();
		}

		iterator end()
		{
			return _map.end();
		}

		const_iterator begin() const
		{
			return _map.begin();
		}

		const_iterator end()  const
		{
			return _map.end();
		}


	private:
		hash_bucket::HashTable<K, pair<const K,V>, KeyOfMap> _map;


	};
}

四、结语

         到此为止,关于哈希表的部分内容就告一段落了,至于其应用诸如位图和布隆过滤器我们会在下一节中为小伙伴们继续介绍,小伙伴们敬请期待呀!

        关注我 _麦麦_分享更多干货:_麦麦_-CSDN博客
        大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!

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

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

相关文章

使用Ubuntu快速部署MinIO对象存储

想拥有自己的私有云存储&#xff0c;安全可靠又高效&#xff1f;MinIO是你的理想选择&#xff01;这篇文章将手把手教你如何在Ubuntu 22.04服务器上部署MinIO&#xff0c;并使用Nginx反向代理和Let’s Encrypt证书进行安全加固。 即使你是新手&#xff0c;也能轻松完成&#xf…

Maven 下载配置 详解 我的学习笔记

Maven 下载配置 详解 我的学习笔记 一、Maven 简介二、maven安装配置三、maven基本使用四、idea配置mavenidea配置maven环境maven坐标idea创建maven项目配置Maven-Helper插件 五、依赖管理 一、Maven 简介 Apache Maven 是一个项目管理和构建工具&#xff0c;它基于项目对象模型…

一文带你了解,全国职业院校技能大赛老年护理与保健赛项如何备赛

老年护理与保健&#xff0c;作为2023年全国职业院校技能大赛的新增赛项&#xff0c;紧密贴合党的二十大精神&#xff0c;致力于加速健康与养老产业的蓬勃发展&#xff0c;并深化医养康养结合的服务模式。此赛项不仅承载着立德树人的教育使命&#xff0c;更通过竞赛的引领作用&a…

STM32ZET6-USART使用

一、原理说明 STM32自带通讯接口 通讯目的 通信方式&#xff1a; 全双工&#xff1a;通信时可以双方同时通信。 半双工&#xff1a;通信时同一时间只能一个设备发送数据&#xff0c;其他设备接收。 单工&#xff1a;只能一个设备发送到另一个设备&#xff0c;例如USART只有…

电话语音机器人,是由哪些功能构成?

电话语音机器人是自动电话销售、筛选意向客户的&#xff0c;只要录入好行业话术&#xff0c;导入要拨打的手机号&#xff0c;机器人就可以上岗工作了。 电话语音机器人组成部分&#xff1a; 1、语音识别器&#xff0c;主要作用&#xff1a;识别客户讲话内容&#xff0c;从而做…

理解 WordPress | 第二篇:结构化分析

WordPress 专题致力于从 0 到 1 搞懂、用熟这种可视化建站工具。 第一阶段主要是理解。 第二阶段开始实践个人博客、企业官网、独立站的建设。 如果感兴趣&#xff0c;点个关注吧&#xff0c;防止迷路。 WordPress 的内容和功能结构可以按照层级来划分&#xff0c;这种层次化的…

vue3项目history模式部署404处理,使用 historyApiFallback 中间件支持单页面应用路由

vue3项目history模式部署404处理&#xff0c;使用 historyApiFallback 中间件支持单页面应用路由 在现代的 web 开发中&#xff0c;单页面应用&#xff08;SPA&#xff09;变得越来越流行。这类应用通常依赖于客户端路由来提供流畅的用户体验&#xff0c;但在服务器端&#xf…

计算机毕业设计Hadoop+PySpark深度学习游戏推荐系统 游戏可视化 游戏数据分析 游戏爬虫 Scrapy 机器学习 人工智能 大数据毕设

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

PHP电商供应链ERP管理系统小程序源码

&#x1f680;电商供应链大揭秘&#xff01;ERP管理系统如何重塑你的商业版图✨ &#x1f50d; 什么是电商供应链ERP管理系统&#xff1f; 电商供应链ERP管理系统是一款基于FastAdminThinkPHP开发的系统。该系统可满足电商企业管理自身进销存&#xff0c;帮助中小型电商企业管…

全参微调与LoRA的区别,及7种LoRA变种方法解析

随着LLM的发展和应用&#xff0c;在LLM的预训练模型基础上做微调&#xff0c;使其适用于自己的业务场景的研究越来越多。与全参数SFT相比LoRA是在冻结LLM本身参数的基础上&#xff0c;在旁路增加两个可学习的矩阵&#xff0c;用于训练和学习&#xff0c;最后推理是LLM输出和可学…

ubuntu工具 -- ubuntu服务器临时没有网络,急需联网下载东西怎么办? 使用手机提供网络

问题 ubuntu服务器配置经常遇到临时需要网络下载文件需求, 通过有线连接又来不及 解决方法 使用手机usb为ubuntu服务器提供网络 先在ubuntu上运行 ifconfig 查看当前的网络接口, 一会看看多了哪个网口 1. 手机端操作 先使用usb数据线将手机连接到服务器上 打开手机的usb共享…

一文快速预览经典深度学习模型(一)——CNN、RNN、LSTM、Transformer、ViT

Hi&#xff0c;大家好&#xff0c;我是半亩花海。本文主要简要并通俗地介绍了几种经典的深度学习模型&#xff0c;如CNN、RNN、LSTM、Transformer、ViT&#xff08;Vision Transformer&#xff09;等&#xff0c;便于大家初探深度学习的相关知识&#xff0c;并更好地理解深度学…

【D3.js in Action 3 精译_038】4.2 D3 折线图的绘制方法及曲线插值处理

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

数据结构(8.7_2)——败者树

多路平衡归并带来的问题 什么是败者树 败者树的构造 败者树的使用 败者树在多路平衡归并中的应用 败者树的实现思路 总结

Web Broker(Web服务应用程序)入门教程(1)

1、介绍 Web Broker 组件&#xff08;位于工具面板的“Internet”选项卡中&#xff09;可以帮助您创建与特定统一资源标识符&#xff08;URI&#xff09;相关联的事件处理程序。当处理完成后&#xff0c;您可以通过编程方式构建 HTML 或 XML 文档&#xff0c;并将它们传输给客…

Redis高级篇之缓存一致性详细教程

文章目录 0 前言1.缓存双写一致性的理解1.1 缓存按照操作来分 2. 数据库和缓存一致性的几种更新策略2.1 可以停机的情况2.2 我们讨论4种更新策略2.3 解决方案 总结 0 前言 缓存一致性问题在工作中绝对没办法回避的问题&#xff0c;比如&#xff1a;在实际开发过程中&#xff0c…

Vue2进阶之Vue3高级用法

Vue3高级用法 响应式Vue2&#xff1a;Object.definePropertyObject.definePropertythis.$set设置响应式 Vue3&#xff1a;Proxy composition APIVue2 option API和Vue3 compositionAPIreactive和shallowReactivereadonly效果toRefs效果 生命周期main.jsindex.htmlLifeCycle.vue…

Unity3D学习FPS游戏(10)子弹攻击敌人掉血(碰撞检测)

前言&#xff1a;前面最然创造出带有血条的敌人&#xff0c;但子弹打中敌人并没有效果。所以本篇将实现子弹攻击敌人&#xff0c;并让敌人掉血。 子弹攻击敌人掉血 整体思路目标补充知识-碰撞检测 准备工作刚体和碰撞器添加添加刚体后子弹代码优化补充知识-标签系统Tag添加 碰…

AMD显卡低负载看视频掉驱动(chrome edge浏览器) 高负载玩游戏却稳定 解决方法——关闭MPO

问题 折磨的开始是天下苦黄狗久矣&#xff0c;为了不再被讨乞丐的显存恶心&#xff0c;一怒之下购入了AMD显卡&#xff08;20GB显存确实爽 头一天就跑了3dmark验机&#xff0c;完美通过&#xff0c;玩游戏也没毛病 但是呢这厮是一点不省心&#xff0c;玩游戏没问题&#xff0c…

服装品牌零售业态融合中的创新发展:以开源 AI 智能名片 S2B2C 商城小程序为视角

摘要&#xff1a;本文以服装品牌零售业态融合为背景&#xff0c;探讨信息流优化和资金流创新的重要作用&#xff0c;并结合开源 AI 智能名片 S2B2C 商城小程序&#xff0c;分析其如何进一步推动服装品牌在零售领域的发展&#xff0c;提高运营效率和用户体验&#xff0c;实现商业…