C++数据结构 -- 哈希表

目录

  • 一、哈希概念
  • 二、 哈希冲突
  • 三、 哈希函数
  • 四、 减少哈希冲突常用的方法
    • 4.1 闭散列
      • 4.1.1 闭散列的开放定址法的增容
      • 4.1.2 闭散列的开放定址法的哈希结构的实现
    • 4.3 开散列
      • 4.3.1 开散列概念
      • 4.3.2 插入元素
      • 4.3.2 删除元素
      • 4.3.3 开散列的哈希桶的增容
      • 4.3.4 开散列的哈希桶(拉链法)代码实现
      • 4.3.5 关于开散列的思考
      • 4.3.6 开散列与闭散列比较

一、哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( l o g 2 N log_2 N log2N),而搜索的效率取决于搜索过程中元素的比较次数。
所以理想的搜索方法是:可以不经过任何比较,一次直接从表中得到要搜索的元素。
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

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

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

以上方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
例如插入:11,3,4,25
在这里插入图片描述

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。
那上述的哈希表中再插入一个1呢?通过计算hash(1)%10=1,也就是说1应该插入到下标为1的位置中,但是hash[1]已经插入了11了。

二、 哈希冲突

对于两个数据元素的关键字 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),即:不同关键字通过相同哈希算法计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞。

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

三、 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
哈希函数设计原则:
1、哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间。
2、哈希函数计算出来的地址能均匀分布在整个空间中。
3、哈希函数应该比较简单。

常见哈希函数:

  1. 直接定址法–(常用)
    取关键字的某个线性函数为散列地址:
    Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况

  2. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
    按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

  3. 平方取中法–(了解)
    假设关键字为2345,对它平方就是5499025,抽取中间的3位990作为哈希地址;
    再比如关键字为5432,对它平方就是29506624,抽取中间的3位506作为哈希地址
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

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

  5. 随机数法–(了解一下即可)
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
    通常应用于关键字长度不等时采用此法

  6. 数学分析法–(了解一下)
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:
    假设要存储班级学生的登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现冲突,还可以对抽取出来的数字进行反转(如5678改成8765)、右环位移(如5678改成8567)、左环移位、前两数与后两数叠加(如5678改成56+78=134)等方法。
    数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况

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

我们在以下的内容中是使用除留余数法实现哈希表的。

四、 减少哈希冲突常用的方法

闭散列和开散列。

4.1 闭散列

闭散列,也称开放定址法。当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?
1、线性探测
插入元素:
如果计算出来的位置已经有元素,那么就通过线性探测往后找到一个空位置插入即可。
在这里插入图片描述
在这里插入图片描述

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

所以对于闭散列的哈希表的存放的数据中应该包含一个标志状态的变量,新插入的节点的状态标志位是EXIST,被删除的节点的标志位设置为DELETE,没插入的节点的标志位默认设置为EMPTY。

线性探测优点:实现非常简单。
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。那么这种情况该如何缓解呢?

2、二次探测
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i =1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。简单来说就是在往后找空位置的时候不是直接找下一个位置,而是找下一个位置平方对应的位置。这样就能更好地缓解冲突的数据全部堆积在一块的问题了。

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

因此:闭散列最大的缺陷就是空间利用率比较低,基本上浪费了50%的空间,同时这也是哈希的缺陷。

4.1.1 闭散列的开放定址法的增容

哈希表会在什么情况下扩容?如何扩容?
在这里插入图片描述

			//负载因子(有效数据在哈希表中的占比)超过70%就进行扩容if (_n * 10 / _table.size() >= 7){//扩容size_t newSize = _table.size() * 2;//这里直接定义一个新的哈希表对象,好处是能够直接复用这个Insert函数,//最后进行指针的交换,为什么这里适合复用Insert函数,因为闭散列的开//放定址法的核心就是线性探测,如果这里不复用Insert自己写的话,跟下面//线性探测的逻辑是一模一样的,所以没必要重新写HashTable<K, V, HashFunc> newHT;newHT._table.resize(newSize);for (size_t i = 0; i < _table.size(); i++){//把旧表中的存在的数据插入到新的哈希表if (_table[i]._state == EXIST){newHT.Insert(_table[i]._kv);}}//两个vector交换,本质是交换vector的_start,_finish,_endofstorage指针,刚好还把//旧哈希表中_start,_finish,_endofstorage指针交换给newTable,newTable是局部对象,//出了作用域自动销毁,刚好满足我们的需求_table.swap(newHT._table);}

4.1.2 闭散列的开放定址法的哈希结构的实现

#include <iostream>
using namespace std;
#include <vector>//仿函数
template <class K>
struct DefaultHashFunc
{size_t operator()(const K& key){//这里强转可以解决负数的映射问题return (size_t)key;}
};//针对string类型的特化的仿函数,所有字符的ASCII值结合起来
template<>
struct DefaultHashFunc<string>
{size_t operator()(const string& s){size_t ret = 0;for (const auto& ch : s){//这个131是别人经过研究计算出来的一个值//*=131目的是想尽可能地减少哈希冲突(哈希碰撞)ret *= 131;ret += ch;}return ret;}
};//闭散列的开放定址法
namespace open_address
{enum State{//有效值EXIST,//无效值DELETE,//空EMPTY};//闭散列存放的数据是一个pair和这个位置的pair的状态,因为删除的时候不能把这个值去掉,所以//任何操作都是改变这个数据的状态而已template <class K,class V>struct HashData{pair<K, V> _kv;State _state = EMPTY;//节点的默认状态应该是EMPTY的,当插入或者删除值的时候再修改};template <class K,class V,class HashFunc= DefaultHashFunc<K>>class HashTable{public://构造函数HashTable(){//提前开辟10个空间_table.resize(10);}//防止外面通过指针修改key值,所以HashData<const K, V>*加上constHashData<const K, V>* Find(const K& key){HashFunc hf;//先到key映射的下标中找,如果找到了就直接返回,如果没找到就往后找//找到最后没找到就绕回来前面找,直到找到EMPTY还没有找到就说明这个//节点不存在了//这里的key不一定是整形,而只有整形的数才能取模,所以我们要通过仿函数//把这个数据的key值变成整形然后取出来,这样才能支持取模size_t hashi = hf(key) % _table.size();//遇到删除状态为DELETE的也要继续找,因为DELETE的节点的后面(或者前面)还有值while (_table[hashi]._state != EMPTY){//找到了就返回地址if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key){//这里需要强转一下,因为&_table[hashi]的类型是HashData<K,V>*return (HashData<const K, V>*)&_table[hashi];}hashi++;//取模一下可以从最后一个位置绕回来到第一个位置hashi %= _table.size();}//走到这里说明找不到这个key了,返回空return nullptr;}bool Erase(const K& key){HashData<const K, V>* ret = Find(key);if (ret){//删除操作就是把该节点的状态改成DELETE即可,无需修改它的值,因为你也不知道该改成什么值ret->_state = DELETE;_n--;return true;}else{return false;}}bool Insert(const pair<K, V>& kv){HashFunc hf;//如果插入的节点存在,那么就不用插入了if (Find(kv.first) != nullptr){return false;}//负载因子(有效数据在哈希表中的占比)超过70%就进行扩容if (_n * 10 / _table.size() >= 7){//扩容size_t newSize = _table.size() * 2;//这里直接定义一个新的哈希表对象,好处是能够直接复用这个Insert函数,//最后进行指针的交换,为什么这里适合复用Insert函数,因为闭散列的开//放定址法的核心就是线性探测,如果这里不复用Insert自己写的话,跟下面//线性探测的逻辑是一模一样的,所以没必要重新写HashTable<K, V, HashFunc> newHT;newHT._table.resize(newSize);for (size_t i = 0; i < _table.size(); i++){//把旧表中的存在的数据插入到新的哈希表if (_table[i]._state == EXIST){newHT.Insert(_table[i]._kv);}}//两个vector交换,本质是交换vector的_start,_finish,_endofstorage指针,刚好还把//旧哈希表中_start,_finish,_endofstorage指针交换给newTable,newTable是局部对象,//出了作用域自动销毁,刚好满足我们的需求_table.swap(newHT._table);}//计算映射的位置(下标)//这里模的必须是_table.size(),因为_table是vector结构,有效的访问的区间是[0,_table.size()),//如果这里模的是_table.capacity(),那么得到的值可能大于_table.size(),被看作为越界访问size_t hashi = hf(kv.first) % _table.size();//线性探测//找到下一个被删除的或者为空的位置(可能是hashi本身,如果不是就往后找)while (_table[hashi]._state == EXIST){hashi++;//取模一下可以从最后一个位置绕回来到第一个位置继续找,我们的结构的设计保证了这里一定能够//找到一个空(或者被删除了的)位置,因为有效值的占比超过70%就会扩容了,所以这个数组不可能被填满的hashi %= _table.size();}//走到这里说明一定找到了一个空(或者被删除了的)位置,因为数组的元素本身已经初始化成了HashData<K,V>的默认值,//所以我们修改这个节点的pair和状态即可_table[hashi]._kv = kv;//把这个位置的节点的状态值标志位EXIST,表示这是一个有效值_table[hashi]._state = EXIST;_n++;return true;}//打印函数void Print(){for (size_t i = 0; i < _table.size(); i++){if (_table[i]._state == EXIST){cout << _table[i]._kv.first << ":" << _table[i]._kv.second << endl;}}cout << endl;}private:vector<HashData<K, V>> _table;//记录插入的有效数据的个数size_t _n = 0;};
}

4.3 开散列

4.3.1 开散列概念

开散列法又叫链地址法(拉链法或者哈希桶),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
在这里插入图片描述
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

4.3.2 插入元素

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3.2 删除元素

就是先计算要删除的元素在数组中的哪一个位置,再在这个位置对应的单链表中查找要删除的元素并删除即可,本质还是单链表的删除。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3.3 开散列的哈希桶的增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

			//扩容,插入的有效节点数等于数组的大小就扩容if (_n == _table.size()){size_t newSize = _table.size() * 2;//这里还适合定义一个新的对象,然后复用这个Insert函数插入旧表中的所有值吗?//这里就不适合了,为什么呢?// 1、因为这里数组中存放的是节点的指针,如果复用这个//Insert函数,在Insert的逻辑中就会重新开辟出所有的节点插入到这个新对象,// 然后还要自己把旧表中的所有的节点释放掉,反而变得麻烦了。// 2、并且这里的插入并不需要线性探测找位置,直接头插到数组对应的位置就行了,所以//这里可以选择直接开辟一个vector数组,然后把旧表中的所有节点都直接转移到这个vector//这样新的vector和旧的vector交换就可以了,因为这里是转移节点,所以旧的vector中已//经没有节点了,此时无需再自己手动释放掉旧的节点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;//计算旧节点在新的vector中的位置size_t hashi = hf(cur->_kv.first) % newTable.size();//把节点从旧表中转移头插到新表cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}//最后记得把旧表的该位置置空,免得后面非法访问_table[i] = nullptr;}//最后交换两个vector表即可_table.swap(newTable);}

4.3.4 开散列的哈希桶(拉链法)代码实现

//仿函数
template <class K>
struct DefaultHashFunc
{size_t operator()(const K& key){//这里强转可以解决负数的映射问题return (size_t)key;}
};//针对string类型的特化的仿函数,所有字符的ASCII值结合起来
template<>
struct DefaultHashFunc<string>
{size_t operator()(const string& s){size_t ret = 0;for (const auto& ch : s){//这个131是别人经过研究计算出来的一个值//*=131目的是想尽可能地减少哈希冲突(哈希碰撞)ret *= 131;ret += ch;}return ret;}
};//拉链法,哈希桶
namespace hash_bucket
{//拉链法中数组存放的是一个pair和_next指针的节点template <class K,class V>struct HashNode{pair<const K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<const 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;}}Node* Find(const K& key){//利用仿函数把key变成可以取模的值,因为key可能是string等本身不能取模的类型HashFunc hf;//像函数一样使用这个仿函数,利用的一定的规则把key转换成某个整形,以便取模求出//key值应该存储的位置的下标size_t hashi = hf(key) % _table.size();//遍历这个下标对应位置的链表,如果key存在,则一定在这个链表中Node* cur = _table[hashi];//遍历该key映射的数组位置的链表查找key值的节点while (cur){//找到了直接返回这个节点指针if (cur->_kv.first == key){return cur;}cur = cur->_next;}//找不到返回空return nullptr;}bool Insert(const pair<K, V>& kv){//如果该kv已经存在,就不再插入了,直接返回if (Find(kv.first)!=nullptr){return false;}HashFunc hf;//扩容,插入的有效节点数等于数组的大小就扩容if (_n == _table.size()){size_t newSize = _table.size() * 2;//这里还适合定义一个新的对象,然后复用这个Insert函数插入旧表中的所有值吗?//这里就不适合了,为什么呢?// 1、因为这里数组中存放的是节点的指针,如果复用这个//Insert函数,在Insert的逻辑中就会重新开辟出所有的节点插入到这个新对象,// 然后还要自己把旧表中的所有的节点释放掉,反而变得麻烦了。// 2、并且这里的插入并不需要线性探测找位置,直接头插到数组对应的位置就行了,所以//这里可以选择直接开辟一个vector数组,然后把旧表中的所有节点都直接转移到这个vector//这样新的vector和旧的vector交换就可以了,因为这里是转移节点,所以旧的vector中已//经没有节点了,此时无需再自己手动释放掉旧的节点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;//计算旧节点在新的vector中的位置size_t hashi = hf(cur->_kv.first) % newTable.size();//把节点从旧表中转移头插到新表cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}//最后记得把旧表的该位置置空,免得后面非法访问_table[i] = nullptr;}//最后交换两个vector表即可_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;}bool Erase(const K& key){HashFunc hf;//计算删除元素在表中的位置size_t hashi = hf(key) % _table.size();//遍历表中对应下标位置的单链表找删除元素Node* cur = _table[hashi];Node* prev = nullptr;while (cur){//找到删除元素if (cur->_kv.first == key){//头删if (prev == nullptr){_table[hashi] = cur->_next;}//中间删除else{prev->_next = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}void Print(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];printf("[%d]->", i);while (cur){cout << "(" << cur->_kv.first << ":" << cur->_kv.second << ")" << "->";cur = cur->_next;}cout << "nullptr" << endl;}}private:vector<Node*> _table;//哈希表数组存放的是节点的指针,这个节点是一个链表的头节点size_t _n = 0;//记录插入的有效节点的个数};
}

4.3.5 关于开散列的思考

1、只能存储key为整形的元素,其他类型怎么解决?
因为我们计算元素在数组中存储的位置是通过除留余数法计算的,即用key模数组的大小得到的余数就是该元素在数组中的存储位置,也就意味着我们的key必须要是整形,因为只有整形才能取模,那么如果我们存储的key是一个字符串呢,字符串是不能取模的,这个时候我们该怎么处理呢?
如果我们要继续采用除留余数法计算key值对应的位置,我们就要想办法把这个字符串按照一定的规则转化成整形,再进行取模,这样就能得到这个字符串在数组中对应的存储位置了。
那么我们该如何把这个string转化成一个整形呢?
这个时候仿函数就要登场了,仿函数的作用在这种场景的就非常的好用,无论插入元素的key是什么类型的值我们都可以通过仿函数把它转化成整形,进而通过除留余数法计算出它在数组中的存储位置了。

//仿函数
template <class K>
struct DefaultHashFunc
{//针对于存储元素的key值是int类型的仿函数,返回值就是这个key本身size_t operator()(const K& key){//这里强转可以解决负数的映射问题return (size_t)key;}
};//针对string类型的特化的仿函数,所有字符的ASCII值结合起来
template<>
struct DefaultHashFunc<string>
{//如果存储元素的key值是string,那么就把这个字符串的所有字符结合起来,算出一个整形size_t operator()(const string& s){size_t ret = 0;for (const auto& ch : s){//这个131是别人经过研究计算出来的一个值//*=131目的是想尽可能地减少哈希冲突(哈希碰撞)ret *= 131;ret += ch;}return ret;}
};

2、除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

以下是别人通过测试计算出来的哈希冲突较小的哈希函数:

size_t GetNextPrime(size_t prime)
{const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul,25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul,1610612741ul, 3221225473ul, 4294967291ul};size_t i = 0;for (; i < PRIMECOUNT; ++i){if (primeList[i] > prime)return primeList[i];}return primeList[i];
}

4.3.6 开散列与闭散列比较

拉链法(哈希桶)处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于闭散列的开放地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用哈希桶反而比开放地址法更节省存储空间。

以上就是今天想要跟大家分享的内容啦,你学会了吗?如果感觉到有所帮助,你就点点赞点点关注呗,后期还会持续更新C++相关的知识哦,我们下期见啦!!!!!

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

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

相关文章

快速搭建SpringBoot3.x项目

快速搭建SpringBoot3.x项目 写在前面一、创建项目二、配置多环境三、连接数据库查询数据3.1 新建数据库mybatisdemo并且创建sys_user表3.2 创建实体类3.2 创建Mapper接口3.3 添加mybatis.xml文件3.4 新建service 接口及实现类3.5 创建Controller 四、封装统一结果返回4.1 定义 …

Attention is all you need 论文笔记

该论文引入Transformer&#xff0c;主要核心是自注意力机制&#xff0c;自注意力&#xff08;Self-Attention&#xff09;机制是一种可以考虑输入序列中所有位置信息的机制。 RNN介绍 引入RNN为了更好的处理序列信息&#xff0c;比如我 吃 苹果&#xff0c;前后的输入之间是有…

【问题记录】解决Git上传文件到GitHub时收到 “GH001: Large files detected” 错误信息!

环境 Windows 11 家庭中文版git version 2.41.0.windows.1GitHub 问题情况 在命令行中使用git上传pdf文件到GitHub服务器时&#xff0c;提示了如下警告信息&#xff1a; 原因是 GitHub 有一个文件大小限制&#xff0c;通常为 100 MB。 如果尝试上传大于此限制的文件&#xff0c…

Long类型雪花算法ID返回前端后三位精度缺失问题解决

目录 一、问题描述二、问题复现1.Maven依赖2.application.yml 配置3.DemoController.java4.snowflakePage.html 页面5.DemoControllerAdvice.java 监听6.问题复现 三、原因分析四、问题解决方案一方案二 一、问题描述 Java 后端使用雪花算法生成 Long 类型的主键 ID&#xff0…

快速学会搭建微信小程序的基础架构

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 基础架构 构建界面 引入 uni-ui 组件库 组件自动引入 配置TS类型 状态管理 持久化 数据交互 请…

最小二乘法

Least Square Method 1、相关的矩阵公式2、线性回归3、最小二乘法3.1、损失函数&#xff08;Loss Function&#xff09;3.2、多维空间的损失函数3.3、解析法求解3.4、梯度下降法求解 1、相关的矩阵公式 P r e c o n d i t i o n : ξ ∈ R n , A ∈ R n ∗ n i : σ A ξ σ ξ…

leetcode 332. Reconstruct Itinerary(重构行程)

有一些票tickets, tickets[ i ] [from, to], 每个出发到达城市名字都是3个大写英文字母&#xff0c; 同一个出发城市时&#xff0c;优先去字母顺序较小的到达城市。 必须先从“JFK”出发。 每个ticket必须用且只用一次&#xff0c;所有ticket一定会形成至少一个有效的行程&…

【JAVA-Day21】序列化和反序列化,学会Java的编解码方法

标题序列化和反序列化&#xff0c;学会Java的编解码方法 序列化和反序列化&#xff0c;学会Java的编解码方法摘要引言一、什么是序列化1.1 序列化的过程 二、什么是反序列化2.1 反序列化的过程 三、为什么要进行序列化和反序列化3.1 主要目的3.2 应用场景 四、总结参考资料 博主…

Springboot 实践(18)Nacos配置中心参数自动刷新测试

前文讲解了Nacos 2.2.3配置中心的服务端的下载安装&#xff0c;和springboot整合nacos的客户端。Springboot整合nacos关键在于使用的jar版本要匹配&#xff0c;文中使用版本如下&#xff1a; ☆ springboot版本: 2.1.5.RELEASE ☆ spring cloud版本 Greenwich.RELEASE ☆ sp…

辅助驾驶功能开发-功能规范篇(21)-4-XP行泊一体方案功能规范

XPilot Parking 自动泊车系统 • 超级自动泊车辅助(Super AutoParking Assist)、语音控制泊车辅助(Autoparking with Speech) - 产品定义 超级自动泊车辅助是⼀个增强的自动泊车辅助系统。在超级自动泊车辅助系统中,识别车位将会变得实时可见, 并且不可泊入的⻋位也将…

如何在 Excel 中计算日期之间的天数

计算两个日期之间的天数是 Excel中的常见操作。无论您是规划项目时间表、跟踪时间还是分析一段时间内的趋势&#xff0c;了解如何在 Excel 中查找日期之间的天数都可以提供强大的日期计算功能。 幸运的是&#xff0c;Excel 提供了多种简单的方法来获取两个日期之间的天数。继续…

数据可视化

一、Flask #通过访问路径&#xff0c;获取用户的字符串参数 app.route(/user/<name>) def welcome(name):return "你好&#xff0c;%s"%nameapp.route(/user/<int:id>) def welcome2(id):return "你好&#xff0c;%d号的会员"%id能够自动根据…

【初阶数据结构】树(tree)的基本概念——C语言

目录 一、树&#xff08;tree&#xff09; 1.1树的概念及结构 1.2树的相关概念 1.3树的表示 1.4树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09; 二、二叉树的概念及结构 2.1二叉树的概念 2.2现实中真正的二叉树 2.3特殊的二叉树 2.4二叉树的性质…

MFC主框架和视类PreCreateWindow()函数学习

在VC生成的单文档应用程序中&#xff0c;主框架类和视类均具有PreCreateWindow函数&#xff1b; 从名字可知&#xff0c;可在此函数中添加一些代码&#xff0c;来控制窗口显示后的效果&#xff1b; 并且它有注释说明&#xff0c; Modify the Window class or styles here by…

Bash脚本学习:AWK, SED

1. AWK AWK 是一种编程语言&#xff0c;设计用于处理文件或数据流中基于文本的数据&#xff0c;或者使用 shell 管道。 可以将 awk 与 shell 脚本结合使用或直接在 shell 提示符下使用。 以上展示使用AWK分别打印第一个位置变量和第二个位置变量。 建立一个文档 csvtest.cs…

Scapy 解析 pcap 文件从HTTP流量中提取图片

Scapy 解析 pcap 文件从HTTP流量中提取图片 前言一、网络环境示例二、嗅探流量示例三、pcap 文件处理最后参考 ​ 作者&#xff1a;高玉涵 ​ 时间&#xff1a;2023.9.17 10:25 ​ 环境&#xff1a;Linux kali 5.15.0-kali3-amd64&#xff0c;Python 3.11.4&#xff0c;scapy…

【OpenSSL】VC编译OpenSSL

VC编译OpenSSL 编译工具准备编译OpenSSL建立Hello World工程创建VS工程 编译工具准备 安装好Visual Studio。安装Perl, 主要是用来生成nmake的。准备好汇编语言编译工具nasm,并添加到path路径。下载好Open SSL源代码。 编译OpenSSL 安装Perl,并加入到path路径&#xff0c;检验…

HTML 学习笔记(基础)

它是超文本标记语言&#xff0c;由一大堆约定俗成的标签组成&#xff0c;而其标签里一般又有一些属性值可以设置。 W3C标准&#xff1a;网页主要三大部分 结构&#xff1a;HTML表现&#xff1a;CSS行为&#xff1a;JavaScript <!DOCTYPE html> <html lang"zh-…

使用 Feature Flags 实现数据库灰度迁移的监控与可观测性

作者&#xff1a;观测云与胡博 场景描述 很多企业会遇到数据库升级、或数据库迁移的情况&#xff0c;尤其是在自建数据库服务向云数据库服务、自建机房向云机房、旧数据库向新数据库迁移等场景。 然而&#xff0c;我们需要在整个移植过程中保证其稳定性、避免数据遗失、服务宕…

后端中间件安装与启动(Redis、Nginx、Nacos、Kafka)

后端中间件安装与启动 RedisNginxNacosKafka Redis 1.打开cmd终端&#xff0c;进入redis文件目录 2.输入redis-server.exe redis.windows.conf即可启动&#xff0c;不能关闭cmd窗口 &#xff08;端口配置方式&#xff1a;redis目录下的redis.windows.conf配置文件&#xff0c;…