数据结构与算法:哈希表

news2025/1/11 17:02:22

目录

1.哈希表和哈希

1.1.知识引入

1.2.为什么需要哈希表呢?

2.简易的哈希表

2.1.哈希表的基础结构

2.2.如何实现基础的哈希表

2.2.1.增

2.2.2.删 

 2.2.3.查

2.3.泛型编程下的哈希表

3.简易的哈希桶


1.哈希表和哈希

1.1.知识引入

 哈希表(Hash Table)是一种基于哈希技术实现的数据结构,它通过将键映射到存储位置来实现高效的数据访问。哈希表通常由一个数组和一个哈希函数组成。当需要插入、查找或删除数据时,通过哈希函数计算键的哈希值,并将该值作为索引在数组中查找或操作对应的数据。 

哈希(Hash)是一种将数据映射到固定大小的唯一值的技术。

 哈希的本质就是一种映射关系!哈希函数是实现哈希的方式!

例如:

  • 我们可以通过取模操作将15映射到5这个数字,
  • 我们也可以通过字符对应的ASCII值相加,将hello映射到数字532

只要实现了映射关系这样子就能称为 “哈希” ,而这种映射的实现叫做哈希函数,是使得不同的输入数据产生不同的哈希值,同时尽量减少不同输入数据产生相同哈希值的概率。  

这里值得注意的是:

  • 我们无法避免哈希值产生冲突,也就是不同的key可能对应着一样的哈希值,但是我们清楚如果过分的出现哈希值冲突,会影响哈希表的作用
  • 我们能做到的只是优化哈希函数来尽量减少哈希冲突

1.2.为什么需要哈希表呢?

首先对于大部分的数据结构,每一次我们查找时总是无法避免遍历整个结构,这样子的效率总是会有一点点低下的,因此哈希表这个数据结构就诞生了,它的O(1)时间复杂度,高性能,可以实现多样的结构,提高实际开发的效率

  1. 高效的查找操作:通过哈希函数将键映射到数组中的位置,可以快速定位到对应的值,时间复杂度为O(1)。相比于其他数据结构如数组或链表,哈希表的查找速度更快。

  2. 快速的插入和删除操作:同样通过哈希函数定位到位置后,可以直接插入或删除对应的值,时间复杂度也是O(1)。这使得哈希表在需要频繁插入和删除元素的场景下非常高效。

  3. 空间利用率高:哈希表使用数组来存储数据,相比于其他数据结构如树,它不需要额外的指针来连接节点,因此空间利用率更高。

  4. 适用于大规模数据:哈希表在处理大规模数据时仍然能够保持较高的性能。通过合理选择哈希函数和调整数组大小,可以减少冲突的概率,提高哈希表的效率。

并且它通过将输入数据(也称为键)通过哈希函数转换成一个固定长度的哈希值(也称为散列值),并将该哈希值与存储空间进行关联。

2.简易的哈希表

2.1.哈希表的基础结构

哈希表的简易原理:

  • 哈希表分为 闭散列 和 开散列(哈希桶) 两种结构,闭散列就是一段连续的有限空间
  • 哈希表内对于元素位置的判断为 “空位置” “删除位置” “存在位置”
  • 哈希表需要存在一定数量的空位置,当我们访问到空时就可以退出,当我们删除了存在的数据时,需要设置该位置为删除,如果设置为空,就会导致查找到部分就退出了
  • 空的位置越多哈希表的效率越高,但是随之空间浪费越大,空的位置越少,哈希表的效率越低,越接近于完全遍历的数据结构,失去了哈希表的优势

那么我们就抽象出来哈希表的简易结构!

代码实现结构如下: 

// 枚举类型实现哈希表节点的三种状态
enum STATUS
{
	EMPTY,
	EXIST,
	DELETE
};

template<class K, class V>
// 定义一个哈希个体的结构体
struct HashData
{
	pair<K, V> _kv;
	STATUS _status = EMPTY;
};

template<class K, class V>

class HashTable
{
public:
    // 定义默认空间
	HashTable() { _table.resize(10); }

	// 增
	bool Insert(const pair<K, V>& kv) 
    {
        // 具体实现
    }
	// 删
	bool Erase(const K& key) 
    {
        // 具体实现
    }
	// 查
	HashData<K, V>* Find(const K& key)
    {
        // 具体实现
    }

private:
    // 用数组这个数据结构来存放若干个哈希节点结构体
    // 数组实现的哈希表
	vector<HashData<K, V>> _table;
	// 节点数
	size_t _num = 0;
};

2.2.如何实现基础的哈希表

这一部分我们主要是对代码进行剖析,注重理解,摸清原理,临摹一份哈希表,从增删查开始!!

2.2.1.增

基本的增加数据的实现

bool Insert(const pair<K, V>& kv)
{
    // 哈希函数
	size_t Hash_i = kv.first % _table.size();
	// 不为空向后走 
	while (_table[Hash_i]._status == EXIST)
	{
		Hash_i++;
		// 超过capacity就取模回来
		Hash_i %= _table.size();
	}
	// 找到 空 或者 删除 位置可以插入
	_table[Hash_i]._kv = kv;
	_table[Hash_i]._status = EXIST;

	return true;
}

这一段代码中:

  • 哈希函数的实现是通过传入的pair值的key对总的size取模,来获得位置,整体逻辑就是如果一个位置存在数据,那么我们就不能插入,需要向后走,直到找到空位置,退出循环进行插入,并返回true
  • 当我们不断地向后遍历,发现等于数组的长度时,也就是Hash_i = 10时,这时从0下标开始重新遍历

那么简易的原理就很容易接受了,但是我们发现两个问题

  1. 如果加入大量的数据,这个大小显然是不够的,所以需要扩容
  2. 缺少了返回false的情况,这里需要引入查找这个模块,我们后面解决

对于问题一,我们首先引入一个存储哈希key个数的变量

我们在上面讲过我们需要控制哈希表中 “空” 位置的数量,一般来说我们通过负载因子来实现,随着数据的插入,num的就会增加,随之空位置数目就减少,负载因子增加,所以哈希表会设定一个负载因子的最大值,当超过这个值时,哈希表会进行扩容!!!

完整的代码如下:

bool Insert(const pair<K, V>& kv)
{
    // 找不到就退出
	if (Find(key) != nullptr)
		return false;

    // double load_fator = (double)_num / (double)_table.size();

	// 扩容,当负载因子lf超过0.7时 进行扩容
	if (_num * 10 / _table.size() == 7)
	{
		HashTable<K, V> newHT;
        // 扩容两倍
		newHT._table.resize(_table.size() * 2);

		// 遍历旧的哈希表 
		// 因为扩容后,负载因子不会超过0.7 直接进入插入操作
		for (size_t i = 0; i < _table.size(); i++)
		{
            // 将旧表的数据插入新表中
			if (_table[i]._status == EXIST)
				newHT.Insert(_table[i]._kv);
		}

		// 将新表覆盖旧表
		_table.swap(newHT._table);
	}

	size_t Hash_i = kv.first % _table.size();
	// 不为空向后走 
	while (_table[Hash_i]._status == EXIST)
	{
		Hash_i++;
		// 超过capacity就取模回来
		Hash_i %= _table.size();
	}
	// 找到 空 或者 删除 位置可以插入
	_table[Hash_i]._kv = kv;
	_table[Hash_i]._status = EXIST;
	_num++;

	return true;

}

代码逻辑:

  • 首先我们通过Find方法来判断有没有重复的键(key),因为这里对应的是unorder_set不允许重复的键,如果找不到这个key就进行插入
  • 当我们需要扩容时,我们通过这个负载因子进行判断,如果负载过大为了哈希表的效率,我们需要扩容,当然我们可以按照我们插入的逻辑来实现这个扩容!!!可是我们会发现原本的13%10=3,现在13%20=13,也就是插入的位置发生变化。那么我们就需要重新对这个哈希表进行调整,所以这里我们通过新增一个哈希表,间接的将数据插入到新表中,再通过交换指针来实现表的替换(本质上是代码的复用!!!)

到了这里哈希表的“增”就基本大功告成了!!!

2.2.2.删 

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

	// 注意这里是 伪删除 (我们并没有将对应数据完全删去)
	HashData<K, V>* del = Find(key);
	del->_status = DELETE;
	_num--;
	return true;
}

没什么好说的就是在结构体数组(哈希表)找对应key结构体的地址,然后删除。值得注意的是,这里的删除我们只是修改了“状态”和减少了“key”的个数,这个位置上的数据并没有删去,再结合“增”中的逻辑:不为空就可以插入,那么这里的删除只是为了将状态从存在转为删除,是一种伪删除,目的是通过插入来实现覆盖式的插入。

 2.2.3.查

查找我们需要注意:

  • 在哪里查找
  • 要找到什么

首先我们知道查找一个数据存不存在,肯定是访问一个结构体对象的状态是否为EXIST,但是上面我们再删除中讲了这个删除是个伪删除,也就是当我们删除了key=3这个结构体,在这个结构体数组中还会存在key=3吗?答案是会的这个结构体为 DELETE,但是key=3,所以我们需要在删除和存在中查找。

这里也体现了哈希表的特殊之处,我们之前回想一下,是不是会疑惑为什么哈希表不只设置“存在”和“空”两种状态,这样子不是更加简洁吗?首先哈希表在遍历时,遇到空就退出,如果只有“空”和“存在”

当我们删除节点43后,会发现当遍历到33时发现下一个节点为空(因为只有“空”和“存在”),那么就会退出。具体一点当我们查找key=53时,Hash_i=3,也就是从下标为3开始判断,最终从43处退出,并且结果是找不到。

当我们知道了需要在非空中找key时,我们继续思考我们要找什么,有人可能会说不就是找key等于kv,first的情况么? 

// 查
HashData<K, V>* Find(const K& key)
{
	size_t Hash_i = key % _table.size();

	// 为空时退出查找
	while (_table[Hash_i]._status != EMPTY)
	{
		if (_table[Hash_i]._kv.first == key)
			return &_table[Hash_i];
		Hash_i++;
	}
	return nullptr;
}

 我们来看一下这个样例,

这时我们可能还是有点不太理解,结合一下我们在删除模块对删除定性是一个“伪删除”,我们如醍醐灌顶般发现其实这个key=3还在这个哈希表中等待被人覆盖,如果插入的是(3,3)那么此时就会发生冲突(默认不允许同名的key)j,因为没有完全删除。当我们先插入(33,33)时,通过哈希函数已经覆盖了key=3所在的位置,所以后续可以正常插入!!!

所以我们需要优化一下这个函数,也就是回到了找什么这个问题!不能只找key=kv.first这个节点,我们还需要判断这个key对应的区域不为删除!

// 查
HashData<K, V>* Find(const K& key)
{
	size_t Hash_i = key % _table.size();

	// 为空时退出查找
	while (_table[Hash_i]._status != EMPTY)
	{
		if (_table[Hash_i]._kv.first == key && _table[Hash_i]._status != DELETE)
			return &_table[Hash_i];
		Hash_i++;
	}
	return nullptr;
}

 当然这里还是有点抽象!

主要是这里查找的逻辑需要满足增和删这两种情况,因为DELETE状态只是为了实现哈希表特性专门在EXIST和EMPTY中间的临时状态,仅仅为了哈希表为空时退出的保护!!!

2.3.泛型编程下的哈希表

我们回到我们的哈希函数

这里我们通过kv.first来对size取模,这种思路只能符合整型类型的key,但是实际上我们哈希表是一种高效存储的数据结构,就避免不了存储string类型,甚至是自定义类型,这些类型可以取模吗???当然是不行的,这也就是哈希函数存在的意义了,实现不同类型的哈希映射,来实现不同的哈希表!!!

 因此借助仿函数和全特化语法来实现不同的类型进入不同的路径,实现不同的哈希关系。

// 枚举类型实现哈希表节点的三种状态
enum STATUS
{
	EMPTY,
	EXIST,
	DELETE
};

template<class K>
struct HashFunc
{
    size_t operator()(const K& key)
    {
        return (size_t)key;
    }
    // 也可以通过函数重载来实现,不过需要设置多种类型
};
// 特化
template<>
struct HashFunc<string>
{
    size_t operator()(const string& key)
    {
        size_t hash = 0;
        for(auto e : key)
        {
            hash *= 31;
            hash += e;    
        }
        return hash;
    }
};


template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	STATUS _status = EMPTY;
};

template<class K, class V, class Hash = HashFunc<K>>

class HashTable
{
public:
    // 定义默认空间
	HashTable() { _table.resize(10); }

	// 增
	bool Insert(const pair<K, V>& kv) 
    {
        // 具体实现
    }
	// 删
	bool Erase(const K& key) 
    {
        // 具体实现
    }
	// 查
	HashData<K, V>* Find(const K& key)
    {
        // 具体实现
    }

private:
    // 用数组这个数据结构来存放若干个哈希节点结构体
    // 数组实现的哈希表
	vector<HashData<K, V>> _table;
	// 节点数
	size_t _num = 0;
};

这样子我们就完成了泛型编程下哈希表的结构了!!!

完整的代码


enum STATUS
{
	EMPTY,
	EXIST,
	// 哈希的空 和 删除 不等价,删除只是为了 不为空 为了哈希的合理性
	DELETE
};

template<class K>
struct HashFunc
{
	size_t operator()(const size_t& key)
	{
		return (size_t)key;
	}
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
            // 对哈希映射关系的实现
            // 防止"abc"和"acb"的哈希关系重复
			hash *= 31;
			hash += e;
		}
		return hash;
	}
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	STATUS _status = EMPTY;
};

template<class K, class V, class HashFunc = HashFunc<K>>
class HashTable
{
public:
	HashTable()
	{
		_table.resize(10);
	}

	// 增
	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first) != nullptr)
			return false;

		// 扩容,当负载因子lf超过0.7时 进行扩容
		if (_num * 10 / _table.size() == 7)
		{
			size_t newSize = _table.size() * 2;
			HashTable<K, V> newHT;
			newHT._table.resize(newSize);

			// 遍历旧的哈希表 
			// 因为扩容后,负载因子不会超过0.7 直接进入插入操作
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]._status == EXIST)
					newHT.Insert(_table[i]._kv);
			}

			// 将新表覆盖旧表
			_table.swap(newHT._table);
		}
        // 需要设置仿函数对key进行操作
		HashFunc hs_func;
		size_t Hash_i = hs_func(kv.first) % _table.size();
		// 不为空向后走 
		while (_table[Hash_i]._status == EXIST)
		{
			Hash_i++;
			// 超过capacity就取模回来
			Hash_i %= _table.size();
		}
		// 找到 空 或者 删除 位置可以插入
		_table[Hash_i]._kv = kv;
		_table[Hash_i]._status = EXIST;
		_num++;

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

		// 注意这里是 伪删除 (我们并没有将对应数据完全删去)
		HashData<K, V>* del = Find(key);
		del->_status = DELETE;
		_num--;
		return true;
	}
	// 查
	HashData<K, V>* Find(const K& key)
	{
        // 需要设置仿函数对key进行操作
		HashFunc hs_func;
		size_t Hash_i = hs_func(key) % _table.size();

		// 为空时退出查找 存在和删除 中寻找
		while (_table[Hash_i]._status != EMPTY)
		{
			if (_table[Hash_i]._kv.first == key && _table[Hash_i]._status != DELETE) { return &_table[Hash_i]; }

			Hash_i++;
		}
		return nullptr;
	}



private:
	vector<HashData<K, V>> _table;
	// 哈希表中key的个数
	size_t _num = 0;
};

因为我们设置仿函数,所以需要对key进行操作,不同的类型,使用对应的不同的哈希函数。

另外对于哈希映射,我们如果只是单纯的进行ASCII值相加会出现“abc”和“acb”的哈希值一致,所以我们常常需要针对不同的类型,进行不同的算法设计,来减少哈希冲突的发生。

详细见:经典字符串hash函数介绍及性能比较_hash的性能比较-CSDN博客

那么我们就可以对这个哈希表的学习进行“完结撒花”了!!!

3.简易的哈希桶

我们发现哈希表的这个结构效率还不是很高,毕竟是通过数组来实现的,也就是一个闭散列,而实际上一般哈希表的实现是通过哈希桶这个结构的,也就是开散列,那什么是哈希桶呢?

接下来我们就要开始哈希桶的篇章了。

首先我们提出一个问题:在哈希桶中我们分别插入1,91,3,33,43,53,5,7这几个数据后,哈希桶的逻辑结构是怎么样的?

哈希桶的本质就是实现一个存储桶的首个节点的地址的指针数组,桶的本质就是一个单向链表。当我们对这个哈希桶进行访问的本质就是:指针数组对应下标是否为空,不为空就是访问桶内的数据,为空就是向后寻找。

实现代码如下,因为主要的逻辑就是增加一个链表结构,并没有太大的难度,并且基本的结构在简易哈希表部分就已经讲的很明白了,但是不同的是这里不再需要像哈希表那样判断3种状态,这一块我们在下一篇博客中再次讲解!!!

// 仿函数!!!
struct HashFunc
{
	size_t operator()(const size_t& key)
	{
		return (size_t)key;
	}
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}
		return hash;
	}
	// 也可以通过函数重载来实现,不过需要设置多种类型
};

template<class K, class V>
struct HashNode
{
	HashNode* _next;
	pair<K, V> _kv;

	HashNode(const pair<K, V>& kv)
		:_kv(kv)
		, _next(nullptr)
	{}

};

template<class K, class V, class Hash = HashFunc>

class HashTable
{
	typedef HashNode<K, V> HashNode;
public:
	// 定义默认空间
	HashTable() { _table.resize(10); }
	// 析构,释放空间
    ~HashTable()
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			HashNode* current = _table[i];
			HashNode* next = nullptr;
			while (current != nullptr)
			{
				next = current->_next;
				delete current;
				current = next;
			}
			_table[i] = nullptr;
		}
	}
	// 增
	bool Insert(const pair<K, V>& kv)
	{
		Hash hs_func;
		// 当负载因子为1时才进行扩容
		if (_num == _table.size())
		{
			vector<HashNode*> newT;
			newT.resize(_table.size() * 2);

			for (size_t i = 0; i < _table.size(); i++)
			{
				HashNode* current = _table[i];
				HashNode* next = nullptr;

				// 将旧表的内容插入进新表
				while (current != nullptr)
				{
					next = current->_next;
					// 头插逻辑
					size_t Hash_i = hs_func(current->_kv.first) % newT.size();
					current->_next = newT[Hash_i];
					current = next;
				}
				_table[i] = nullptr;
			}

			_table.swap(newT);
		}

		size_t Hash_i = hs_func(kv.first) % _table.size();
		HashNode* newNode = new HashNode(kv);

		// 头插(针对哈希桶)
		// 新节点的下一个节点就是原本的头节点
		newNode->_next = _table[Hash_i];
		_table[Hash_i] = newNode;
		_num++;

		return true;
	}
	// 删
	bool Erase(const K& key)
	{
		Hash hs_func;
		size_t Hash_i = hs_func(key) % _table.size();
		HashNode* current = _table[Hash_i];
		HashNode* prev = nullptr;
		while (current != nullptr)
		{
			if (current->_kv.first == key)
			{
				if (prev == nullptr)
				{
					_table[Hash_i] = current->_next;
				}
				else
				{
					prev->_next = current->_next;
				}

				delete current;

				return true;
			}
			prev = current;
			current = current->_next;
		}

		return false;
	}
	// 查
	HashNode* Find(const K& key)
	{
		Hash hs_func;
		size_t Hash_i = hs_func(key) % _table.size();
		HashNode* current = _table[Hash_i];

		while (current != nullptr)
		{
			if (current->_kv.first == key)
				return current;

			current = current->_next;
		}

		return nullptr;
	}

private:
	// 指针数组存储哈希桶的首元素地址
	vector<HashNode*> _table;
	// 节点数
	size_t _num = 0;
};

注意这里的插入部分,实现的是头插!也就是不断地修改桶的头结点指针,再进行连接。当我们需要扩容时,也就是负载因子过大时,需要将数据分散。

注意这里只是粗略显示如何进行扩容,并不是实际的场景!!!

 另外我们需要知道,对于内置类型vector会在堆区开辟空间,我们需要释放它的资源,防止出现内存泄漏问题。那么到了这里哈希桶我们也实现了!!!

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

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

相关文章

面试必问!鸿蒙开发中的FA模型和Stage模型是什么?他们分别有什么区别?

鸿蒙OS&#xff08;HarmonyOS&#xff09; 是面向全场景的分布式操作系统&#xff0c;它通过创新的应用模型&#xff0c;为开发者提供了强大的应用开发框架。 在HarmonyOS的发展过程中&#xff0c;FA模型&#xff08;Feature Ability&#xff09;和Stage模型是两种重要的应用模…

10倍提效!用ChatGPT编写系统功能文档。。。

系统功能文档是一种描述软件系统功能和操作方式的文档。它让开发团队、测试人员、项目管理者、客户和最终用户对系统行为有清晰、全面的了解。 通过ChatGPT&#xff0c;我们能让编写系统功能文档的效率提升10倍以上。 ​《Leetcode算法刷题宝典》一位阿里P8大佬总结的刷题笔记…

单调栈(LeetCode-下一个更大元素)

每日一题 今天刷到了一道用到单调栈来解决的题目&#xff0c;想到自己没有总结过单调栈的知识点&#xff0c;因此想总结一下。 介绍 什么是单调栈&#xff1f; 单调栈的定义其实很简单&#xff0c;所谓单调栈就是指一个单调递增或是单调递减的栈。 那单调栈有什么用呢&#x…

[AI in sec]-039 DNS隐蔽信道的检测-特征构建

DNS隐蔽信道是什么 DCC是指利用DNS数据包中的可定义字段秘密传递信息的通道。其中,“DNS 协议”是目前网络上使用的标准域名解析协议;“可定义字段”是DNS 数据包中的 QNAME 字段、RDATA 字段及RawUDP字段。利用DNS数据包可以构建2种信道:存储信道及时间信道。DCC可以被用于…

长文本大模型火爆国内AI市场,算力需求激增引领行业变革

近期&#xff0c;一款名为Kimi的大模型火爆国内AI市场&#xff0c;以其出色的长文本处理能力和广泛的应用前景吸引了众多关注。随着Kimi等长文本大模型的流行&#xff0c;算力需求持续增长&#xff0c;为AI行业带来了新的变革和机遇。 Kimi突破长文本处理极限&#xff0c;为复杂…

RFID涉密载体柜 RFID智能文件柜系统

涉密载体管控RFID智能柜&#xff08;载体柜DW-G101R&#xff09;通过对涉密物资、设备进行RFID唯一标识并放置于RFID设备涉密物资柜柜体&#xff0c;通过定位每台设备每件涉密物资的位置&#xff0c;实现涉密物资审批、自助借还、防盗等出入库全流程自动化管理。主要管理对象移…

计算机研究生规划

一、计算机研究生技术栈 两条腿走路: 左侧工程实践能力&#xff1a;要掌握python编程语言&#xff0c;它和机器学习、神经网络&#xff08;这两门几乎是必须掌握的技能&#xff09;的学习有很大关系 右侧学术创新能力 二、编程语言能力提升 左边基础&#xff0c;右边教你写…

ICLR24_OUT-OF-DISTRIBUTION DETECTION WITH NEGATIVE PROMPTS

摘要 分布外检测&#xff08;OOD Detection&#xff09;的研究对于开放世界&#xff08;open-world&#xff09;学习非常重要。受大模型&#xff08;CLIP&#xff09;启发&#xff0c;部分工作匹配图像特征和提示来实现文本-图像特征之间的相似性。 现有工作难以处理具有与已…

ping命令返回无法访问目标主机和请求超时浅析

在日常经常用ping命令测试网络是否通信正常&#xff0c;使用ping命令时也经常会遇到这两种情况&#xff0c;那么表示网络出现了问题。 1、请求超时的原因 可以看到“请求超时”没有收到任何回复。要知道&#xff0c;IP数据报是有生存时间的&#xff0c;当其生存时间为零时就会…

Linux虚拟网络设备全景解析:定义、工作模式与实践应用

在深入探索Linux操作系统的强大功能时&#xff0c;我们不可避免地会遇到虚拟网络设备的概念。这些设备扮演着构建和维护虚拟化环境中网络通信的关键角色。本文旨在详细介绍Linux虚拟网络设备的定义、工作模式以及它们的多样化用途。 1. Linux虚拟网络设备的定义 Linux虚拟网络…

Dubbo 服务发现

Dubbo 服务发现 1、什么是服务发现 **服务发现&#xff08;Service discovery&#xff09;**是自动检测一个计算机网络内的设备及其提供的服务。 2、Dubbo 与 服务发现 Dubbo 提供的是一种 Client-Based 的服务发现机制&#xff0c;依赖第三方注册中心组件来协调服务发现过…

思维的类比

Learn More, Study Less 中提出了整体学习法&#xff08;Holistic learning&#xff09;&#xff0c;其基本思想是&#xff1a;你不可能孤立地学会一个概念&#xff0c;而只能将其融入已有的概念体系中&#xff0c;从不同角度对其进行刻画来弄懂其内涵和外延并且书中使用三个类…

前端layui自定义图标的简单使用

iconfont-阿里巴巴矢量图标库 2. 3. 4.追加新图标 5.文件复制追加新图标

解决电脑无故自动关机或重启的15种方法,总有一种适合你

序言 你的Windows PC是否在没有警告的情况下关闭或重新启动?这背后有几个潜在的原因。例如,它可能是软件/硬件冲突、过热或硬盘驱动器错误。本故障排除指南将概述在Windows 10/11中修复自动关闭和重新启动的多个解决方案。 如果你的计算机经常关闭,则必须在安全模式下启动…

【Java】maven传递依赖冲突解决

传递依赖的概念&#xff1a; 传递依赖:&#xff1a; A.jar 依赖 B.jar, B.jar 依赖 C.jar, 这个时候我们就说B是A的直接依赖, C是A传递依赖; 传递依赖可能会产生冲突: 联系着上面, 新导入一个jar包D.jar, D依赖C.jar, 但是B依赖的1.1版本, 而D依赖的是1.2版本, 这时候C这个j…

Oracle 常用SQL命令

Oracle 常用SQL命令 1、备份单张表 创建复制表结构 create table employeesbak as select * from cims.employees 如果只复制表结构&#xff0c;只需要在结尾加上 where 10 插入数据 insert into employeesbak select * from cims.employees 删除一条…

Mysql主键优化之页分裂与页合并

主键设计原则 满足业务需求的情况下&#xff0c;尽量降低主键的长度。因为如果主键太长&#xff0c;在多个二级索引中&#xff0c;主键索引值所占用的空间就会过大。 插入数据时&#xff0c;尽量选择顺序插入&#xff0c;选择使用AUTO_INCREMENT自增主键。因为乱序插入会导致页…

物联网系统未来的发展趋势

一、引言 物联网系统作为新一代的信息技术&#xff0c;正在逐渐改变我们的生活和工作方式。随着物联网技术的不断发展和应用场景的拓展&#xff0c;未来物联网系统的发展趋势将更加明显。本文将从技术、应用、安全等方面探讨物联网系统未来的发展趋势。 二、技术发展趋势 1.…

在NBA我需要翻译--适配器模式

1.1 在NBA我需要翻译&#xff01; "你说姚明去了几年&#xff0c;英语练出来了哦&#xff0c;我看教练在那里布置战术&#xff0c;他旁边也没有翻译的&#xff0c;不住点头&#xff0c;瞧样子听懂没什么问题了。" "要知道&#xff0c;最开始&#xff0c…

SwiftUI Swift 选择图片 添加图片

1. 添加记帐时添加图片功能 2. Show me the code // // TestPhotoPicker.swift // pandabill // // Created by 朱洪苇 on 2024/3/30. //import SwiftUI import PhotosUI import Foundationstruct TestPhotoPicker: View {State private var selectedItem: PhotosPickerIt…