【数据结构】C++实现哈希表

闭散列哈希表

哈希表的结构

在闭散列的哈希表中,哈希表每个位置除了存储所给数据之外,还应该存储该位置当前的状态,哈希表中每个位置的可能状态如下:

  1. EMPTY(无数据的空位置)。
  2. EXIST(已存储数据)。
  3. DELETE(原本有数据,但现在被删除了)。

我们可以用枚举定义这三个状态。

// 闭散列哈希表
enum State {EMPTY,// 哈希表位置为NULLEXITS,// 哈希表位置有值了DELETE// 哈希表位置为删除标志
};

为什么需要标识哈希表中每个位置的状态?

若是不设置哈希表中每个位置的状态,那么在哈希表中查找数据的时候可能是这样的。以除留余数法的线性探测为例,我们若是要判断下面这个哈希表是否存在元素40,步骤如下:

  1. 通过除留余数法求得元素40在该哈希表中的哈希地址是0。
  2. 从0下标开始向后进行查找,若找到了40则说明存在。

但是我们在寻找元素40时,不可能从0下标开始将整个哈希表全部遍历一次,这样就失去了哈希的意义。我们只需要从0下标开始往后查找,直到找到元素40判定为存在,或是找到一个空位置判定为不存在即可。

在这里插入图片描述

因为线性探测在为冲突元素寻找下一个位置时是依次往后寻找的,既然我们已经找到了一个空位置,那就说明这个空位置的后面不会再有从下标0位置开始冲突的元素了。比如我们要判断该哈希表中是否存在元素90,步骤如下:

  1. 通过除留余数法求得元素90在该哈希表中的哈希地址是0。
  2. 从0下标开始向后进行查找,直到找到下标为5的空位置,停止查找,判定元素90不存在。

但这种方式是不可行的,原因如下:

  1. 如何标识一个空位置?用数字0吗?那如果我们要存储的元素就是0怎么办?因此我们必须要单独给每个位置设置一个状态字段。
  2. 如果只给哈希表中的每个位置设置存在和不存在两种状态,那么当遇到下面这种情况时就会出现错误。

我们先将上述哈希表当中的元素1000找到,并将其删除,此时我们要判断当前哈希表当中是否存在元素40,当我们从0下标开始往后找到2下标(空位置)时,我们就应该停下来,此时并没有找到元素40,但是元素40却在哈希表中存在。

在这里插入图片描述

因此我们必须为哈希表中的每一个位置设置一个状态,并且每个位置的状态应该有三种可能,当哈希表中的一个元素被删除后,我们不应该简单的将该位置的状态设置为EMPTY,而是应该将该位置的状态设置为DELETE。

这样一来,当我们在哈希表中查找元素的过程中,若当前位置的元素与待查找的元素不匹配,但是当前位置的状态是EXIST或是DELETE,那么我们都应该继续往后进行查找,而当我们插入元素的时候,可以将元素插入到状态为EMPTY或是DELETE的位置。

因此,闭散列的哈希表中的每个位置存储的结构,应该包括所给数据和该位置的当前状态。

template<class K, class V>
struct HashData {pair<K, V> _kv;State _state = EMPTY;//状态初始化为空
};

而为了在插入元素时好计算当前哈希表的负载因子,我们还应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。

//哈希表
template<class K, class V>
class HashTable{
public://...
private:vector<HashData<K, V>> _tables;// 将Hash值存放在vector中size_t _n = 0;                 // 存储的数据个数
};

哈希表的查找

在哈希表中查找数据的步骤如下:

  1. 先判断哈希表的大小是否为0,若为0则查找失败。
  2. 通过哈希函数计算出对应的哈希地址。
  3. 从哈希地址处开始,采用线性探测向后向后进行数据的查找,直到找到待查找的元素判定为查找成功,或找到一个状态为EMPTY的位置判定为查找失败。

注意: 在查找过程中,必须找到位置状态为EXIST,并且key值匹配的元素,才算查找成功。若仅仅是key值匹配,但该位置当前状态为DELETE,则还需继续进行查找,因为该位置的元素已经被删除了。

HashData<K, V> *Find(const K &key) {//哈希表大小为0,表示哈希表为空,返回nullptrif (this->_tables.size() == 0) {return nullptr;}//哈希函数size_t hashi = key % this->_tables.size();size_t i = 1;size_t index = hashi;// index是插入的位置// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找while (this->_tables[index]._state != EMPTY) {// 当表中值跟key相等,并且状态为存在时才返回,因为可能值的状态被改为了delete说明刚刚被删除,不可以返回if (this->_tables[index]._kv.first == key && this->_tables[index]._state == EXITS) {return &this->_tables[index];}index = hashi + i;  //线性探测//index = hashi + i * i;  //二次探测index %= this->_tables.size();// 防止index越界,绕回去i++;// 这里的_state可能都是存在或者删除,那么程序就可能陷入死循环,所以需要给定条件退出// 如果已经查找一圈,那么说明全是存在+删除if (index == hashi) {break;}}return nullptr;
}

哈希表的插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。

其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10。
  • 若哈希表的负载因子大于0.7,则先创建一个新的哈希表,该哈希表的大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

注意: 在将原哈希表的数据插入到新哈希表的过程中,不能只是简单的将原哈希表中的数据对应的挪到新哈希表中,而是需要根据新哈希表的大小重新计算每个数据在新哈希表中的位置,然后再进行插入。

将键值对插入哈希表的具体步骤如下:

  1. 通过哈希函数计算出对应的哈希地址。
  2. 若产生哈希冲突,则从哈希地址处开始,采用线性探测向后寻找一个状态为EMPTY或DELETE的位置。
  3. 将键值对插入到该位置,并将该位置的状态设置为EXIST。

注意: 产生哈希冲突向后进行探测时,一定会找到一个合适位置进行插入,因为哈希表的负载因子是控制在0.7以下的,也就是说哈希表永远都不会被装满。

bool Insert(const pair<K, V> &kv) {//1.查找值if (Find(kv.first)) {return false;}// 当我们的哈希表是空或者负载因子大于0.7的时候,我们需要给将哈希表增容// 负载因子 = 表中有效数据个数 / 空间的大小if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {//为空的时候,给初始10,负载因子大于0.7就扩容两倍size_t newsize = this->_tables.size() == 0 ? 10 : this->_tables.size() * 2;HashTable<K, V> newHashTable;// 重新创建一个HashTable类newHashTable._tables.resize(newsize);// 遍历旧表,重新映射到新表for (auto &data: this->_tables) {// data是_table中的类型,对应的HashDataif (data._state == EXITS) {newHashTable.Insert(data._kv);// 将旧的kv插入到新的类对象中}}//交换this->_tables.swap(newHashTable._tables);}//哈希函数size_t hashi = kv.first % this->_tables.size();// 线形探测size_t i = 1;size_t index = hashi;// index是最后要插入的位置// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找while (this->_tables[index]._state == EXITS) {index = hashi + i;//index = hashi + i * i;  //二次线性探测index %= this->_tables.size();// 防止index越界,绕回去i++;}this->_tables[index]._kv = kv;this->_tables[index]._state = EXITS;this->_n++;// 存储的数据个数+1return true;
}

哈希表的删除

删除哈希表中的元素非常简单,我们只需要进行伪删除即可,也就是将待删除元素所在位置的状态设置为DELETE。

在哈希表中删除数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若不存在则删除失败。
  2. 若存在,则将该键值对所在位置的状态改为DELETE即可。
  3. 哈希表中的有效元素个数减一。

注意: 虽然删除元素时没有将该位置的数据清0,只是将该元素所在状态设为了DELETE,但是并不会造成空间的浪费,因为我们在插入数据时是可以将数据插入到状态为DELETE的位置的,此时插入的数据就会把该数据覆盖。

bool Erase(const K &key) {HashData<K, V> *ret = Find(key);if (ret) {ret->_state = DELETE;this->_n--;return true;} else {return false;}
}

完整代码

#pragma once
#include <iostream>
#include <vector>
using namespace std;
// 闭散列哈希表
enum State {EMPTY,// 哈希表位置为NULLEXITS,// 哈希表位置有值了DELETE// 哈希表位置为删除标志
};template<class K, class V>
struct HashData {pair<K, V> _kv;State _state = EMPTY;//状态初始化为空
};template<class K, class V>
class HashTable {
public:HashData<K, V> *Find(const K &key) {//哈希表大小为0,表示哈希表为空,返回nullptrif (this->_tables.size() == 0) {return nullptr;}//哈希函数size_t hashi = key % this->_tables.size();size_t i = 1;size_t index = hashi;// index是插入的位置// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找while (this->_tables[index]._state != EMPTY) {// 当表中值跟key相等,并且状态为存在时才返回,因为可能值的状态被改为了delete说明刚刚被删除,不可以返回if (this->_tables[index]._kv.first == key && this->_tables[index]._state == EXITS) {return &this->_tables[index];}index = hashi + i;//线性探测//index = hashi + i * i;  //二次探测index %= this->_tables.size();// 防止index越界,绕回去i++;// 这里的_state可能都是存在或者删除,那么程序就可能陷入死循环,所以需要给定条件退出// 如果已经查找一圈,那么说明全是存在+删除if (index == hashi) {break;}}return nullptr;}bool Insert(const pair<K, V> &kv) {//1.查找值if (Find(kv.first)) {return false;}// 当我们的哈希表是空或者负载因子大于0.7的时候,我们需要给将哈希表增容// 负载因子 = 表中有效数据个数 / 空间的大小if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) {//为空的时候,给初始10,负载因子大于0.7就扩容两倍size_t newsize = this->_tables.size() == 0 ? 10 : this->_tables.size() * 2;HashTable<K, V> newHashTable;// 重新创建一个HashTable类newHashTable._tables.resize(newsize);// 遍历旧表,重新映射到新表for (auto &data: this->_tables) {// data是_table中的类型,对应的HashDataif (data._state == EXITS) {newHashTable.Insert(data._kv);// 将旧的kv插入到新的类对象中}}//交换this->_tables.swap(newHashTable._tables);}//哈希函数size_t hashi = kv.first % this->_tables.size();// 线形探测size_t i = 1;size_t index = hashi;// index是最后要插入的位置// 当哈希状态为EXITS,说明表中位置已经有值,那么就继续查找while (this->_tables[index]._state == EXITS) {index = hashi + i;//index = hashi + i * i;  //二次线性探测index %= this->_tables.size();// 防止index越界,绕回去i++;}this->_tables[index]._kv = kv;this->_tables[index]._state = EXITS;this->_n++;// 存储的数据个数+1return true;}bool Erase(const K &key) {HashData<K, V> *ret = Find(key);if (ret) {ret->_state = DELETE;this->_n--;return true;} else {return false;}}private:vector<HashData<K, V>> _tables;// 将Hash值存放在vector中size_t _n = 0;                 // 存储的数据个数
};

开散列哈希表(哈希桶)

哈希表的结构

在开散列的哈希表中,哈希表的每个位置存储的实际上是某个单链表的头结点,即每个哈希桶中存储的数据实际上是一个结点类型,该结点类型除了存储所给数据之外,还需要存储一个结点指针用于指向下一个结点。

template<class K, class V>
struct HashNode {HashNode<K, V> *_next;pair<K, V> _kv;HashNode(const pair<K, V> &kv): _kv(kv), _next(nullptr) {}
};

与闭散列的哈希表不同的是,在实现开散列的哈希表时,我们不用为哈希表中的每个位置设置一个状态字段,因为在开散列的哈希表中,我们将哈希地址相同的元素都放到了同一个哈希桶中,并不需要经过探测寻找所谓的“下一个位置”。

哈希表的开散列实现方式,在插入数据时也需要根据负载因子判断是否需要增容,所以我们也应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。

//哈希表
template<class K, class V>
class HashTable{
public://...
private:vector<Node *> _tables;size_t n = 0;// 存储有效数据的个数
};

只能存储key为整形的元素,其他类型怎么解决?

使用模板特化编写仿函数

template<class K>
struct HashFunc {size_t operator()(const K &key) {return key;}
};// 特化模板,传string的话,就走这个
template<>
struct HashFunc<string> {size_t operator()(const string &s) {size_t hash = 0;for (auto ch: s) {hash += ch;hash *= 31;}return hash;}
};

这样我们的结构就变成了:

template<class K, class V, class Hash = HashFunc<K>>// Hash用于将key转换成可以取模的类型
class HashTable {
public://
private:vector<Node *> _tables;size_t n = 0;// 存储有效数据的个数
};

哈希表的查找

在哈希表中查找数据的步骤如下:

  1. 先判断哈希表的大小是否为0,若为0则查找失败。
  2. 通过哈希函数计算出对应的哈希地址。
  3. 通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可。
Node *Find(const K &key) {if (this->_tables.size() == 0) {return nullptr;}Hash hash;    //用于处理各种类型的仿函数size_t hashi = hash(key) % this->_tables.size();Node *cur = this->_tables[hashi];while (cur) {if (cur->_kv.first == key) {return cur;}cur = cur->_next;}return nullptr;
}

哈希表的插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。

其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10。
  • 若哈希表的负载因子已经等于1了,则先创建一个新的哈希表,该哈希表的大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

重点: 在将原哈希表的数据插入到新哈希表的过程中,不要通过复用插入函数将原哈希表中的数据插入到新哈希表,因为在这个过程中我们需要创建相同数据的结点插入到新哈希表,在插入完毕后还需要将原哈希表中的结点进行释放,多此一举。

实际上,我们只需要遍历原哈希表的每个哈希桶,通过哈希函数将每个哈希桶中的结点重新找到对应位置插入到新哈希表即可,不用进行结点的创建与释放。

说明一下: 下面代码中为了降低时间复杂度,在增容时取结点都是从单链表的表头开始向后依次取的

将键值对插入哈希表的具体步骤如下:

  1. 通过哈希函数计算出对应的哈希地址。
  2. 若产生哈希冲突,则直接将该结点头插到对应单链表即可。
bool Insert(const pair<K, V> &kv) {Hash hash;// 仿函数用于不能取模的值// 已经有这个数,就不用插入了if (Find(kv.first)) {return false;}// 负载因子 == 1时扩容if (this->n == this->_tables.size()) {// size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;size_t newsize = this->GetNextPrime(_tables.size());vector<Node *> newtables(newsize, nullptr);for (auto &cur: this->_tables) {// cur是Node*while (cur) {// 保存下一个Node *next = cur->_next;// 头插到新表size_t hashi = hash(cur->_kv.first) % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}size_t hashi = hash(kv.first) % this->_tables.size();// 头插Node *newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;this->n++;return true;
}

哈希表的删除

在哈希表中删除数据的步骤如下:

  1. 通过哈希函数计算出对应的哈希桶编号。
  2. 遍历对应的哈希桶,寻找待删除结点。
  3. 若找到了待删除结点,则将该结点从单链表中移除并释放。
  4. 删除结点后,将哈希表中的有效元素个数减一。

注意: 不要先调用查找函数判断待删除结点是否存在,这样做如果待删除不在哈希表中那还好,但如果待删除结点在哈希表,那我们还需要重新在哈希表中找到该结点并删除,还不如一开始就直接在哈希表中找,找到了就删除。

bool Erase(const K &key) {Hash hash;size_t hashi = hash(key) % this->_tables.size();//删除的时候需要找到前一个节点和后一个节点进行链接Node *prev = nullptr;Node *cur = this->_tables[hashi];//cur初始为头结点//遍历单链表while (cur) {if (cur->_kv.first == key) {if (prev == nullptr) {//要找的结点就是头结点则直接更新头this->_tables[hashi] = cur->_next;} else {//链接prev->_next = cur->_next;}delete cur;return true;} else {//更新prev和curprev = cur;cur = cur->_next;}}return false;
}

扩容优化

在哈希表中,使用素数作为表的大小可以有效地减少哈希冲突。这主要基于以下两点:

  1. 哈希函数的设计:哈希函数的目的是将键均匀地散列在哈希表中,以尽可能减少哈希冲突。许多哈希函数都会利用取模操作来计算元素在表中的位置,例如hash(key) = key % table_size。在这种情况下,如果table_size是素数,那么哈希函数就能够更好地将不同的键散列在表的不同位置,从而减少哈希冲突。
  2. 避免周期性模式:如果我们使用的哈希表大小不是一个素数,特别是如果它有多个不同的因子,那么可能会产生周期性的模式,同样的键可能会被映射到同一个位置。这是因为两个数字如果它们的差是哈希表大小的因子,那么它们的哈希值将会相同。使用素数作为表的大小可以避免这种情况,因为素数只有两个因子,1和它自己。

因此,当哈希表需要扩容时,通常选择下一个较大的素数作为新的表的大小,以优化哈希表的性能。

// 扩容优化,使用素数扩容
size_t GetNextPrime(size_t prime) {// SGIstatic 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};size_t i = 0;for (; i < __stl_num_primes; ++i) {if (__stl_prime_list[i] > prime)return __stl_prime_list[i];}return __stl_prime_list[i];
}

完整代码

#pragma once
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <vector>
using namespace std;template<class K, class V>
struct HashNode {HashNode<K, V> *_next;pair<K, V> _kv;HashNode(const pair<K, V> &kv): _kv(kv), _next(nullptr) {}
};template<class K>
struct HashFunc {size_t operator()(const K &key) {return key;}
};// 特化模板,传string的话,就走这个
template<>
struct HashFunc<string> {size_t operator()(const string &s) {size_t hash = 0;for (auto ch: s) {hash += ch;hash *= 31;}return hash;}
};template<class K, class V, class Hash = HashFunc<K>>// Hash用于将key转换成可以取模的类型
class HashTable {typedef HashNode<K, V> Node;public:~HashTable() {for (auto &cur: this->_tables) {while (cur) {Node *next = cur->_next;delete cur;cur = next;}cur = nullptr;}}Node *Find(const K &key) {if (this->_tables.size() == 0) {return nullptr;}Hash hash;size_t hashi = hash(key) % this->_tables.size();Node *cur = this->_tables[hashi];while (cur) {if (cur->_kv.first == key) {return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K &key) {Hash hash;size_t hashi = hash(key) % this->_tables.size();//删除的时候需要找到前一个节点和后一个节点进行链接Node *prev = nullptr;Node *cur = this->_tables[hashi];//cur初始为头结点//遍历单链表while (cur) {if (cur->_kv.first == key) {if (prev == nullptr) {//要找的结点就是头结点则直接更新头this->_tables[hashi] = cur->_next;} else {//链接prev->_next = cur->_next;}delete cur;return true;} else {//更新prev和curprev = cur;cur = cur->_next;}}return false;}// 扩容优化,使用素数扩容size_t GetNextPrime(size_t prime) {// SGIstatic 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};size_t i = 0;for (; i < __stl_num_primes; ++i) {if (__stl_prime_list[i] > prime)return __stl_prime_list[i];}return __stl_prime_list[i];}bool Insert(const pair<K, V> &kv) {Hash hash;// 仿函数用于不能取模的值// 已经有这个数,就不用插入了if (Find(kv.first)) {return false;}// 负载因子 == 1时扩容if (this->n == this->_tables.size()) {// size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;size_t newsize = this->GetNextPrime(_tables.size());vector<Node *> newtables(newsize, nullptr);for (auto &cur: this->_tables) {// cur是Node*while (cur) {// 保存下一个Node *next = cur->_next;// 头插到新表size_t hashi = hash(cur->_kv.first) % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}size_t hashi = hash(kv.first) % this->_tables.size();// 头插Node *newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;this->n++;return true;}// 获取哈希表索引最大长度(哈希桶长度)size_t MaxBucketSize() {size_t max = 0;for (int i = 0; i < _tables.size(); ++i) {auto cur = _tables[i];size_t size = 0;while (cur) {++size;cur = cur->_next;}printf("[%d]->%d\n", i, size);if (size > max) {max = size;}if (max == 5121) {printf("%d", i);break;}}return max;}private:vector<Node *> _tables;size_t n = 0;// 存储有效数据的个数
};

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

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

相关文章

Linux Day18 TCP_UDP协议及相关知识

一、网络基础概念 1.1 网络 网络是由若干结点和连接这些结点的链路组成&#xff0c;网络中的结点可以是计算机&#xff0c;交换机、 路由器等设备。 1.2 互联网 把多个网络连接起来就构成了互联网。目前最大的互联网就是因特网。 网络设备有&#xff1a;交换机、路由器、…

图层混合算法(一)

常见混合结果展示 图层混合后变暗 正常模式&#xff08;normal&#xff09; 混合色*不透明度&#xff08;100%-混合色不透明度&#xff09; void layerblend_normal(Mat &base,Mat &blend,Mat &dst,float opacity) {if (base.rows ! blend.rows ||base.cols ! b…

测试C#图像文本识别模块Tesseract的基本用法

微信公众号“dotNET跨平台”的文章《c#实现图片文体提取》&#xff08;参考文献3&#xff09;介绍了C#图像文本识别模块Tesseract&#xff0c;后者是tesseract-ocr&#xff08;参考文献2&#xff09; 的C#封装版本&#xff0c;目前版本为5.2&#xff0c;关于Tesseract的详细介绍…

使用Python+Flask/Moco框架/Fiddler搭建简单的接口Mock服务

一、Mock测试 1、介绍 mock&#xff1a;就是对于一些难以构造的对象&#xff0c;使用虚拟的技术来实现测试的过程mock测试&#xff1a;在测试过程中&#xff0c;对于某些不容易构造或者不容易获取的对象&#xff0c;可以用一个虚拟的对象来代替的测试方法接口mock测试&#x…

多维时序 | MATLAB实现WOA-CNN-BiLSTM-Attention多变量时间序列预测(SE注意力机制)

多维时序 | MATLAB实现WOA-CNN-BiLSTM-Attention多变量时间序列预测&#xff08;SE注意力机制&#xff09; 目录 多维时序 | MATLAB实现WOA-CNN-BiLSTM-Attention多变量时间序列预测&#xff08;SE注意力机制&#xff09;预测效果基本描述模型描述程序设计参考资料 预测效果 基…

stc8H驱动并控制三相无刷电机综合项目技术资料综合篇

stc8H驱动并控制三相无刷电机综合项目技术资料综合篇 🌿相关项目介绍《基于stc8H驱动三相无刷电机开源项目技术专题概要》 🔨停机状态,才能进入设置状态,可以设置调速模式,以及转动方向。 ✨所有的功能基本已经完成调试,目前所想到的功能基本已经都添加和实现。引脚利…

SpringSecurity 认证流程

文章目录 前言认证入口&#xff08;过滤器&#xff09;认证管理器认证器说明默认认证器的实现 总结 前言 通过上文了解SpringSecurity核心组件后&#xff0c;就可以进一步了解其认证的实现流程了。 认证入口&#xff08;过滤器&#xff09; 在SpringSecurity中处理认证逻辑是…

CMU15-445 format\clang-format\clang-tidy 失败

CMU15-445 format\clang-format\clang-tidy 失败 问题修改 问题 -- Setting build type to Debug as none was specified. -- Youre using Clang 14.0.0 CMake Warning at CMakeLists.txt:67 (message):BusTub/main couldnt find clang-format.CMake Warning at CMakeLists.tx…

虚幻4学习笔记(15)读档 和存档 的实现

虚幻4学习笔记 读档存档 B站UP谌嘉诚课程&#xff1a;https://www.bilibili.com/video/BV164411Y732 读档 添加UI蓝图 SaveGame_UMG 添加Scroll Box 修改Scrollbar Thickness滚动条厚度 15 15 勾选 is variable 添加text 读档界面 添加背景模糊 添加UI蓝图 SaveGame_Slot …

Rowset Class

本节介绍 This chapter provides an overview of Rowset class and discusses the following topics: Shortcut considerations. Rowset object declaration. Scope of a Rowset object. Rowset class built-in functions. Rowset class methods. Rowset class propertie…

计算机毕业设计 智慧养老中心管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

golang实现远程控制主机

文章目录 ssh原理使用golang远程下发命令使用golang远程传输文件 ssh原理 说到ssh原理个人觉得解释最全的一张图是这张华为画的 Connection establishment 这一步就是建立tcp连接 version negotiation 这一步是ssh客户端(连接者)和被ssh服务端(连接者)进行协议的交换&#xf…

字符函数和字符串函数(1)

前言 C语言中对字符和字符串的处理很是频繁&#xff0c;但是C语言本身是没有字符串类型的&#xff0c;字符串通常放在 常量字符串 中或者 字符数组 中。 字符串常量 适用于那些对它不做修改的字符串函数. 1.求字符串长度 strlen 1.1 strlen size_t strlen ( const char * s…

Windows安装cuda和cudnn教程最新版(2023年9月)

文章目录 cudacudnn cuda 查看电脑的cuda最高驱动版本&#xff08;适用于N卡电脑-Nvidia&#xff09; winR打开命令行&#xff0c;输入nvidia-smi 右上角cuda -version就是目前支持的最高cuda版本&#xff0c;目前是12.2 nvidia官网下载cuda 下载地址&#xff1a;https://d…

华为NFC设置教程(门禁卡/公交卡/校园卡等)

今天把华为NFC设置教程分享给大家 出门带门禁卡、校园卡、银行卡、身份证……东西又多&#xff0c;携带又麻烦&#xff0c;还容易搞丢&#xff0c;有没有一种方法可以把它们都装下&#xff1f;有&#xff01;只要一部手机&#xff0c;出门不带卡包&#xff0c;各种证件&#x…

010_第一代软件开发(二)

第一代软件开发(二) 文章目录 第一代软件开发(二)项目介绍界面布局功能完善快照功能获取可用串口播放按键提示音 关键字&#xff1a; Qt、 Qml、 QSerialPort、 QPixmap、 QSoundEffect 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;这个项目结合了 QML&#xff…

主打低功耗物联网国产替代,纵行科技ZT1826芯片以速率和灵敏度出圈

在低功耗物联网领域&#xff0c;国产替代的趋势越演越烈。 9月20日&#xff0c;纵行科技在“IOTE 2023深圳物联网通信技术与应用高峰论坛”发表了“自主原创Advanced M-FSK调制技术助力国产替代和泛在物联”的演讲&#xff0c;并推出了ZT1826芯片&#xff0c;以“更低功耗、更…

NetSuite BOM成本查询

这是个23.2的新功能&#xff0c;如题所示是对BOM成本的一个查询工具&#xff0c;是对之前版本那个无用的“Costed Bill of Materials Inquiry”的一次救赎。 其重要的功能是&#xff1a; •基于BOM所使用的版本、工艺路线和成本模板&#xff0c;通过Break Down的方式计算一个装…

超好用的接口自动化框架,lemon-easytest内测版发布,赶紧用起来~

easytest easytest 是一个接口自动化框架。 功能特点&#xff1a; 支持 http 接口测试 支持 json&#xff0c;html,xml 格式的响应断言 支持数据库断言 支持用例标记筛选 支持用例失败重运行 支持多线程 安装 pip install lemon_easytest 快速使用 不需要写任何代码…