数据结构 - 哈希表
文章目录
- 前言
- 一、哈希思想
- 二、哈希表概念
- 三、哈希函数
- 1、哈希函数设计原则
- 2、常用的哈希函数
- 四、哈希冲突
- 1、什么是哈希冲突
- 2、解决哈希冲突
- 闭散列
- 开散列
- 五、哈希表的性能分析
- 时间复杂度分析
- 空间复杂度分析
前言
一、哈希思想
哈希思想(Hashing)是计算机科学中一种非常重要的思想,它主要用于实现数据的快速查找、去重、以及作为数据结构如哈希表(HashTable)或哈希集合(Hash Set)的基础。哈希思想的核心在于通过一个哈希函数(Hash Function)将任意长度的输入(通常称为键或关键字)映射到一个固定长度的输出,这个输出通常是一个整数,我们称之为哈希值(Hash Value)或哈希码(Hash Code)。
二、哈希表概念
哈希表(Hash Table)是一种使用哈希函数组织数据,以支持快速插入、删除和查找操作的数据结构。它是通过把键(Key)映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数称为哈希函数,存放记录的数组称为哈希表。
三、哈希函数
1、哈希函数设计原则
(1)哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间。
(2)哈希函数计算出来的地址能均匀分布在整个空间中。
(3)哈希函数应该比较简单。
2、常用的哈希函数
(1) 直接定址法–(常用)
A. 直接定址法通过某种固定的计算方式,直接将关键字映射到哈希表的某个位置上。这种映射通常是线性的,即哈希地址与关键字之间存在一种直接的、确定的数学关系。
B. 计算公式 直接定址法的计算公式一般形式为: H(key)=a×key+b
其中,H(key) 表示关键字 key 的哈希地址,a 和 b 是根据哈希表长度和关键字分布情况确定的常数。这个公式可以根据具体情况进行调整,但核心思想是通过简单的数学运算将关键字映射到哈希表的一个位置。C.特点
简单直接:直接定址法的计算过程简单直接,没有复杂的迭代或递归过程。
确定性:对于同一个关键字,无论计算多少次,其哈希地址都是相同的,这保证了哈希表的确定性。
适用范围有限:直接定址法通常适用于关键字分布均匀且范围较小的情况。如果关键字范围过大或分布不均匀,可能会导致哈希表的某些位置空闲而另一些位置拥挤。
D. 应用场景 直接定址法适用于一些特定的应用场景,如:
关键字集合已知且固定不变的情况。 关键字范围较小且分布均匀的情况。 对哈希表的性能要求不是特别高的情况。
E.注意事项:在使用直接定址法时,需要注意以下几点:
根据关键字集合的实际情况选择合适的 a 和 b 值,以确保哈希地址分布均匀。
如果关键字范围过大或分布不均匀,可能需要考虑使用其他哈希函数构造方法,如除留余数法、平方取中法等。
在实际应用中,还需要考虑哈希表的扩容和冲突解决机制,以确保哈希表的性能。
综上所述,直接定址法是哈希表中一种简单直观的哈希函数构造方法,适用于特定的应用场景。在使用时需要根据实际情况进行选择和调整。

(2)除留余数法
A.除留余数法取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。 即:H(key)=key%p
其中,p是一个整数,且通常满足p≤m(m为散列表的长度)。为了确保哈希表的性能,p的选择很关键,一般推荐p为质数,或者至少是一个不包含小于20质因子的合数。B.特点与优势
简单性:除留余数法的计算过程相对简单,容易实现。
灵活性:通过调整p的值,可以在一定程度上控制哈希表的性能。
适用性:适用于大多数关键字分布的情况,尤其是当关键字范围较大时。
C.缺点
可能造成key冲突。

四、哈希冲突
1、什么是哈希冲突
对于两个数据元素的关键字 k i k_i ki和 k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希函数计算出相同的哈希地址(哈希函数设计不够合理),该种现象称为哈希冲突或哈希碰撞,如上述除留余数法中的5和13。
2、解决哈希冲突
闭散列
闭散列的基本原理是,当发生哈希冲突时,如果哈希表未被装满,那么在哈希表中必然还有空位置。因此,可以把产生冲突的元素存放到冲突位置的“下一个”空位置中去。这个“下一个”空位置可以通过某种探测方法找到,如线性探测、二次探测等。
(1)探测方法
线性探测:
从发生冲突的位置开始,依次向后探测,直到找到空位置为止。 例如,如果元素A的哈希地址为i,但位置i已被占用,则依次检查i+1, i+2, …,直到找到空位置。

二次探测:
发生哈希冲突时,通过二次探测法寻找“下一个”空位置。 公式为:Hi = (H0 + i2) % m 或 Hi = (H0 - i2) %m,其中H0是通过哈希函数计算得到的初始位置,m是哈希表的大小,i是探测次数(i=1,2,3,…,(m-1)/2)。

(2)使用闭散列实现哈希表
基本框架
//将K转化为整形
template<class K>
class InvertSize_t
{
public:size_t operator()(const K & key){return (size_t)key;}
};
//string类特化
template<>
class InvertSize_t<string>
{
public:size_t operator()(const string& key){size_t sum = 0;for (auto& e : key){sum = sum * 31 + e;}return sum;}
};//状态表示
enum State { EMPTY, EXIST, DELETE };//闭散列
template<class K, class V,class InvertKey = InvertSize_t<K>>
class HashTable
{//节点struct Elem{pair<K, V> _val; //数据State _state; //状态表示Elem(pair<K, V> val = pair<K, V>()):_val(val),_state(EMPTY){}};public://默认容量为10HashTable(size_t capacity = 10): _ht(capacity), _size(0){}private:vector<Elem> _ht;size_t _size;
};
模板参数:
K:键值类型
V:数据类型
InvertKey:将键值类型转化为整形的哈希函数,关于该函数的设计可参考:字符串哈希算法
节点成员变量:
标志当前位置的状态
_state(空、存在、删除)
键值与数据构成的对组_val
闭散列哈希表类成员变量:
当前存在元素个数
_size
储存哈希表的线性表_ht(默认长度为10)
插入
负载因子:
哈希表中的负载因子(Load Factor)是一个重要的概念,它用于衡量哈希表的填充程度,即哈希表中已存储元素的占用程度,当超过一定程度就进行扩容。

步骤:
(1)判断键值是否存在,若存在插入失败,不存在找到适合位置插入。
(2)判断是否需要扩容
(3)将键值转化为整形再通过哈希函数找到哈希值,再通过线性探测(也可以使用二次探测)找到合适位置(为空或者被删除的位置)进行元素储存。
(4)元素个数+1,状态修改为存在。
查找
步骤:
(1)将键值转化为整形再通过哈希函数找到哈希值,再通过线性探测(也可以使用二次探测)查找。
(2)当前哈希值位置如果为空,说明不存在该值,如果为删除就继续使用线性探测,如果为存在且键值不相同则继续,如果为存在且键值相同则成功找到。
删除
步骤:
(1)判断键值是否存在,若不存在删除失败,存在找到并删除。
(2)将键值转化为整形再通过哈希函数找到哈希值,再通过线性探测(也可以使用二次探测)找到该元素。
(3)元素个数-1,状态修改为删除。
当前元素个数
直接返回
_size即可。
判断是否为空
通过判断
_size是否等于0
代码实现
//状态表示
enum State { EMPTY, EXIST, DELETE };//闭散列
template<class K, class V,class InvertKey = InvertSize_t<K>>
class HashTable
{//节点struct Elem{pair<K, V> _val; //数据State _state; //状态表示Elem(pair<K, V> val = pair<K, V>()):_val(val),_state(EMPTY){}};public:HashTable(size_t capacity = 10): _ht(capacity), _size(0){}// 插入bool Insert(const pair<K, V>& val){//是否存在,存在就不会插入if (Find(val.first)) return false;//转化为整数InvertKey ik;//除留余数法查找int hashi = ik(val.first) % _ht.size();//超过负载因子if (_size * 10 / _ht.size() >= 7){HashTable tmp(_ht.size() * 2);for (int i = 0; i < _ht.size(); i++){if (_ht[i]._state == EXIST){tmp.Insert(_ht[i]._val);}}_ht.swap(tmp._ht);}//当删除或者为空储存while (_ht[hashi]._state == EXIST){hashi = ++hashi % _ht.size();}_ht[hashi] = Elem(val);_ht[hashi]._state = EXIST;_size++;return true;}// 查找Elem* Find(const K& key){InvertKey ik; //转化整形仿函数int hashi = ik(key) % _ht.size(); //除留余数法求哈希地址//查到为空即停止while (_ht[hashi]._state != EMPTY){//符合条件if (_ht[hashi]._state == EXIST && _ht[hashi]._val.first == key){return &_ht[hashi];}hashi = ++hashi % _ht.size();}return nullptr;}// 删除bool Erase(const K& key){//查找该值Elem* cur = Find(key);//不存在情况if (cur == nullptr) return false;cur->_state = DELETE;//改变状态_size--; //元素个数--return true;}//存在个数size_t Size()const{return _size;}//是否为空bool Empty() const{return _size == 0;}private:vector<Elem> _ht;size_t _size;
};
开散列
(1)开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

(2)使用开散列实现哈希表
基础框架
//将K转化为整形
template<class K>
class InvertSize_t
{
public:size_t operator()(const K & key){return (size_t)key;}
};//string类特化
template<>
class InvertSize_t<string>
{
public:size_t operator()(const string& key){size_t sum = 0;for (auto& e : key){sum = sum * 31 + e;}return sum;}
};//哈希表节点
template<class T>
struct HashBucketNode
{HashBucketNode(const T& val): _pNext(nullptr), _val(val){}HashBucketNode<T>* _pNext; //前驱指针T _val; //数据
};template<class K,class T, class KeyOfValue, class HF >
class HashBucket
{typedef HashBucketNode<T> Node; //节点重命名typedef HashBucket<K,T, KeyOfValue, HF> Self; //自身重命名
public://构造函数 哈希桶初始化大小为10HashBucket(size_t capacity = 10): _table(10), _size(0)
{}
private:vector<Node*> _table; //储存哈希桶头节点指针size_t _size; // 哈希表中有效元素的个数
};
哈希表节点模板参数
T:数据类型
哈希表节点成员变量
_pNext:前驱指针
_val:数据
哈希表类模板参数
K:键值类型
T:数据类型
KeyOfValue:将数据转化为键值的仿函数类型
HF:将键值类型转化为整形的哈希函数
哈希表成员变量
_table:储存链表头节点指针的线性表
_size:元素个数
迭代器
迭代器类:
根据开散列哈希表的特性设计该迭代器为正向迭代器。
基础框架:
template<class K,class T,class KeyOfValue,class HF,class Ptr,class Ref>
struct HsIterator
{
//重命名
typedef HashBucketNode<T> Node;
typedef HashBucket<K, T, KeyOfValue, HF> Hash;
typedef HsIterator<K, T, KeyOfValue, HF,Ptr,Ref> Self;
//成员变量
Node* _cur; //当前指针
const Hash* _hash; //哈希表指针
//构造函数
HsIterator(const Hash* hash, Node* cur ):_cur(cur),_hash(hash){}
}
迭代器类模板参数
K:键值类型
T:数据类型
KeyOfValue:将数据转化为键值的仿函数类型
HF:将键值类型转化为整形的哈希函数
Ptr:T*/const T*
Ref:T&/const T&
迭代器类成员变量
_cur:哈希节点指针
_hash:哈希表指针
重载前置++
步骤:
(1)判断迭代器中
_cur的前驱指针是否为空。
(2)_cur的前驱指针不为空,说明该哈希桶没有遍历完,_cur =_cur->_pNext即可。
(3)_cur的前驱指针为空,说明该哈希桶已经遍历完了,此时需要通过_cur->_val找到当前哈希桶的哈希值,哈希值++,再通过哈希表找到下一个哈希桶,如果不为空,_cur等于哈希桶的头节点,结束,否则继续++,直到找到不为空的或者将所有桶都遍历一遍。

重载 == 和 !=
通过指针是否相等来判断。
重载解引用
通过返回
_cur->_val的引用。
重载->
通过返回
_cur->_val的地址。
迭代器类代码实现:
//迭代器
template<class K,class T,class KeyOfValue,class HF,class Ptr,class Ref>
struct HsIterator
{typedef HashBucketNode<T> Node;typedef HashBucket<K, T, KeyOfValue, HF> Hash;typedef HsIterator<K, T, KeyOfValue, HF,Ptr,Ref> Self;Node* _cur;const Hash* _hash;HsIterator(const Hash* hash, Node* cur ):_cur(cur),_hash(hash){}Self& operator++(){ KeyOfValue func;HF hf;if (_cur->_pNext != nullptr){_cur = _cur->_pNext;}else{int hashi = hf(func(_cur->_val)) % _hash->_table.size();Node* cur = _hash->_table[++hashi];while (hashi < _hash->_table.size() && cur == nullptr){cur = _hash->_table[hashi];hashi++; }_cur = cur;}return *this;}bool operator==(const Self& it){return _cur == it._cur;}bool operator!= (const Self& it){return _cur != it._cur;}Ref operator*(){return _cur->_val;}Ptr operator->(){return &(_cur->_val);}
};
哈希表迭代器
begin()
找到第一个不为空的哈希桶,用该哈希桶头节点去构造迭代器。
end()
直接利用
nullptr构造迭代器。
插入
步骤:
(1)判断哈希表中是否存在该键值 。
(2)如果存在,插入失败,结束。
加粗样式 (3)如果不存在,判断是否超过负载因子,超过就需要扩容。
(4)通过KeyOfValue 和 HF 函数来求哈希值,通过哈希值找到哈希桶,通过头插法将数据插入。
删除
步骤:
(1)判断哈希表中是否存在该键值 。
(2)如果不存在,删除失败,结束。
(3)通过KeyOfValue 和 HF 函数来求哈希值,通过哈希值找到哈希桶,将节点重新连接,再进行删除。
查找
通过
KeyOfValue和HF函数来求哈希值,通过哈希值找到哈希桶,在该桶遍历该桶。
大小
直接返回
_size。
判断是否为空
通过
_size是否等于0来判断。
清空
遍历哈希表,如果哈希桶不为空,再将哈希桶中的每个节点删除,最后在置空。
析构函数
复用清空接口即可。
拷贝构造
遍历需要拷贝的哈希表,如果哈希桶不为空,再将哈希桶中的每个节点插入新哈希表即可。
赋值重载
先清理原来的哈希表,再利用传过来的临时哈希表然后复用交换接口,进行交换即可。
开散列哈希表代码实现:
//将数据转化为整数
template<class K>
class HashFunc
{
public:size_t operator()(const K& val){return val;}
};template<>
class HashFunc<string>
{
public:size_t operator()(const string& s){const char* str = s.c_str();unsigned int seed = 131; // 31 131 1313 13131 131313unsigned int hash = 0;while (*str){hash = hash * seed + (*str++);}return hash;}
};//将数据转化为键值
template<class K>
struct KeyOfValue
{const K& operator()(const K& data){return data;}
};//前置声明 -->给迭代器向上查找
template<class K, class T, class KeyOfValue, class HF >
class HashBucket;//哈希节点
template<class T>
struct HashBucketNode
{HashBucketNode(const T& val): _pNext(nullptr), _val(val){}HashBucketNode<T>* _pNext; T _val; //数据
};//迭代器
template<class K,class T,class KeyOfValue,class HF,class Ptr,class Ref>
struct HsIterator
{typedef HashBucketNode<T> Node;typedef HashBucket<K, T, KeyOfValue, HF> Hash;typedef HsIterator<K, T, KeyOfValue, HF,Ptr,Ref> Self;Node* _cur;const Hash* _hash;HsIterator(const Hash* hash, Node* cur ):_cur(cur),_hash(hash){}Self& operator++(){ KeyOfValue func;HF hf;if (_cur->_pNext != nullptr){_cur = _cur->_pNext;}else{int hashi = hf(func(_cur->_val)) % _hash->_table.size();Node* cur = _hash->_table[++hashi];while (hashi < _hash->_table.size() && cur == nullptr){cur = _hash->_table[hashi];hashi++; }_cur = cur;}return *this;}bool operator==(const Self& it){return _cur == it._cur;}bool operator!= (const Self& it){return _cur != it._cur;}Ref operator*(){return _cur->_val;}Ptr operator->(){return &(_cur->_val);}
}; template<class K,class T, class KeyOfValue, class HF >
class HashBucket
{typedef HashBucketNode<T> Node; //节点重命名typedef HashBucket<K,T, KeyOfValue, HF> Self; //自身重命名
public:typedef HsIterator<K, T, KeyOfValue, HF, T*, T&> Iterator; //迭代器重命名typedef HsIterator<K, T, KeyOfValue, HF,const T*,const T&> ConstIterator; //const迭代器重命名//迭代器作为有友元template<class K, class T, class KeyOfValue, class HF, class Ptr, class Ref>friend struct HsIterator;//构造函数 哈希桶初始化大小为10HashBucket(size_t capacity = 10): _table(10), _size(0){}//拷贝构造HashBucket(const Self& self){_table.resize(10);//通过遍历Self和插入接口即可for (int i = 0; i < self._table.size(); i++){Node* cur = self._table[i];while (cur){Insert(cur->_val);cur = cur->_pNext;}}}//赋值重载Self& operator=(Self self){Clear();Swap(self);return *this;}~HashBucket(){Clear();}Iterator Begin(){//找到第一个不为空的桶int i = 0;while (_table[i] == nullptr){i++;}return Iterator(this, _table[i]);}Iterator End(){//用nullptr代表最后一个元素的后一个位置return Iterator(this, nullptr);}ConstIterator Begin() const{int i = 0;while (_table[i] == nullptr){i++;}return ConstIterator(this, _table[i]);}ConstIterator End() const{return ConstIterator(this, nullptr);}//返回值:为了unordered_map的重载[]能够使用pair<bool, Iterator>Insert(const T & val){//将数据转化为键值KeyOfValue func;//不允许重复键值Iterator it = Find(func(val));if ( it != End()){return make_pair(false,it);}//将键值转化为整形(哈希地址)HF ik;int hashi = ik(func(val)) % _table.size();//超过负载因子if (_size == _table.size()){//库容2倍vector<Node*> tmp(_table.size() * 2);//将原来的数据转移到tmpfor (int i = 0; i < _table.size(); i++){if (_table[i] != nullptr){Node* cur = _table[i];while (cur){//重新获取哈希地址int k = ik(func(cur->_val)) % tmp.size();//头插入tmpNode* next = cur->_pNext;cur->_pNext = tmp[k];tmp[k] = cur;cur = next;}}}//将容器进行交换_table.swap(tmp);}//插入Node* cur = new Node(val);cur->_pNext = _table[hashi];_table[hashi] = cur;_size++;return make_pair(true, Iterator(this,cur));}// 删除哈希桶中为data的元素(data不会重复)bool Erase(const K& key){//将数据转化为键值KeyOfValue func;//将数据转化为键值HF ik;int hashi = ik(func(key)) % _table.size();//获取头节点Node* cur = _table[hashi];//前驱指针Node* prev = nullptr;while (cur){//找到,重新连接if (func(cur->_val) == key){if (prev == nullptr){_table[hashi] = cur->_pNext;}else{prev->_pNext = cur->_pNext;}_size--;delete cur;return true;}prev = cur;cur = cur->_pNext;}return false;}Iterator Find(const K& key){//将数据转化为键值KeyOfValue func;//将数据转化为键值HF ik;int hashi = ik(key) % _table.size();//获取头节点Node* cur = _table[hashi];//查找while (cur){if (func(cur->_val) == key){return Iterator(this,cur);}cur = cur->_pNext;}return Iterator(this, nullptr);}//大小size_t Size()const{return _size;}//是否为空bool Empty()const{return 0 == _size;}//清空void Clear(){//遍历容器清空每个节点for (int i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_pNext;delete cur;cur = next;}//最后置空_table[i] = nullptr;}//大小为0_size = 0;}//交换void Swap(Self& ht){//交换内部容器即可_table.swap(ht._table);swap(_size, ht._size);}private:vector<Node*> _table; //储存哈希桶头节点指针size_t _size = 0; // 哈希表中有效元素的个数
};
五、哈希表的性能分析
时间复杂度分析
1、哈希表的时间复杂度分析主要关注其三个基本操作:查找(Search)、插入(Insert)和删除(Delete)。这些操作的时间复杂度通常依赖于几个关键因素:哈希函数的质量、哈希表的负载因子(即表中元素数量与表容量的比值)、以及冲突解决策略。
2、链地址法(Separate Chaining):
在链地址法中,每个槽位维护一个链表,所有映射到该槽位的关键字都存储在这个链表中。查找、插入和删除操作的时间复杂度取决于链表的长度。在最坏情况下(即所有关键字都映射到同一个槽位),这些操作的时间复杂度会退化到O(n),其中n是哈希表中元素的数量。但在平均情况下,如果哈希函数设计得当且负载因子适中,这些操作的时间复杂度可以接近O(1)。
3、开放地址法(Open Addressing):
在开放地址法中,当发生哈希冲突时,算法会尝试在哈希表中的另一个槽位找到空闲位置来存储新的关键字。查找、插入和删除操作可能需要遍历多个槽位,直到找到所需的关键字或空闲位置。最常用的开放地址法是线性探测(Linear
Probing)、二次探测(Quadratic Probing)。这些方法的时间复杂度同样取决于负载因子和哈希函数的质量。在平均情况下,如果负载因子适中且哈希函数分布均匀,这些操作的时间复杂度也可以接近O(1)。但在最坏情况下(例如,所有关键字都映射到连续的槽位,并且这些槽位都被占满),这些操作的时间复杂度可能会退化到O(n)。
4、总结
哈希表的时间复杂度通常取决于哈希函数的质量、装载因子以及冲突解决策略。在理想情况下,哈希表的操作可以达到O(1)的时间复杂度。但在实际情况中,由于哈希冲突的存在,这些操作的时间复杂度可能会退化。然而,通过优化哈希函数、控制装载因子和选择合适的冲突解决策略,我们可以将哈希表的操作保持在接近O(1)的时间复杂度范围内。
空间复杂度分析
1、哈希表的空间复杂度通常是O(n),其中n是哈希表中存储的元素个数。这是因为哈希表通过分配一个固定大小的数组(或类似的连续存储空间)来存储元素,并且这个数组的大小通常是基于预期的元素数量来选择的。尽管哈希表可能会预留一些额外的空间以减少哈希冲突和提高性能,但总体上,哈希表所需的空间与存储的元素数量成线性关系。
2、链地址法(Separate Chaining):在链地址法中,每个槽位维护一个链表来存储所有映射到该槽位的关键字。这种情况下,哈希表的空间复杂度仍然是O(n),因为虽然链表的长度可能随着冲突的增加而增长,但总体上,所有链表占用的空间仍然与存储的元素数量成线性关系。
3、开放地址法(Open Addressing):在开放地址法中,哈希表通过探测空闲槽位来解决冲突。这种方法不需要额外的数据结构来存储冲突的元素,因此从空间复杂度的角度来看,它通常比链地址法更节省空间。然而,它可能会导致哈希表在达到其容量之前就开始拒绝新的插入操作(因为找不到空闲槽位),从而需要更频繁地进行扩容操作。
相关文章:
数据结构 - 哈希表
文章目录 前言一、哈希思想二、哈希表概念三、哈希函数1、哈希函数设计原则2、常用的哈希函数 四、哈希冲突1、什么是哈希冲突2、解决哈希冲突闭散列开散列 五、哈希表的性能分析时间复杂度分析空间复杂度分析 前言 一、哈希思想 哈希思想(Hashing)是计…...
电商选品这几点没做好,等于放弃80%的流量!
在竞争激烈的电商领域,选品是决定店铺命运的核心环节。到底是哪些关键要点能够帮助我们在选品时抢占流量高地,稳步出单呢? 一、深入了解市场需求 选品的第一步是对市场进行深入调研。要关注当前的消费趋势、热门品类以及潜在的需求缺口。通…...
【教程】最新可用!Docker国内镜像源列表
转载请注明出处:小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你,欢迎[点赞、收藏、关注]哦~ 目录 镜像加速器地址 用法示例 一、自动配置地址 二、配置单次地址 镜像加速器地址 Docker镜像加速站https://hub.uuuadc.top/docker.1panel.live…...
使用RabbitMQ在Spring Boot入门实现简单的消息的发送与接收
文章目录 要引入spring-boot-starter-amqp依赖才能开始后续操作 1. 配置RabbitMQ地址2. 编写消息发送测试类3. 实现消息接收 在本文中,我们将介绍如何在Spring Boot应用中使用RabbitMQ实现消息的发送与接收。我们将创建两个服务,一个用于发送消息&#x…...
基于物联网的水质监测系统设计与实现:React前端、Node.js后端与TCP/IP协议的云平台集成(代码示例)
一、项目概述 随着环境保护意识的增强,水质监测在水资源管理和污染防治中变得尤为重要。本项目旨在设计一个基于物联网的水质监测系统,能够实时监测水中的pH值、溶解氧、电导率和浊度等参数,并将数据传输至云端,以便进行分析和可…...
Vcpkg安装指定版本包或自定义安装包
在使用 vcpkg 安装特定版本的包或自定义包时,你可以按照以下步骤进行操作: 安装特定版本的包 列出可用的版本: 使用以下命令列出特定包的所有可用版本: vcpkg search <package-name>安装特定版本: 使用 vcpkg …...
【C++深度探索】红黑树实现Set与Map的封装
🔥 个人主页:大耳朵土土垚 🔥 所属专栏:C从入门至进阶 这里将会不定期更新有关C/C的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉 文章目录…...
终于有人把客户成功讲明白了
作者:沈建明 对ToB企业来说,只有客户成功才能带来持久增长,在SaaS企业下行大背景下,客户成功是唯一的救命稻草。大家是不是都听过这样的说法? ToB和SaaS企业的老客户贡献对于企业至关重要。因为获取新客户的成本是留…...
[新械专栏] 肾动脉射频消融仪及一次性使用网状肾动脉射频消融导管获批上市
近日,国家药品监督管理局批准了上海魅丽纬叶医疗科技有限公司“肾动脉射频消融仪”和“一次性使用网状肾动脉射频消融导管”两个创新产品注册申请。 肾动脉射频消融仪由主机、脚踏开关、主机连接线、中性电极连接线以及电源线组成。一次性使用网状肾动脉射频消融导…...
leetcode-119-杨辉三角II
原理: 1、初始化每行一维数组nums[1]; 2、从第2行开始,在nums的头插入0(因为杨辉三角每行的第一个1相当于是上一行的1与其前面的0相加之和)后进行相加操作。 代码:...
【第八节】python正则表达式
目录 一、python中的re模块 1.1 基本匹配和搜索 1.2 替换和分割 1.3 编译正则表达式 二、正则表达式对象 2.1 re.RegexObject 和 re.MatchObject 2.2 正则表达式修饰符 - 可选标志 2.3 正则表达式模式 2.4 正则表达式实例 一、python中的re模块 正则表达式是一种独特的…...
三大浏览器Google Chrome、Edge、Firefox内存占用对比
问题 Chrome、Edg、Firefox三家究竟谁的占用少 结论 打开一个页面内存占用 Firefox>Edge>Chrome 打开打量页面内存占用 Firefox>Chrome>Edge 从监视器可以看到Edge增加一个页面增加一个页面不到100M而其它浏览器需要150M左右;Firefox浏览器主线程内存占用800M比…...
【wiki知识库】08.添加用户登录功能--后端SpringBoot部分
目录 一、今日目标 二、SpringBoot后端实现 2.1 新增UserLoginParam 2.2 修改UserController 2.3 UserServiceImpl代码 2.4 创建用户上下文工具类 2.5 通过token校验用户(重要) 2.6 创建WebMvcConfig 2.7 用户权限校验拦截器 一、今日目标 上篇…...
vue中nextTick的作用
nextTick是Vue.js提供的一个非常有用的方法,其主要作用是在DOM更新之后执行延迟回调函数。以下是nextTick的具体作用及其实现原理的详细解析: nextTick的作用 确保DOM更新完成: 当Vue实例的数据发生变化时,Vue会异步地更新DOM。…...
计算机网络面试-核心概念-问题理解
目录 1.计算机网络OSI协议七层结构功能分别是什么?如何理解这些功能 2.物理层、数据链路层、网络层、传输层和应用层,这五个层之间功能的关系,或者说是否存在协调关系 3. 数据链路层功能理解 4.MAC地址和以太网协议 5.以太网协议中的CSMA…...
go语言创建协程
前言 Go 语言中,协程是通过 go 关键字来创建的,这使得 Go 语言成为实现并发程序的一个非常直观和强大的工具。Go 运行时管理着协程,这些协程在内部被称为 goroutine。 协程(goroutines)本身是轻量级的线程,…...
RabbitMQ之基于注解声明队列交换机:使用@RabbitListener实现消息监听
文章目录 什么是RabbitListener?队列和交换机的基本概念使用RabbitListener注解声明队列和交换机代码解析1. QueueBinding2. 消费者方法 运行原理应用场景总结 在现代的微服务架构中,消息队列是一种重要的异步通信机制。RabbitMQ作为一种流行的消息代理软…...
【grafana 】mac端grafana配置的文件 grafana.ini 及login
brew services start grafana 以后,怎么知道mac端的配置文件的路径 brew services restart grafana#brew services start grafana在macOS上使用Homebrew安装并启动Grafana服务后,通常的配置文件路径是在以下两个位置之一: Homebrew默认配置文件路径:/usr/local/etc/grafana…...
程序员如何在人工智能时代保持核心竞争力
目录 1.概述 1.1. 技术深度与广度的平衡 1.2. 软技能的培养 1.3. 持续学习和适应性 1.4. 理解和应用AI 1.5. 伦理和责任意识 2.AI辅助编程对程序员工作的影响 2.1.AI工具对编码实践的积极影响 2.2.AI工具的潜在风险 2.3.如何平衡利与弊 3.程序员应重点发展的核心能力…...
回溯排列+棋盘问题篇--代码随想录算法训练营第二十三天| 46.全排列,47.全排列 II,51. N皇后,37. 解数独
46.全排列 题目链接:. - 力扣(LeetCode) 讲解视频: 组合与排列的区别,回溯算法求解的时候,有何不同? 题目描述: 给定一个不含重复数字的数组 nums ,返回其 所有可能…...
大数据学习栈记——Neo4j的安装与使用
本文介绍图数据库Neofj的安装与使用,操作系统:Ubuntu24.04,Neofj版本:2025.04.0。 Apt安装 Neofj可以进行官网安装:Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...
【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...
并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...
【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...
协议转换利器,profinet转ethercat网关的两大派系,各有千秋
随着工业以太网的发展,其高效、便捷、协议开放、易于冗余等诸多优点,被越来越多的工业现场所采用。西门子SIMATIC S7-1200/1500系列PLC集成有Profinet接口,具有实时性、开放性,使用TCP/IP和IT标准,符合基于工业以太网的…...
Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...
