深度剖析哈希

news2024/12/23 13:53:42

目录

一.   哈希概念

1.1   哈希概念

1.2   哈希冲突

1.3   哈希函数

二.   闭散列

2.1   线性探测法

2.2   引进状态

2.3   闭散列的查找、插入、删除操作

2.4   闭散列插入时的扩容

2.4   仿函数

2.5   整体代码

三.   开散列

​编辑

2.1    闭散列节点定义

2.2   开散列的插入查找删除操作

2.3   开散列插入时的扩容

2.4   整体代码

四.   素数做除数和哈希表结构问题


一.   哈希概念

C++11中引进了unordered系列的四个容器,而之所以这几个容器效率如此之高,是因为运用到了哈希的思想。

1.1   哈希概念

        顺序结构以及平衡树中,元素关键码与其存储位置没有对应的关系,所以在插入和查找操作时,需要遍历结构,这样造成的时间复杂度太高。

        而我们的哈希就是通过某种函数,让元素的关键码与元素的存储位置构建起一一映射关系。这就是哈希的概念。

当向该结构中:

  • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功

 哈希中使用的函数叫做哈希函数,通过哈希构建的结构称为哈希表或者散列表。

特别注意:以上不论是顺序搜索,还是平衡树搜索或者哈希搜索,得到的key值都不能相同。​​​​​​​

1.2   哈希冲突

什么是哈希冲突呢?其实啊就是不同的关键码通过相同的哈希函数得到相同的映射位置。

这种就叫哈希冲突或者哈希碰撞。发生哈希冲突并具有相同映射位置的不同的关键码就叫同义词。

1.3   哈希函数

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

我们的哈希函数需要满足:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

我们有下面常见的哈希函数:

 1、直接定址法(常用)

直接定址法字面上来说就是通过关键码直接得到映射位置。最多再加一个倍数变换,也就是取关键字的某个线性函数为散列地址。

Hash(Key)= A*Key + B

直接定址法的优点是简单并且消除哈希冲突,但是需要事先知道数据的分布情况,因为直接定址法适用于数据集中且数据小的情况,如下:

2、除留余数法(常用)

如果我们设散列表允许的地址数为m,取一个不大于m,但最接近或者等于m的质数作为除数。

Hash(key)=key%p(p<=m)

简单来说,就是将关键码除以散列表的大小得到的余数作为映射的位置。除留余数法的优点是可以处理分散的数据,缺点是会导致哈希冲突,例如对于{1,4,5,614}

可以看见是会发生哈希冲突的。 

3、平方取中法(了解) 

        假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。平方取中法适用于不知道关键字分布,但是数据又不是很大的时候。

4、折叠法(了解)

        折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这 几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

5. 随机数法(了解)

        选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中

random为随机数函数。随机数法适用于关键词长度不等的情况。

6. 数学分析法(了解)

        设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只 有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的 若干位分布较均匀的情况。

二.   闭散列

闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被填满,说明哈希表中必然还有空位置,那么可以把key存放到冲突位置的下一个空位置中去,为什么说是空位置呢?下面我们会讲解。

2.1   线性探测法

这里我们采用线性探测的方法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

我们来看看实例,假设我们要将arr映射到Hash表中,而表的大小为10,那么:

int arr[]={4,6,14,25,34}
Hash(i)=arr[i]%10;//表的大小为10

那么就应该如下:

注意:当元素本来该在的位置被占用时,需要往后找下一个空位置。比如:4先进入表,就把4的位置占住了,那么当14再进去的时候,由于下标是4的地方已经被占住了,所以向后找到下标为5的位置,插入。而如果此时再来一个28,那么就插入下标为9的地方,而如果再来了一个29,我们可以看到后面的表已经满了,那么就要从头开始查找插入,结果就应该是:

2.2   引进状态

那么好,我们现在已经知道插入一个元素时,先用他的值对表的大小取模,得到位置,如果该位置为空,那么就插入;如果不为空,就向后找下一个空位置。那么大家有没有考虑过我先删掉一个元素再插入呢?

例如我先删除14,再插入54呢?可以看到有两个问题:

  1. 删除之后,我们该将删除之后位置的值变成多少呢?好像多少都不合适,因为无论你变成多少,都有可能会有新插入的元素值跟他一样!
  2. 我们插入54之后,按理来说我们应该插入到删除的14的位置,因为实际上来说删除了元素之后,那里就没有元素了,可是我们代码又如何去看该位置是不是空呢?因为第一个问题就是删除之后不知道该把值变为多少。

那么其实我们的解决方法就是,为每个位置引进一个state状态(EXIST,DELETE,NULL)。

2.3   闭散列的查找、插入、删除操作

那么我们就可以进行查找、插入、删除操作了:

  1. 查找:现根据哈希函数查找到该元素本来该在的位置,然后再考虑发生过哈希冲突的情况,那我们就要依次向后找不为空的位置,直到找到该元素且状态为存在。
  2. 插入:对于插入来说,我们要先根据哈希函数来获取插入元素在哈希表中的位置。如果该位置没有元素则插入,如果该位置有元素发生了哈希冲突,则向后依次找下一个空位置。如果负载因子(哈希表中的元素个数/哈希表的大小)超过给定的大小,则需要对哈希表进行扩容。
  3. 删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。先查找到位置,然后将该位置的状态变为DELETE。​​​​​​​

代码实现如下:

#pragma once

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

//标识每个存储位置的状态--空、存在与删除
enum State {
	EMPTY,
	EXIST,
	DELETE
};

//定义哈希表中每个位置的结构
template<class K, class V>
struct HashData {
	pair<K, V> _kv;
	State _state = EMPTY;  //默认为空
};

//哈希表
template<class K, class V>
class HashTable {

public:
	HashTable()
		: _n(0)
	{
		//此处将哈希表的大小默认给为10,各位可以自己给定
		_tables.resize(10);
	}

	bool insert(const pair<K, V>& kv) {
		if (find(kv.first))
			return false;

		//除留余数法 && 线性探测法
		//将数据映射到 数据的key值除以哈希表的大小得到的余数 的位置,如果该位置被占用往后放
		size_t hashi = kv.first % _tables.size();
		//不能放在EXIST的位置,DELETE和EMPTY都能放
		while (_tables[hashi]._state == EXIST)
		{
			++hashi;
			hashi%= _tables.size();//防止超过表的大小
		}

		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;

		return true;
	}

	//将find的返回值定义为Data的地址,可以方便我们进行删除以及修改V
	HashData<K, V>* find(const K& key) 
    {
		Hash hash;  //仿函数对象,后面会细讲
		size_t hashi = hash(key) % _tables.size();
		//记录hashi的起始位置
		size_t starti = hashi;
		//最多找到空
		while (_tables[hashi]._state != EMPTY) {
			//key相等并且state为EXIST才表示找到
			if (_tables[hashi]._kv.first == key && _tables[hashi]._state == EXIST)
			{
                return &_tables[hashi];
            }
			++hashi;
			hashi %= _tables.size();
		}

		return nullptr;
	}

	bool erase(const K& key) {
		//找不到就不删,找到就把状态置为DELETE即可
		HashData<K, V>* ret = find(key);
		if (ret) {
			ret->_state = DELETE;
			return true;
		}

		return false;
	}

private:
	vector<Data> _tables;
	size_t _n;  //记录表中有效数据的个数
};

2.4   闭散列插入时的扩容

可以看到,我们表的大小是有限的,不可能插入无数个数,所以不可避免的会发生扩容操作。

​​​​​​​

 我们可以回忆一下数组的扩容,我们是直接在原来的数组上直接多开辟空间。那么同样的方法在这里行不行得通呢?

答案是否定的,因为我们这里要符合哈希函数的映射,如果我们扩容后映射条件是会改变的,比如表的大小从10扩容到20,那么同样的17,原来应该是在下标为7的地方,扩容后就应该在下标17的地方,所以扩容前后的映射条件是不同的。

那我们是不是要等到表满了才扩容呢,其实并不是,如果满了再扩容就有点局促了,没有一点的缓冲的地方,所以我们要规定,插入的元素达到表的多少就扩容。我们定义一个载荷因子。

 我们这里定义载荷因子为0.7。

所以扩容具体操作如下:

//扩容
if ((double)_n / _tables.size() > 0.7)
{
	//此处不能直接原地扩容,因为映射关系需要改变
	Hashtable<K,V,Hash> newHT;
	newHT._tables.resize(_tables.size() * 2);
	for (auto& e : _tables)
	{
		if(e._state==EXIST)
		{
			//此处不会死循环,因为不会进入需不需要扩容这个判断
			newHT.Insert(e._data);
		}
	}
	_tables.swap(newHT._tables);
}

2.4   仿函数

我们上面考虑的情况都是插入的值为整数的情况,那么如果插入的是字符串呢?

按道理来讲我们要先将字符串转换为整数,再进行插入。那我们能不能把求映射位置的表达式换成这样呢?

size_t hashi = kv.first[0] % _tables.size();

很显然是不能的,这样又只能满足字符串,那么编译器又不知道你要插入什么类型的元素,万一你插入整型,而整型又不能取[],又会显得鸡肋。

这种问题还得是需要仿函数这位大哥来解决。我们可以为不同的类型定义不同的处理方式:

//int类型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		return s[0];
	}
};

那么对于字符串这种特殊情况,就有很多人去研究具体该如何定义他的映射函数才是更完美的。那么其中最好的就是BKDR哈希字符串算法,也就是将每一位的字符乘以131或者特定的数字,最后相加。

template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t len = 0;
		for (auto e : s)
		{
			len += e;
			len *= 131;
		}
		return len;
	}
};

2.5   整体代码

#include<iostream>
#include<vector>
using namespace std;
//int类型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t len = 0;
		for (auto e : s)
		{
			len += e;
			len *= 131;
		}
		return len;
	}
};

namespace open_address
{
	enum State
	{
		EMPTY,
		DELETE,
		EXIST
	};
	template<class K, class V>
	struct Hashdata
	{
		pair<K, V> _data;
		State _state = EMPTY;
	};

	template<class K, class V,class Hash=HashFunc<K>>
	class Hashtable
	{
	public:
		Hashtable(size_t size = 10)
		{
			_tables.resize(size);
		}
		Hashdata<K, V>* Find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._data.first == key
					&& _tables[hashi]._state == EXIST)
				{
					return &_tables[hashi];
				}
				++hashi;
				hashi %= _tables.size();
			}
			return nullptr;
		}
		bool Insert(const pair<K,V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			//扩容
			if ((double)_n / _tables.size() > 0.7)
			{
				//此处不能直接原地扩容,因为映射关系需要改变
				Hashtable<K,V,Hash> newHT;
				newHT._tables.resize(_tables.size() * 2);
				for (auto& e : _tables)
				{
					if(e._state==EXIST)
					{
						//此处不会死循环,因为不会进入需不需要扩容这个判断
						newHT.Insert(e._data);
					}
				}
				_tables.swap(newHT._tables);
			}
			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();
			while (_tables[hashi]._state != EMPTY)
			{
				++hashi;
				hashi%= _tables.size();
			}
			_tables[hashi]._data = kv;
			_tables[hashi]._state = EXIST;
			return true;
		}
		bool Erase(const K& key)
		{
			if (!Find(key))
			{
				return false;
			}
			else
			{
				Hashdata<K, V>* ret = Find(key);
				ret->_state = DELETE;
				_n--;
				return true;
			}
		}
	private:
		vector<Hashdata<K,V>> _tables;
		size_t _n = 0;	//实际存储的数据个数
	}; 
}

三.   开散列

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

2.1    闭散列节点定义

闭散列的各个元素之间不会互相影响,所以就不用为每个节点定义状态了。而且每个节点里面都是链表节点。

//int类型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t len = 0;
		for (auto e : s)
		{
			len += e;
			len *= 131;
		}
		return len;
	}
};
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 K, class V, class Hash = HashFunc<K>>
	class HashTable {
		typedef HashNode<K, V> Node;
        	private:
		vector<Node*> _tables;  //指针数组
		size_t _n;  //表中有效数据的个数
	};
}

2.2   开散列的插入查找删除操作

开散列所用的结构已经与闭散列不相同了,所以对应的操作也应该作出变换。

  1. 插入:还是先根据哈希函数算出该插入的位置,用该值创建一个新节点,我们这里采用的是头插的方式插入新节点,因为尾插还要找到尾部,影响了效率。
  2. 查找:根据哈希函数算出这个值所在的位置,从头结点开始遍历,一直遍历到某个节点的值等于要查找的值为止。
  3. 删除:现根据查找函数找出要删除的节点,如果该节点前面没有节点,则直接删除,然后让哈希表该位置的值为空,如果前面还有值,就先记录前面与后面的节点,删除节点之后,链接这两个节点。
//插入
bool insert(const pair<K, V>& kv) {
    if (find(kv.first))
        return false;

    //调用仿函数的匿名对象来将key转换为整数
    size_t hashi = Hash()(kv.first) % _tables.size();
    //哈希桶头插
    Node* newNode = new Node(kv);
    newNode->next = _tables[hashi];
    _tables[hashi] = newNode;
    ++_n;

    return true;
}

//查找
Node* find(const K& key) {
	size_t hashi = Hash()(key) % _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) {
    //由于单链表中删除节点需要改变上一个节点的指向,所以这里不能find后直接erase
    size_t hashi = Hash()(key) % _tables.size();
    Node* prev = nullptr;
    Node* cur = _tables[hashi];
    while (cur) {
        //删除还要分是否为头结点
        if (cur->_kv.first == key) {
            if (cur == _tables[hashi])
                _tables[hashi] = cur->next;
            else
                prev->next = cur->next;

            delete cur;
            --_n;
            return true;
        }

        //迭代
        prev = cur;
        cur = cur->next;
    }

    return false;
}

2.3   开散列插入时的扩容

与闭散列同样的道理,开散列的插入操作也涉及到扩容操作。因为当我们插入元素时,每个桶中的节点个数会增加,那么极端情况下,可能某个桶下面会挂炒鸡多的节点,这样会影响查找和删除操作的效率。我们就需要进行扩容操作。

我们该什么时候扩容呢?哈希表最好的情况是每个下面都挂一个节点,所以当插入元素个数等于表的大小的时候就进行扩容,此时载荷因子大小为1:

代码如下:

Hash hs;
if (_n == _tables.size())
{
	//不用跟开放地址法一样重新创一个HashTable对象,因为会造成指针节点创建又删除
	vector<Node*> newvector(_tables.size()*2,nullptr);
	for (size_t i = 0; i < _tables.size(); i++)
	{
		Node* cur1 = _tables[i];
		while (cur1)
		{
			Node* cur2 = cur1->_next;
			size_t hashi = hs(cur1->_data) % newvector.size();
			cur1->_next = newvector[i];
			newvector[i] = cur1;
			cur1 = cur2;
		}

		//把原来的指针置空
		_tables[i] = nullptr;
	}
	_tables.swap(newvector);
}

此处我们无需再去跟开放地址法一样创建一个HashTable对象,然后再根据所有的值各自创建节点插入到扩容后的新表中,因为这样导致新节点的创建,旧节点的销毁。这样是得不偿失的。

我们采用的是直接取出旧表中的节点插入到新表中(注意:不能将整个桶直接插入到新表,因为同样的值对于新表来说映射结果可能改变了),然后交换旧表与新表即可,这样节约了大量的时间。

2.4   整体代码

//哈希表的仿函数
template<class K>
struct HashFunc {
	size_t operator()(const K& key) {
		return key;
	}
};

//类模板特化
template<>
struct HashFunc<string> {
	size_t operator()(const string& key) {
		//BKDR字符串哈希算法
		size_t sum = 0;
		for (auto ch : key)
			sum = sum * 131 + ch;

		return sum;
	}
};

//开散列
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 K, class V, class Hash = HashFunc<K>>
	class HashTable {
		typedef HashNode<K, V> Node;

	public:
		//构造
		HashTable()
			: _n(0)
		{
			_tables.resize(10, nullptr);
		}

		//析构--手动释放哈希表中的每个元素,以及每个元素指向的哈希桶
		~HashTable() {
			//释放每个元素的哈希桶
			for (size_t i = 0; i < _tables.size(); ++i) {
				Node* cur = _tables[i];
				while (cur) {
					Node* next = cur->next;
					delete cur;
					cur = next;
				}
			}
		}

		//插入
		bool insert(const pair<K, V>& kv) {
			if (find(kv.first))
				return false;
            
            Hash hs;
			//扩容--当载荷因子达到1时我们进行扩容
			if (_n == _tables.size()) {
				vector<Node*> newTables;
				newTables.resize(_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); ++i) {
					Node* cur = _tables[i];
					while (cur) {
						Node* next = cur->next;
						//重新计算映射关系--调用仿函数的匿名对象来将key转换为整数
						size_t hashi = hs(cur->_kv.first) % newTables.size();
						cur->next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}
				}

				_tables.swap(newTables);
				newTables.clear();  //不写也可以,局部的vector变量出作用域自动调用析构
			}

			//调用仿函数的匿名对象来将key转换为整数
			size_t hashi = Hash()(kv.first) % _tables.size();
			//哈希桶头插
			Node* newNode = new Node(kv);
			newNode->next = _tables[hashi];
			_tables[hashi] = newNode;
			++_n;

			return true;
		}

		//查找
		Node* find(const K& key) {
            Hash hs;
			size_t hashi = hs(key) % _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;
			//由于单链表中删除节点需要改变上一个节点的指向,所以这里不能find后直接erase
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur) {
				//删除还要分是否为头结点
				if (cur->_kv.first == key) {
					if (cur == _tables[hashi])
						_tables[hashi] = cur->next;
					else
						prev->next = cur->next;

					delete cur;
					--_n;
					return true;
				}

				//迭代
				prev = cur;
				cur = cur->next;
			}

			return false;
		}

	private:
		vector<Node*> _tables;  //指针数组
		size_t _n;  //表中有效数据的个数
	};
}

四.   素数做除数和哈希表结构问题

我们上面在介绍除留余数法时给出的定义是这样的:设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的素数p作为除数,按照 哈希函数 Hash(key) = key % p (p<=m), 将关键码转换成哈希地址;

这里为什么要选择素数来作为除数呢?因为这样能减少哈希冲突发生的概率。

所以我们C++中就有人专门去研究了哪些素数最合适作为除数。那么STL源码如下:

// Note: assumes long is at least 32 bits.
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,         97,         193,       389,       769,
  1543,       3079,       6151,      12289,     24593,
  49157,      98317,      196613,    393241,    786433,
  1572869,    3145739,    6291469,   12582917,  25165843,
  50331653,   100663319,  201326611, 402653189, 805306457, 
  1610612741, 3221225473, 4294967291
};

inline unsigned long __stl_next_prime(unsigned long n)
{
  const unsigned long* first = __stl_prime_list;
  const unsigned long* last = __stl_prime_list + __stl_num_primes;
  const unsigned long* pos = lower_bound(first, last, n);
  return pos == last ? *(last - 1) : *pos;
}

这里还有一个问题,我们设定的是当载荷因子达到1的时候就进行扩容,这样保证哈希表中的每个位置下面挂着三五个节点,保证了效率。

但是极端情况下,例如经历某些特殊攻击的时候(专门插入位于同一种哈希碰撞情况的素数),那么机会导致某个链表很长,这里我们就要对链表这个数据机构做出变换,可以把它换成红黑树,红黑树可以保证查找、插入和删除操作的时间复杂度都是 O(log n),比链表快得多。

所以在极端情况下,可以用红黑树来作为存储结构,而普通情况下就采用链表来存储就可以了。


总结

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)

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

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

相关文章

HBuilder真机调试检测不到荣耀Magic UI系列(包括手机和电脑)解决办法

HBuilder真机调试检测不到荣耀Magic UI系列&#xff08;包括手机和电脑&#xff09;解决办法解决方法&#xff1a; 1.在开发人员选项中开启USB调试 如何进入开发者选项&#xff1f; 设置->关于->版本号&#xff0c;点击版本号直至出现您已处于开发者模式 2.选择USB配置…

Swin Transformer 浅析

Swin Transformer 浅析 文章目录 Swin Transformer 浅析引言Swin Transformer 的网络结构W-MSA 窗口多头注意力机制SW-MSA 滑动窗口多头注意力机制Patch Merging 图块合并 引言 因为ViT无法实现CNN中的层次化构建以及局部信息&#xff0c;由此微软团队提出了Swin Transformer来…

【论文阅读】YOLO-World | 开集目标检测

Date&#xff1a;2024.02.22&#xff0c;Tencent AI Lab&#xff0c;华中科技大学Paper&#xff1a;https://arxiv.org/pdf/2401.17270.pdfGithub&#xff1a;https://github.com/AILab-CVC/YOLO-World 论文解决的问题&#xff1a; 通过视觉语言建模和大规模数据集上的预训练来…

【QT+OpenCV】车牌号检测 学习记录 遇到的问题

【QTOpenCV】车牌号检测 学习记录 首先在QT里面配置好OpenCV .pro文件中加入&#xff1a; INCLUDEPATH G:/opencv/build/include LIBS -L"G:/opencv/build/x64/vc14/lib"\-lopencv_core \-lopencv_imgproc \-lopencv_highgui \-lopencv_ml \-lopencv_video \-lo.c…

展览展会媒体媒体邀约执行应该怎么做?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 展览展会邀请媒体跟其他活动邀请媒体流程大致相同&#xff0c;包括 制定媒体邀约计划&#xff0c;准备新闻稿&#xff0c;发送邀请函&#xff0c;确认媒体参会&#xff0c;现场媒体接待及…

页缓存(PageCache)和预读机制(readahead )

页缓存&#xff08;PageCache)和预读机制&#xff08;readahead &#xff09; 页缓存&#xff08;PageCache)是操作系统&#xff08;OS&#xff09;对文件的缓存&#xff0c;用于加速对文件的读写。 page 是内存管理分配的基本单位&#xff0c; Page Cache 由多个 page 构成&…

【观察】容器化部署“再简化”,云原生体验“再升级”

自2013年云原生概念被提出以来&#xff0c;云原生技术和架构在过去十多年得到了迅速的发展&#xff0c;并对数字基础设施、应用架构和应用构建模式带来了深刻的变革。根据IDC预测&#xff0c;到2024年&#xff0c;新增的生产级云原生应用在新应用的占比将从2020年的10%增加到60…

YoloV9改进策略:下采样改进|自研下采样模块(独家改进)|疯狂涨点|附结构图

摘要 本文介绍我自研的下采样模块。本次改进的下采样模块是一种通用的改进方法&#xff0c;你可以用分类任务的主干网络中&#xff0c;也可以用在分割和超分的任务中。已经有粉丝用来改进ConvNext模型&#xff0c;取得了非常好的效果&#xff0c;配合一些其他的改进&#xff0…

MTK MFNR 学习笔记

和你一起终身学习&#xff0c;这里是程序员Android 经典好文推荐&#xff0c;通过阅读本文&#xff0c;您将收获以下知识点: 一、MFNR 简介二、MFNR 开关与决策三、MFNR 相关的adb 命令四、MFNR log 分析五 参考文献 一、MFNR 简介 MFNR : Multiple Frame Noise ReductionMFLL …

lv_micropython for ESP32/S2/S3/C3

由于官方的lv_micropython编译ESP32S3/S2/C3会报错&#xff0c;因为这些芯片的esp-idf底层重写了接口&#xff0c;参照网友提供的方法修改lv_bindings/driver/esp32里的文件&#xff0c;解决编译错误。 问题列举&#xff1a;Issues lvgl/lv_binding_micropython GitHub 一…

Maven的dependencyManagement与dependencies区别

先说结论&#xff1a;Maven 使用dependencyManagement 元素来提供了一种管理依赖版本号的方式。 在maven多模块项目的pom文件中&#xff0c;有的小伙伴会发现最外层的pom文件和里面的pom文件有个地方不一样 如下图 父pom 子pom 一般来说是在maven的最外父工程pom文件里&…

4.18学习总结

多线程补充 等待唤醒机制 现在有两条线程在运行&#xff0c;其中一条线程可以创造一个特殊的数据供另一条线程使用&#xff0c;但这个数据的创建也有要求&#xff1a;在同一时间只允许有一个这样的特殊数据&#xff0c;那么我们要怎样去完成呢&#xff1f;如果用普通的多线程…

云原生Kubernetes: K8S 1.29版本 部署Kuboard

目录 一、实验 1.环境 2.K8S 1.29版本 部署Kuboard (第一种方式) 3.K8S 1.29版本 部署Kuboard (第二种方式) 4.K8S 1.29版本 使用Kuboard 二、问题 1.docker如何在node节点间移动镜像 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 主机架构版本IP备注ma…

Java学习-Module的概念和使用、IDEA的常用设置及常用快捷键

Module的概念和使用 【1】在Eclipse中我们有Workspace (工作空间)和Project (工程)的概念&#xff0c;在IDEA中只有Project (工程)和Module (模块)的概念。 这里的对应关系为: IDEA官网说明: An Eclipse workspace is similar to a project in IntelliJ IDEA An Eclipse pr…

二刷大数据(三)- Flink1.17

目录 Flink概念与SparkStreaming区别分层API 工作流程部署模式**Local Mode****Standalone Mode****YARN Mode****Kubernetes Mode****Application Mode** 运行架构stand alone 核心概念算子链任务槽 窗口窗口**窗口的目的与作用****时间窗口&#xff08;Time Windows&#xff…

ARouter之kotlin build.gradle.kts

ARouter之kotlin build.gradle.kts kotlin的配置需要用到kapt 项目的build.gradle.kts plugins {id("com.android.application") version "8.1.2" apply falseid("org.jetbrains.kotlin.android") version "1.9.0" apply falseid(&…

typecho博客的相对地址实现

typecho其中的博客地址,必须写上绝对地址,否则在迁移网址的时候会出现问题,例如页面记载异常 修改其中的 typecho\var\Widget\Options\General.php 中的165行左右, /** 站点地址 */if (!defined(__TYPECHO_SITE_URL__)) {$siteUrl new Form\Element\Text(siteUrl,null,$this-…

OpenCV从入门到精通实战(五)——dnn加载深度学习模型

从指定路径读取图像文件、利用OpenCV进行图像处理&#xff0c;以及使用Caffe框架进行深度学习预测的过程。 下面是程序的主要步骤和对应的实现代码总结&#xff1a; 1. 导入必要的工具包和模型 程序开始先导入需要的库os、numpy、cv2&#xff0c;同时导入utils_paths模块&…

【opencv】dnn示例-person_reid.cpp 人员识别(ReID,Re-Identification)系统

ReID(Re-Identification&#xff0c;即对摄像机视野外的人进行再识别) 0030_c1_f0056923.jpg 0042_c5_f0068994.jpg 0056_c8_f0017063.jpg 以上为输出结果&#xff1a;result文件夹下 galleryLIst.txt queryList.txt 模型下载&#xff1a; https://github.com/ReID-Team/ReID_e…

华为手机p70即将上市,国内手机市场或迎来新局面?

4月15日&#xff0c;华为官宣手机品牌全新升级&#xff0c;p系列品牌升级为Pura。华为P70系列手机预计将于2024年第一季度末发布&#xff0c;而网友也纷纷表示期待p70在拍照、性能上的全新突破。 网友们对华为P70系列的热情高涨&#xff0c;也印证了国内高端手机市场的潜力巨大…