C++的哈希 哈希表 哈希桶

news2024/11/16 17:17:13

目录

Unordered系列关联式容器

什么是哈希

哈希表

闭散列

载荷因子α

扩容

查找

删除 

字符串哈希算法

最终代码

开散列

插入

查找

删除

最终代码

完整代码


Unordered系列关联式容器

C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可以达到log_2 N,即最差情况下要比较红黑树的高度次树中的结点特别多且有序

        但又因为键值对与其存储位置之间没有对应的关系,查找一个元素时必须要多次比较键值对的键,而我们理想中的搜索方法应该是“可以不经过任何比较,一次直接从表中得到要搜索的元素”,而实现这一搜索方法应该需要构造一种存储结构,通过某种函数使元素的存储位置与它的键值对的键之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素,所以C++11中又提供了4个unordered系列的关联式容器unordered_map、unordered_set、unordered_multimap、unordered_multiset,它们与之前的map、set等容器的使用方式基本类似,只是底层结构不同,unordered序列的关联式容器的底层结构是哈希表,这使得它们在查询时的时间复杂度可以达到O(1)

什么是哈希

基本概念:哈希是一种将任意大小的数据映射到固定大小的值(通常为整数)的思想(建立值和值存储间的映射关系)

注意事项:哈希是一种思想,哈希表是基于哈希表的实现的一种数据结构 

哈希表

基本概念:本质是一个数组。这个数组的每个位置(称为桶或槽)用于存储键值对,哈希表需要使用哈希函数依据键值对中key将该键值对映射到数组的某个位置,因为如果直接映射会出现“值不易映射”和“值分散”的问题

  • 值不易映射:键的类型是string、结构体类型的对象
  • 值分散:多个值之间的差值过大导致的空间浪费(存放并建立10001、11、55、24、19与存放位置间的映射关系,如果不做特殊处理就需要开辟很大的数组来存放这些数

“除留余数法”可以解决值分散的问题(使得值的大小和空间无关),但会出现哈希冲突问题(此外引起哈希冲突的一个原因可能是:哈希函数设计不够合理)

哈希函数: 将key转换为数组(哈希表)的索引(可能是一行代码,也可能是一个函数)

哈希函数的设计理念:

  • 均匀性一个好的哈希函数应该能够尽可能地将不同的键映射到不同的哈希值,以确保数据在哈希表中分布均匀。这样可以减少哈希冲突的可能性,提高哈希表的效率

  • 确定性哈希函数应该是确定性的,即对于相同的输入键,哈希函数应该始终返回相同的哈希值。这是保证哈希表中数据一致性的重要特性

  • 简单性:尽量设计简单的哈希函数,以降低计算成本和实现复杂度,计算哈希值所需的时间应该是常量级的,这样可以确保哈希表的操作效率高,不会成为性能瓶颈

  • 有效性:如果散列表有 m 个地址(桶),则哈希函数的值域(输出范围)必须在 0 到 m-1 之间,以确保哈希值可以映射到散列表的有效地址范围内

常见的哈希函数:

  • 除留余数法:hash = key % table_size
  • 直接定址法:
  • Multiplicative Hash Function:hash = floor(table_size * (key * A % 1))
  • 平方取中法:key = 1234key^2 = 1522756,取中间的 227 作为哈希值
  • FNV Hash (Fowler-Noll-Vo):
uint32_t fnv1a_hash(const std::string& key) {
    uint32_t hash = 2166136261u; // FNV offset basis
    for (char c : key) {
        hash ^= c;
        hash *= 16777619;
    }
    return hash;
}
  • DJB2 Hash:
uint32_t djb2_hash(const std::string& key) {
    uint32_t hash = 5381;
    for (char c : key) {
        hash = ((hash << 5) + hash) + c; // hash * 33 + c
    }
    return hash;
}

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

哈希冲突:当不同的键被哈希函数映射到相同的索引时,称为哈希冲突,哈希表需要处理这些冲突,常见的方法有链地址法(开散列)开放地址法(闭散列)

闭散列

基本概念:发生冲突时,通过探测(如线性探测、二次探测或双重哈希等方法)来寻找下一个键可用的位置

优点:不需要额外的存储空间,访问速度快

缺点:探测过程中可能出现“聚集”现象使得存放效率降低,且它的删除操作复杂

删除操作复杂的原因:

1、寻找一个数字时,先去该数字的映射位置寻找,找不到就继续向后寻找,如果遇到空时还没找到就停止寻找(因为如果映射位置没有,那么该数字一定是在映射位置后的某个位置插入了,插入时应该是数字、新数字 ...而不是空 新数字 ...)

2、如果按照上述的方式寻找一个数字,那么当删除目标数字前的某个数字时就会导致虽然目标数字存在但是找不到目标数字的问题

删除操作:

1、映射位置除了要存放数值外,还要存放一个标志位,用于标识当前映射位置的状态

enum State
{
	EMPTY,//为空
	EXIST,//存在
	DELETE//删除
};
  • 当55被删除后,它的状态被设为删除而不是空,这样就可以接着继续向后寻找

进行插入操作前先进行依据上面的内容写出插入操作(负载因子后面有描述):

bool Insert(const pair<K, V>& kv)
{
	//防止冗余
	if (Find(kv.first))
		return false;

	//当负载因子大于等于0.7时进行扩容,以空间换时间
	if (_n * 10 / _table.size() >= 0.7)//采用/取整得不到小数,所以直接令_n * 10 
	{
		//新建一个哈希表
		HashTable<K,V, Hash> newHT;
		//确定新哈希表的大小
		newHT._table.resize(_table.size() * 2);

		//将旧表中的键重新映射到新表中
		for (size_t i = 0; i < _table.size(); i++)
		{
			if (_table[i]._state ==  EXIST)//原表中某下标的状态为存在时,将原表某下标中存放的键值对插入新表中
			{
				newHT.Insert(_table[i]._kv);//直接复用Insert操作
			}
		}
		_table.swap(newHT._table);
	}

	size_t hashi = kv.first % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数

	//线性探测
	while (_table[hashi]._state == EXIST)//探查到目标映射位置存在数字时就进行探测
	{
		++hashi;
		hashi %= _table.size();//取模的方式防止hashi越界
	}

	//确定好位置后进行插入
	_table[hashi]._kv = kv;//在合适的映射位置插入
	_table[hashi]._state = EXIST;//然后后将该位置的状态标识为EXIST
	++_n;//哈希表中有效数据元素+1
	return true;
}

注意事项:哈希表的大小(table_size)和底层数组的容量(capacity)是两个不同的概念, 假设我们有一个哈希表,初始大小为 7(即 table_size = 7),底层数组的容量为 10(即 capacity = 10)。我们有一组键需要插入到哈希表中:{10, 20, 30, 40, 50}:

哈希函数为 hash = key % table_size

  • 10 % 7 = 3,键 10 存储在桶 3
  • 20 % 7 = 6,键 20 存储在桶 6
  • 30 % 7 = 2,键 30 存储在桶 2
  • 40 % 7 = 5,键 40 存储在桶 5
  • 50 % 7 = 1,键 50 存储在桶 1

哈希函数为 hash = key % capacity

  • 10 % 10 = 0,键 10 存储在桶 0
  • 20 % 10 = 0,键 20 存储在桶 0(冲突)
  • 30 % 10 = 0,键 30 存储在桶 0(冲突)
  • 40 % 10 = 0,键 40 存储在桶 0(冲突)
  • 50 % 10 = 0,键 50 存储在桶 0(冲突)

        可以看出,使用 capacity 作为模数会导致所有键都存储在桶 0,导致严重的冲突和性能下降。而使用 table_size 作为模数可以确保键均匀地分布在不同的桶中,从而提高查找效率,此外,如果table_size 满了也会出现陷入死循环问题,为此引入了闭散列的另一个重要概念:载荷因子α(一定要分清底层容器大小capacity、哈希表的大小hash_table.size、哈希表中存放的有效数据个数n,一般情况下哈希表的大小和底层容器大小的设定是分开的,但也可以让它们两个相等)
 

载荷因子α

基本概念:载荷因子α表示哈希表中已存储元素数量与哈希表容量的比值,即哈希表的填充程度,载荷因子是开放定址法中十分重要的因素

计算公式:α = 填入表中的元素个数 / 哈希表的长度

α 越高,冲突率越高,插入等操作的效率越低,空间利用率越高

α 越低,冲突率越低,插入等操作的效率越高,空间利用率越低

α 的取值应该严格控制在0.7~0.8之间

扩容

中心思想:扩容时需要将原表中参与映射的键在新表中重新映射一次

vector的swap方法:vector::swap - C++ 参考 (cplusplus.com)

问题:_table.swap(newHT._table)时都做了什么?

  1. 交换容量:当前可以存储的元素数量被交换
  2. 交换大小:当前已存储的元素数量被交换
  3. 结论:交换了双方的数据结构,_table 现在持有新表的数据结构,而 newHT 持有旧表的数据结构,函数结束后 newHT 会被自动销毁,从而实现了高效的扩容操作

  交换不会改变新旧表的地址:

cout <<"交换前旧表的地址为:" <<  & _table << endl;
cout <<"交换前新表的地址为:" <<  &newTable << endl;

_table.swap(newTable);//交换

cout << "交换后旧表的地址为:" << &_table << endl;
cout << "交换后新表的地址为:" << &newTable << endl;

查找

HashDate<K, V>* Find(const K& key)
{
	//计算要查早的键的映射位置
	size_t hashi = key % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数

	//线性探测
	while (_table[hashi]._state != EMPTY)//到映射位置上去查找,如果该位置上不为空,
	{
		if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以
		//错误的:if (_table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以
		{
			return &_table[hashi];//返回该位置的地址
		}
		++hashi;
		hashi %= _table.size();//取模的方式防止hashi越界
	}
	return nullptr;//找不到就返回空

}

删除 

bool Erase(const K& key)
{
	HashDate<K, V>* ret = Find(key);
	if (ret == nullptr)
	{
		return false;
	}
	else
	{
		ret->_state = DELETE;//这里只是将删除键所处位置的状态标识符改为了DELETE
		--_n;
		return true;
	}
}

字符串哈希算法

基本概念:key不支持强转为整型后取模,需要自己提供转换为整型的仿函数,仿函数中的()重载一般为将字符串映射成固定长度的哈希值的算法(stoi只能处理"1112"类型的字符串,但是除了不了"2115few"甚至是由汉字组成的字符串"样非分"),常见的字符串哈希算法如下:

1、DJB2

unsigned long djb2(const std::string& str) {
    unsigned long hash = 5381;
    for (char c : str) {
        hash = ((hash << 5) + hash) + c; // hash * 33 + c
    }
    return hash;
}

2、SDBM

unsigned long sdbm(const std::string& str) {
    unsigned long hash = 0;
    for (char c : str) {
        hash = c + (hash << 6) + (hash << 16) - hash;
    }
    return hash;
}

3、BKDR Hash

unsigned long bkdrHash(const std::string& str) {
    unsigned long seed = 131; // 31, 131, 1313, 13131, 131313, etc.
    unsigned long hash = 0;
    for (char c : str) {
        hash = hash * seed + c;
    }
    return hash;
}

4、ELF Hash 

unsigned long elfHash(const std::string& str) {
    unsigned long hash = 0;
    unsigned long x = 0;
    for (char c : str) {
        hash = (hash << 4) + c;
        if ((x = hash & 0xF0000000L) != 0) {
            hash ^= (x >> 24);
        }
        hash &= ~x;
    }
    return hash;
}

5、FNV-1a Hash

unsigned long fnv1aHash(const std::string& str) {
    const unsigned long fnv_prime = 0x100000001b3;
    const unsigned long offset_basis = 0xcbf29ce484222325;
    unsigned long hash = offset_basis;
    for (char c : str) {
        hash ^= c;
        hash *= fnv_prime;
    }
    return hash;
}

6、MD5

#include <openssl/md5.h>

std::string md5Hash(const std::string& str) {
    unsigned char digest[MD5_DIGEST_LENGTH];
    MD5((unsigned char*)str.c_str(), str.size(), (unsigned char*)&digest);    

    char mdString[33];
    for(int i = 0; i < 16; i++)
        sprintf(&mdString[i*2], "%02x", (unsigned int)digest[i]);

    return std::string(mdString);
}

7. SHA-1 

#include <openssl/sha.h>

std::string sha1Hash(const std::string& str) {
    unsigned char hash[SHA_DIGEST_LENGTH];
    SHA1((unsigned char*)str.c_str(), str.size(), hash);

    char buf[SHA_DIGEST_LENGTH*2];
    for (int i = 0; i < SHA_DIGEST_LENGTH; i++)
        sprintf((char*)&(buf[i*2]), "%02x", hash[i]);

    return std::string(buf);
}

 参考网址:各种字符串Hash函数 - clq - 博客园 (cnblogs.com)

BKDR Hash的综合评分是最高的,ELF Hash的综合评分是最低的

为了泛型编程一般需要两种仿函数:处理可以强转为int的仿函数 和 处理不能强转为int的仿函数

//可以直接强转的类型,将它们强转为size_t类型,因为即使是整数也可能为负
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//这里采用的是BKDR Hash字符串哈希算法写的仿函数
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;
		}
		return hash;
	}
};

最终代码

#pragma once

#include <iostream>
#include <vector>
#include <utility>
using namespace std;


//将该仿函数放在了命名空间外,使得开散列和闭散列都可以使用该仿函数
//可以直接强转的类型,将它们强转为size_t类型,因为即使是整数也可能为负
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//对HashFunc采用特化处理,如果是int等就会用上面的仿函数,如果是string就会用该特化版本的HashFunc
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;
		}
		return hash;
	}
};


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

	template<class K, class V>
	struct HashDate
	{
		pair<K, V> _kv;
		State _state = EMPTY;//默认情况下为EMPTY
	};


	//处理字符串类型的强制类型转换
	struct StringHashFunc
	{
		size_t operator()(const string& key)
		{
			size_t hashi = 0;
			for (auto ch : key)
			{
				hashi = hashi * 131 + ch;
			}
			return hashi;
		}
	};


	template <class K, class V, class Hash = HashFunc<K>>//强转方式的缺省值是强转为整型
	class HashTable
	{
	public:
		//实例化哈希表时,将哈希表的大小和底层数组的容量均调整为10
		HashTable()
		{
			_table.resize(10);
		} 

		bool Insert(const pair<K, V>& kv)
		{
			//防止冗余
			if (Find(kv.first))
				return false;

			//当负载因子大于等于0.7时进行扩容,以空间换时间
			if (_n * 10 / _table.size() >= 0.7)//采用/取整得不到小数,所以直接令_n * 10 
			{
				//新建一个哈希表
				HashTable<K,V, Hash> newHT;
				//确定新哈希表的大小
				newHT._table.resize(_table.size() * 2);
				 
				//将旧表中的键重新映射到新表中
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state ==  EXIST)//原表中某下标的状态为存在时,将原表某下标中存放的键值对插入新表中
					{
						newHT.Insert(_table[i]._kv);//直接复用Insert操作
					}
				}
				_table.swap(newHT._table);
			}

			Hash sh;//处理强转为整型的仿函数

			size_t hashi = sh(kv.first) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数

			//线性探测
			while (_table[hashi]._state == EXIST)//探查到目标映射位置存在数字时就进行探测
			{
				++hashi;
				hashi %= _table.size();//取模的方式防止hashi越界
			}

			//确定好位置后进行插入
			_table[hashi]._kv = kv;//在合适的映射位置插入
			_table[hashi]._state = EXIST;//然后后将该位置的状态标识为EXIST
			++_n;//哈希表中有效数据元素+1
			return true;
		}


		HashDate<K, V>* Find(const K& key)
		{
			Hash hs;//处理强转为整型的仿函数

			//计算要查早的键的映射位置
			size_t hashi = hs(key) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数

			//线性探测
			while (_table[hashi]._state != EMPTY)//到映射位置上去查找,如果该位置上不为空,
			{
				if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以
					//错误的:if (_table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以
				{
					return &_table[hashi];//返回该位置的地址
				}
				++hashi;
				hashi %= _table.size();//取模的方式防止hashi越界
			}
			return nullptr;//找不到就返回空

		}


		bool Erase(const K& key)
		{
			HashDate<K, V>* ret = Find(key);
			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;//删除时只是将该位置的标识符置为了DELETE该位置上还存放着一个键,只不过该键可以被随时替换
				--_n;
				return true;
			}

		}


	private:
		vector<HashDate<K, V>> _table;//哈希表的大小和底层数组的容量都是通过 _table来管理的,正常情况下哈希表的大小_table.size和底层数组的容量是要分开的
		size_t _n;//哈希表中的有效数据个数
	};

	void TeshHT1()
	{
		int a[] = { 10001,11,55,24,19,12,31 };
		HashTable<int, int>  ht;//使用缺省的强转方式
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		cout << ht.Find(55) << endl;
		cout << ht.Find(31) << endl;

		ht.Erase(55);

		cout << ht.Find(55) << endl;
		cout << ht.Find(31) << endl;
	}

	void TeshHT2()
	{
		使用StringHashFunc
		//HashTable<string, int, StringHashFunc> ht;

		//ht.Insert(make_pair("sort", 1));
		//ht.Insert(make_pair("left", 1));
		//ht.Insert(make_pair("insert", 1));

		//查看是否可以用仿函数StringHashFunc将string转换为整型(与上面的内容无关)
		StringHashFunc()实例化一个匿名的仿函数类对象,然后("bacd")向该仿函数传参
		//cout << StringHashFunc()("bacd") << endl;
		//cout << StringHashFunc()("abcd") << endl;
		//cout << StringHashFunc()("aadd") << endl;
	

		//使用特化版本
		HashTable<string, int> ht;

		ht.Insert(make_pair("sort", 1));
		ht.Insert(make_pair("left", 1));
		ht.Insert(make_pair("insert", 1));

		//查看是否可以用仿函数的特化版本HashFunc<string>将string转换为整型
		cout << HashFunc<string>()("bacd") << endl;
		cout << HashFunc<string>()("abcd") << endl;
		cout << HashFunc<string>()("aadd") << endl;

	}

}

开散列

基本概念:将所有映射到相同哈希值的元素存储在一个链表或其他结构中,冲突发生时就将冲突位置的链表结点++,这样每个哈希表的桶实际上是一个指向链表头部的指针

优点:冲突处理简单,扩展容易,删除操作高效

缺点:需要额外的存储空间来存储链表节点,可能导致内存碎片

  • 可以是尾插但是尾插还要去寻找尾,而且发生冲突存入同一位置的结点也是有顺序的双向循环差入也行,但是没必要

插入

//采用头插的方式
bool Insert(const pair<K, V>& kv)
{
	//防止冗余
	if(Find(kv.first))
		return false;

	//如果还是重新建立映射关系,如果结点个数过多,在新表创建结点和移动后删除旧表的结点需要很多的资源
	//因为是队列是一个个挂在数组上的,所以当存放的有效元素个数 == 数组大小时才需要扩容
	//if (_n == _table.size())
	//{
	//	//新建一个哈希表
	//	HashTable<K, V> newHT;
	//	//确定新哈希表的大小
	//	newHT._table.resize(_table.size() * 2);

	//	//将旧表中的键重新映射到新表中
	//	for (size_t i = 0; i < _table.size(); i++)//每一次的for循环都是将某一个数组下标中的结点都链接到新表中
	//	{
	//		Node* cur = _table[i];
	//		while (cur)
	//		{
	//			newHT.Insert(_table[i]._kv);//直接复用Insert操作
	//			cur = cur->_next;
	//		}
	//	}
	//	_table.swap(newHT._table);//使用交换新旧表名中代表的地址信息
	//}

	//应该在确认了映射关系后,直接将旧表中的所有结点移动到新表中,这样移动后旧表中只用析构一个vector,且不用创建新的结点
	if (_n == _table.size())
	{
		vector<Node*> newTable(_table.size() * 2, nullptr);//设定新表大小及初始化各个位置
		for (size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];//cur指向旧表的下标为i位置的结点(该结点是一条链表的头结点)
					
			//遍历一条链表
			while (cur)
			{
				Node* next = cur->_next;//next存放cur的下一个结点的位置
				//确定将旧表cur指向的结点要头插新表的哪个位置
				size_t hashi = cur->_kv.first % newTable.size();
				cur->_next = newTable[hashi];//头插(忘了回去看外面的注释,这点就是有点会让人发懵)
				newTable[hashi] = cur;

				cur = next;//cur指向自己的下一个结点
			}

			_table[i] = nullptr;//置空
		}
		/*cout <<"交换前旧表的地址为:" <<  & _table << endl;
		cout <<"交换前新表的地址为:" <<  &newTable << endl;*/

		_table.swap(newTable);//交换

	/*	cout << "交换后旧表的地址为:" << &_table << endl;
		cout << "交换后新表的地址为:" << &newTable << endl;*/
	}

	size_t hashi = kv.first % _table.size();
	Node* newnode = new Node(kv);//匿名结点对象
	//头插(数组中某个位置没有结点插入时_table[hashi] == nullptr)
	newnode->_next = _table[hashi];//_新结点的next结点指向当前链表的头指针指向的结点
	_table[hashi] = newnode;//令链表头指针指向新结点
	++_n;
	return true;

	//哈希桶的头插类似于:
	//Node* head = nullptr; // 初始化一个空的链表,现在数组中每个位置都是一个空指针
	//void insertAtHead(int value) {
	//	Node* newNode = new Node(value); // 创建一个新节点
	//	newNode->next = head; // 将新节点的后继节点指向当前的头节点
	//	head = newNode; // 更新头节点指针,使其指向新节点
	//}
}

假设我们有以下插入过程:

  1. 初始状态

    • _tables 被初始化为大小为 10 的向量,每个元素都是 nullptr
    • 这意味着 _tables[0]_tables[9] 都是 nullptr
  2. 第一次插入

    • 插入键值对 (key1, value1)
    • 计算哈希值 hashi = key1 % 10,假设 hashi 为 2。
    • 创建新节点 newnode,其 _kv(key1, value1),且 _next 初始化为 nullptr
    • newnode->_next = _tables[hashi]; 此时 _tables[2]nullptr,因此 newnode->_next 也指向 nullptr
    • newnode 赋值给 _tables[hashi],即 _tables[2] = newnode
  3. 第二次插入

    • 插入键值对 (key2, value2)
    • 计算哈希值 hashi = key2 % 10,假设 hashi 仍然为 2。
    • 创建新节点 newnode,其 _kv(key2, value2),且 _next 初始化为 nullptr
    • newnode->_next = _tables[hashi]; 此时 _tables[2] 已经指向第一个节点(即存储 (key1, value1) 的节点),所以 newnode->_next 指向这个第一个节点。
    • newnode 赋值给 _tables[hashi],即 _tables[2] = newnode

通过这种方式,链表中的节点按插入顺序逆序排列,因为每次新节点都插入到链表的头部。

查看扩容过程的视频:20240526_213552-CSDN直播

结论:交换过程中结点的地址不发生改变,存放结点存放的位置一直在改变,且重新插入后一条链表中结点的先后顺序发生改变,原本的头变成了尾;交换结束后新旧表的地址不变使用&新表对象得到的依然是原地址,而不是旧表的地址,实际交换的是两个表的各个数据结构

查找

Node* Find(const K& key)
{
	size_t hashi = kv.first % _tables.size();
	Node* cur = _tables[hashi];//直接去映射位置上查找
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			return cur;
		}

		cur = cur->_next;
	}

	return nullptr;
}

删除

bool Erase(const K& key)
{
	Hash hs;

	size_t hashi = hs(key) % _table.size();
	Node* prev = nullptr;
	Node* cur = _table[hashi];//直接去映射位置上查找
	
    while (cur)
	{
		if (cur->_kv.first == key)
		{
            delete cur;
		}	
			
		else
		{
			prev = cur->_next;
			cur = prev;
		}
		cur = cur->_next;
	}
	return nullptr;
}

最终代码

namespace hash_bucket
{
	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 T>
	//struct HashNode
	//{
	//	T _data;
	//	HashNode<T>* _next;

	//	HashNode(const T& data)
	//		:_data(data)
	//		, _next(nullptr)
	//	{}
	//};

	template<class K,class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;

	public:
		HashTable()
		{
			_table.resize(10, nullptr);//初始化时,数组中有十个位置,每个位置都存放的是空指针
			_n = 0;
		}

		//哈希表的底层容器vector函数结束后自动析构,但是vector中各个位置存放的都是自定义类型Node,自定义类型的析构时会调用它们的析构函数,所以还要重新写一个析构函数
		~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;

			Hash hs;

			//如果还是重新建立映射关系,如果结点个数过多,在新表创建结点和移动后删除旧表的结点需要很多的资源
			//因为是队列是一个个挂在数组上的,所以当存放的有效元素个数 == 数组大小时才需要扩容
			//if (_n == _table.size())
			//{
			//	//新建一个哈希表
			//	HashTable<K, V> newHT;
			//	//确定新哈希表的大小
			//	newHT._table.resize(_table.size() * 2);

			//	//将旧表中的键重新映射到新表中
			//	for (size_t i = 0; i < _table.size(); i++)//每一次的for循环都是将某一个数组下标中的结点都链接到新表中
			//	{
			//		Node* cur = _table[i];
			//		while (cur)
			//		{
			//			newHT.Insert(_table[i]._kv);//直接复用Insert操作
			//			cur = cur->_next;
			//		}
			//	}
			//	_table.swap(newHT._table);//使用交换新旧表名中代表的地址信息
			//}

			//应该在确认了映射关系后,直接将旧表中的所有结点移动到新表中,这样移动后旧表中只用析构一个vector,且不用创建新的结点
			if (_n == _table.size())
			{
				vector<Node*> newTable(_table.size() * 2, nullptr);//设定新表大小及初始化各个位置
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];//cur指向旧表的下标为i位置的结点(该结点是一条链表的头结点)

					//遍历一条链表
					while (cur)
					{
						Node* next = cur->_next;//next存放cur的下一个结点的位置
						//确定将旧表cur指向的结点要头插新表的哪个位置
						size_t hashi = hs(cur->_kv.first) % newTable.size();
						cur->_next = newTable[hashi];//头插(忘了回去看外面的注释,这点就是有点会让人发懵)
						newTable[hashi] = cur;

						cur = next;//cur指向自己的下一个结点
					}

					_table[i] = nullptr;//置空
				}
				/*cout <<"交换前旧表的地址为:" <<  & _table << endl;
				cout <<"交换前新表的地址为:" <<  &newTable << endl;*/

				_table.swap(newTable);//交换

				/*	cout << "交换后旧表的地址为:" << &_table << endl;
					cout << "交换后新表的地址为:" << &newTable << endl;*/
			}

			size_t hashi = hs(kv.first) % _table.size();
			Node* newnode = new Node(kv);//匿名结点对象
			//头插(数组中某个位置没有结点插入时_table[hashi] == nullptr)
			newnode->_next = _table[hashi];//_新结点的next结点指向当前链表的头指针指向的结点
			_table[hashi] = newnode;//令链表头指针指向新结点
			++_n;
			return true;

			//哈希桶的头插类似于:
			//Node* head = nullptr; // 初始化一个空的链表,现在数组中每个位置都是一个空指针
			//void insertAtHead(int value) {
			//	Node* newNode = new Node(value); // 创建一个新节点
			//	newNode->next = head; // 将新节点的后继节点指向当前的头节点
			//	head = newNode; // 更新头节点指针,使其指向新节点
			//}
		}

		//链表的遍历
		Node* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(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)
		{
			Hash hs;

			size_t hashi = hs(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];//直接去映射位置上查找
			while (cur)
			{
				if (cur->_kv.first == key)
				{

					delete cur;
				}
				else
				{
					prev = cur->_next;
					cur = prev;
				}
				cur = cur->_next;
			}

			return nullptr;
		}

	private:
		vector<Node*> _table;//数组中的每个位置存放的都是结点的指针
		size_t _n;
	};

	void HashTest1()
	{
		int a[] = { 10001,11,55,24,19,12,31,4,34,44 };
			HashTable<int, int> ht;
			for (auto e : a)
			{
				ht.Insert(make_pair(e, e));
			}


			//ht.Insert(make_pair(32, 32));
			//ht.Insert(make_pair(32, 32));
	}

	void HashTest2()
	{
		HashTable<string, int> ht;
		ht.Insert(make_pair("sort", 1));
		ht.Insert(make_pair("left", 1));
		ht.Insert(make_pair("insert", 1));
	}
}

完整代码

#pragma once

#include <iostream>
#include <vector>
#include <utility>
using namespace std;


//将该仿函数放在了命名空间外,使得开散列和闭散列都可以使用该仿函数
//可以直接强转的类型,将它们强转为size_t类型,因为即使是整数也可能为负
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//对HashFunc采用特化处理,如果是int等就会用上面的仿函数,如果是string就会用该特化版本的HashFunc
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash = hash * 131 + ch;
		}
		return hash;
	}
};


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

	template<class K, class V>
	struct HashDate
	{
		pair<K, V> _kv;
		State _state = EMPTY;//默认情况下为EMPTY
	};


	//处理字符串类型的强制类型转换
	struct StringHashFunc
	{
		size_t operator()(const string& key)
		{
			size_t hashi = 0;
			for (auto ch : key)
			{
				hashi = hashi * 131 + ch;
			}
			return hashi;
		}
	};


	template <class K, class V, class Hash = HashFunc<K>>//强转方式的缺省值是强转为整型
	class HashTable
	{
	public:
		//实例化哈希表时,将哈希表的大小和底层数组的容量均调整为10
		HashTable()
		{
			_table.resize(10);
		} 

		bool Insert(const pair<K, V>& kv)
		{
			//防止冗余
			if (Find(kv.first))
				return false;

			//当负载因子大于等于0.7时进行扩容,以空间换时间
			if (_n * 10 / _table.size() >= 0.7)//采用/取整得不到小数,所以直接令_n * 10 
			{
				//新建一个哈希表
				HashTable<K,V, Hash> newHT;
				//确定新哈希表的大小
				newHT._table.resize(_table.size() * 2);
				 
				//将旧表中的键重新映射到新表中
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state ==  EXIST)//原表中某下标的状态为存在时,将原表某下标中存放的键值对插入新表中
					{
						newHT.Insert(_table[i]._kv);//直接复用Insert操作
					}
				}
				_table.swap(newHT._table);
			}

			Hash sh;//处理强转为整型的仿函数

			size_t hashi = sh(kv.first) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数

			//线性探测
			while (_table[hashi]._state == EXIST)//探查到目标映射位置存在数字时就进行探测
			{
				++hashi;
				hashi %= _table.size();//取模的方式防止hashi越界
			}

			//确定好位置后进行插入
			_table[hashi]._kv = kv;//在合适的映射位置插入
			_table[hashi]._state = EXIST;//然后后将该位置的状态标识为EXIST
			++_n;//哈希表中有效数据元素+1
			return true;
		}


		HashDate<K, V>* Find(const K& key)
		{
			Hash hs;//处理强转为整型的仿函数

			//计算要查早的键的映射位置
			size_t hashi = hs(key) % _table.size();//获取映射位置,应该键值的key % 数组中有效元素的个数

			//线性探测
			while (_table[hashi]._state != EMPTY)//到映射位置上去查找,如果该位置上不为空,
			{
				if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以
					//错误的:if (_table[hashi]._kv.first == key)//不为空还有两种删除和存在两种情况,需要满足位置的状态为存在时才可以
				{
					return &_table[hashi];//返回该位置的地址
				}
				++hashi;
				hashi %= _table.size();//取模的方式防止hashi越界
			}
			return nullptr;//找不到就返回空

		}


		bool Erase(const K& key)
		{
			HashDate<K, V>* ret = Find(key);
			if (ret == nullptr)
			{
				return false;
			}
			else
			{
				ret->_state = DELETE;//删除时只是将该位置的标识符置为了DELETE该位置上还存放着一个键,只不过该键可以被随时替换
				--_n;
				return true;
			}

		}


	private:
		vector<HashDate<K, V>> _table;//哈希表的大小和底层数组的容量都是通过 _table来管理的,正常情况下哈希表的大小_table.size和底层数组的容量是要分开的
		size_t _n;//哈希表中的有效数据个数
	};

	void TeshHT1()
	{
		int a[] = { 10001,11,55,24,19,12,31 };
		HashTable<int, int>  ht;//使用缺省的强转方式
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		cout << ht.Find(55) << endl;
		cout << ht.Find(31) << endl;

		ht.Erase(55);

		cout << ht.Find(55) << endl;
		cout << ht.Find(31) << endl;
	}

	void TeshHT2()
	{
		使用StringHashFunc
		//HashTable<string, int, StringHashFunc> ht;

		//ht.Insert(make_pair("sort", 1));
		//ht.Insert(make_pair("left", 1));
		//ht.Insert(make_pair("insert", 1));

		//查看是否可以用仿函数StringHashFunc将string转换为整型(与上面的内容无关)
		StringHashFunc()实例化一个匿名的仿函数类对象,然后("bacd")向该仿函数传参
		//cout << StringHashFunc()("bacd") << endl;
		//cout << StringHashFunc()("abcd") << endl;
		//cout << StringHashFunc()("aadd") << endl;
	

		//使用特化版本
		HashTable<string, int> ht;

		ht.Insert(make_pair("sort", 1));
		ht.Insert(make_pair("left", 1));
		ht.Insert(make_pair("insert", 1));

		//查看是否可以用仿函数的特化版本HashFunc<string>将string转换为整型
		cout << HashFunc<string>()("bacd") << endl;
		cout << HashFunc<string>()("abcd") << endl;
		cout << HashFunc<string>()("aadd") << endl;

	}

}

namespace hash_bucket
{
	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 T>
	//struct HashNode
	//{
	//	T _data;
	//	HashNode<T>* _next;

	//	HashNode(const T& data)
	//		:_data(data)
	//		, _next(nullptr)
	//	{}
	//};

	template<class K,class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;

	public:
		HashTable()
		{
			_table.resize(10, nullptr);//初始化时,数组中有十个位置,每个位置都存放的是空指针
			_n = 0;
		}

		//哈希表的底层容器vector函数结束后自动析构,但是vector中各个位置存放的都是自定义类型Node,自定义类型的析构时会调用它们的析构函数,所以还要重新写一个析构函数
		~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;

			Hash hs;

			//如果还是重新建立映射关系,如果结点个数过多,在新表创建结点和移动后删除旧表的结点需要很多的资源
			//因为是队列是一个个挂在数组上的,所以当存放的有效元素个数 == 数组大小时才需要扩容
			//if (_n == _table.size())
			//{
			//	//新建一个哈希表
			//	HashTable<K, V> newHT;
			//	//确定新哈希表的大小
			//	newHT._table.resize(_table.size() * 2);

			//	//将旧表中的键重新映射到新表中
			//	for (size_t i = 0; i < _table.size(); i++)//每一次的for循环都是将某一个数组下标中的结点都链接到新表中
			//	{
			//		Node* cur = _table[i];
			//		while (cur)
			//		{
			//			newHT.Insert(_table[i]._kv);//直接复用Insert操作
			//			cur = cur->_next;
			//		}
			//	}
			//	_table.swap(newHT._table);//使用交换新旧表名中代表的地址信息
			//}

			//应该在确认了映射关系后,直接将旧表中的所有结点移动到新表中,这样移动后旧表中只用析构一个vector,且不用创建新的结点
			if (_n == _table.size())
			{
				vector<Node*> newTable(_table.size() * 2, nullptr);//设定新表大小及初始化各个位置
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];//cur指向旧表的下标为i位置的结点(该结点是一条链表的头结点)

					//遍历一条链表
					while (cur)
					{
						Node* next = cur->_next;//next存放cur的下一个结点的位置
						//确定将旧表cur指向的结点要头插新表的哪个位置
						size_t hashi = hs(cur->_kv.first) % newTable.size();
						cur->_next = newTable[hashi];//头插(忘了回去看外面的注释,这点就是有点会让人发懵)
						newTable[hashi] = cur;

						cur = next;//cur指向自己的下一个结点
					}

					_table[i] = nullptr;//置空
				}
				/*cout <<"交换前旧表的地址为:" <<  & _table << endl;
				cout <<"交换前新表的地址为:" <<  &newTable << endl;*/

				_table.swap(newTable);//交换

				/*	cout << "交换后旧表的地址为:" << &_table << endl;
					cout << "交换后新表的地址为:" << &newTable << endl;*/
			}

			size_t hashi = hs(kv.first) % _table.size();
			Node* newnode = new Node(kv);//匿名结点对象
			//头插(数组中某个位置没有结点插入时_table[hashi] == nullptr)
			newnode->_next = _table[hashi];//_新结点的next结点指向当前链表的头指针指向的结点
			_table[hashi] = newnode;//令链表头指针指向新结点
			++_n;
			return true;

			//哈希桶的头插类似于:
			//Node* head = nullptr; // 初始化一个空的链表,现在数组中每个位置都是一个空指针
			//void insertAtHead(int value) {
			//	Node* newNode = new Node(value); // 创建一个新节点
			//	newNode->next = head; // 将新节点的后继节点指向当前的头节点
			//	head = newNode; // 更新头节点指针,使其指向新节点
			//}
		}

		//链表的遍历
		Node* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(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)
		{
			Hash hs;

			size_t hashi = hs(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];//直接去映射位置上查找
			while (cur)
			{
				if (cur->_kv.first == key)
				{

					delete cur;
				}
				else
				{
					prev = cur->_next;
					cur = prev;
				}
				cur = cur->_next;
			}

			return nullptr;
		}

	private:
		vector<Node*> _table;//数组中的每个位置存放的都是结点的指针
		size_t _n;
	};

	void HashTest1()
	{
		int a[] = { 10001,11,55,24,19,12,31,4,34,44 };
			HashTable<int, int> ht;
			for (auto e : a)
			{
				ht.Insert(make_pair(e, e));
			}


			//ht.Insert(make_pair(32, 32));
			//ht.Insert(make_pair(32, 32));
	}

	void HashTest2()
	{
		HashTable<string, int> ht;
		ht.Insert(make_pair("sort", 1));
		ht.Insert(make_pair("left", 1));
		ht.Insert(make_pair("insert", 1));
	}
}

~over~

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

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

相关文章

浅谈Docker容器的网络通信原理

文章目录 1、回顾容器概念2、容器网络3、容器与主机之间的网络连通4、交换机的虚拟实现---虚拟网桥&#xff08;Bridge&#xff09;5、Docker 守护进程daemon管理容器网络 1、回顾容器概念 我们知道容器允许我们在同一台宿主机&#xff08;电脑&#xff09;上运行多个服务&…

【蓝桥杯省赛真题44】python计算N+N的值 中小学青少年组蓝桥杯比赛 算法思维python编程省赛真题解析

目录 python计算NN的值 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python计算NN的值 第十四届蓝桥杯青少年组python省赛真题 一、题目要求…

VMware安装Ubuntu系统(超详细)

一.Ubuntu官网下载镜像 Ubuntu官网&#xff1a;Enterprise Open Source and Linux | Ubuntu 二.安装Ubuntu系统 选择文件->创建虚拟机新建虚拟机&#xff08;ControlN&#xff09;&#xff0c;这里直接选择典型即可 选择稍后安装系统 选择linux Ubuntu 64位 填写虚拟机名称…

PanTools v1.0.25 多网盘批量管理工具 批量管理、分享、转存、重命名、复制...

一款针对多个热门网盘的文件管理、批量分享、批量转存、批量复制、批量重命名、批量链接检测、跨账号移动文件、多账号文件搜索等&#xff0c;支持不同网盘的不同账号的资源文件操作。适用于网站站长、资源爱好者等&#xff0c;对于管理名下具有多个网盘多个账号具有实用的效果…

这方法真牛B!论文降重从81%直降1.9%

目录 一、万字论文&#xff0c;从0到1&#xff0c;只需1小时二、获取途径三、论文从81&#xff05;降到1.9&#xff05;四、内容是别人的&#xff0c;话是自己的五、AI工具 --> 中文论文降重六、论文降重小技巧 一、万字论文&#xff0c;从0到1&#xff0c;只需1小时 通过O…

Github 2024-05-27 开源项目周报Top15

根据Github Trendings的统计,本周(2024-05-27统计)共有15个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量TypeScript项目6Rust项目3Python项目3JavaScript项目3Java项目1C#项目1C++项目1Cuda项目1C项目1Lua项目1JavaScript算法与数据结构 创建周期:2…

读人工智能时代与人类未来笔记15_改变人类经验

1. 认识世界的方式 1.1. 信仰 1.2. 理性 1.2.1. 理性不仅革新了科学&#xff0c;也改变了我们的社会生活、艺术和信仰 1.2.2. 在其浸染之下&#xff0c;封建等级制度瓦解了&#xff0c;而民主&#xff0c;即理性的人应该自治的理念崛起了 1.3. 人工智能 1.3.1. 这种转变将…

关于我转生从零开始学C++这件事:升级Lv.25

❀❀❀ 文章由不准备秃的大伟原创 ❀❀❀ ♪♪♪ 若有转载&#xff0c;请联系博主哦~ ♪♪♪ ❤❤❤ 致力学好编程的宝藏博主&#xff0c;代码兴国&#xff01;❤❤❤ OK了老铁们&#xff0c;又是一个周末&#xff0c;大伟又来继续给大家更新我们的C的内容了。那么根据上一篇博…

Spring Boot 项目统一异常处理

在 Spring Boot 项目开发中&#xff0c;异常处理是一个非常重要的环节。良好的异常处理不仅能提高应用的健壮性&#xff0c;还能提升用户体验。本文将介绍如何在 Spring Boot 项目中实现统一异常处理。 统一异常处理有以下几个优点&#xff1a; 提高代码可维护性&#xff1a;…

每日AIGC最新进展(12):在舞蹈视频生成中将节拍与视觉相融合、Text-to-3D综述、通过内容感知形状调整进行 3D 形状增强

Diffusion Models专栏文章汇总&#xff1a;入门与实战 Dance Any Beat: Blending Beats with Visuals in Dance Video Generation https://DabFusion.github.io 本文提出了一种名为DabFusion的新型舞蹈视频生成模型&#xff0c;该模型能够根据给定的静态图像和音乐直接生成舞蹈…

优化FPGA SelectIO接口VREF生成电路

引言&#xff1a;FPGA设计中使用了各种PCB SelectIO™接口VREF生成电路。有时即使在以前的设计中已经成功的在电路板上设计了VREF生成电路&#xff0c;也会在VREF引脚上发现大量噪声&#xff08;200–400mV&#xff09;。大量VREF噪声的存在可能导致高性能SelectIO接口&#xf…

Jenkins部署成功后自动发通知到钉钉群

钉钉上如何配置 选择钉钉群&#xff0c;找到群设置-机器人-添加机器人 选择自定义 选择【添加】 选择【加签】&#xff0c;复制值&#xff0c;后续在jenkins里配置时会用到 复制Webhook地址&#xff0c;后面在jenkins里配置的时候要用到 Jenkins上如何配置 系统管理-插件管…

Vue3实战笔记(46)—Vue 3高效开发定制化Dashboard的权威手册

文章目录 前言Dashboard开发总结 前言 后台管理系统中的Dashboard是一种图形化的信息显示工具&#xff0c;通常用于提供一个特定领域或系统的概况。它可以帮助用户监控和分析数据&#xff0c;快速获取重要信息。可以帮助用户监控业务状况、分析数据、获取关键信息和管理资源。…

PyTorch学习笔记:新冠肺炎X光分类

前言 目的是要了解pytorch如何完成模型训练 https://github.com/TingsongYu/PyTorch-Tutorial-2nd参考的学习笔记 数据准备 由于本案例目的是pytorch流程学习&#xff0c;为了简化学习过程&#xff0c;数据仅选择了4张图片&#xff0c;分为2类&#xff0c;正常与新冠&#xf…

Golang | Leetcode Golang题解之第114题二叉树展开为链表

题目&#xff1a; 题解&#xff1a; func flatten(root *TreeNode) {curr : rootfor curr ! nil {if curr.Left ! nil {next : curr.Leftpredecessor : nextfor predecessor.Right ! nil {predecessor predecessor.Right}predecessor.Right curr.Rightcurr.Left, curr.Righ…

95.网络游戏逆向分析与漏洞攻防-ui界面的设计-ui的设计与架构

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 如果看不懂、不知道现在做的什么&#xff0c;那就跟着做完看效果&#xff0c;代码看不懂是正常的&#xff0c;只要会抄就行&#xff0c;抄着抄着就能懂了 内容…

JVM之【运行时数据区】

JVM简图 运行时数据区简图 一、程序计数器&#xff08;Program Counter Register&#xff09; 1.程序计数器是什么&#xff1f; 程序计数器是JVM内存模型中的一部分&#xff0c;它可以看作是一个指针&#xff0c;指向当前线程所执行的字节码指令的地址。每个线程在执行过程中…

通过acme.sh和cloudflare实现免费ssl证书自动签发

参考使用acme.sh通过cloudflare自动签发免费ssl证书 | LogDicthttps://www.logdict.com/archives/acme.shshi-yong-cloudflarezi-dong-qian-fa-mian-fei-sslzheng-shu

服务器数据恢复—服务器正常断电重启后raid信息丢失的数据恢复案例

服务器数据恢复环境&#xff1a; 一台某品牌DL380 G4服务器&#xff0c;服务器通过该服务器品牌smart array控制器挂载了一台国产的磁盘阵列&#xff0c;磁盘阵列中有一组由14块SCSI硬盘组建的RAID5。服务器安装LINUX操作系统&#xff0c;搭建了NFSFTP&#xff0c;作为内部文件…

ROS添加GDB调试

文章目录 一、问题描述二、配置步骤1. debug 模式编译2. rosrun 添加GDB指令3. launch 添加GDB指令 三、GDB基本命令1. 基本2. 显示被调试文件信息3. 查看/修改内存4. 断点5. 调试运行 一、问题描述 在享受ROS带来便利的同时&#xff0c;但因每运行出现错误&#xff0c;ROS不会…