【数据结构】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.xdnf.cn/news/141051.html

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

相关文章

Qt创建线程(线程池)

1.线程池可以创建线程统一的管理线程&#xff08;统一创建、释放线程&#xff09; 2.使用线程池方法实现点击开始按钮生成10000个随机数&#xff0c;然后分别使用冒泡排序和快速排序排序这10000个随机数&#xff0c;最后在窗口显示排序后的数字&#xff1a; mainwindow.h文件…

FPGA的DQPSK调制解调Verilog

名称&#xff1a;DQPSK调制解调 软件&#xff1a;Quartus 语言&#xff1a;Verilog 要求&#xff1a; 使用Verilog语言进行DQPSK调制和解调&#xff0c;并进行仿真 代码下载&#xff1a;DQPSK调制解调verilog&#xff0c;quartus_Verilog/VHDL资源下载 代码网&#xff1a;h…

Vector Art - 矢量艺术

什么是矢量艺术&#xff1f; 矢量图形允许创意人员构建高质量的艺术作品&#xff0c;具有干净的线条和形状&#xff0c;可以缩放到任何大小。探索这种文件格式如何为各种规模的项目提供创造性的机会。 什么是矢量艺术作品? 矢量艺术是由矢量图形组成的艺术。这些图形是基于…

【音视频】ffplay源码解析-PacketQueue队列

包队列架构位置 对应结构体源码 MyAVPacketList typedef struct MyAVPacketList {AVPacket pkt; //解封装后的数据struct MyAVPacketList *next; //下一个节点int serial; //播放序列 } MyAVPacketList;PacketQueue typedef struct PacketQueue {MyAVPacketList …

快递发货小程序商城的效果是什么

商家搭建小程序商城后&#xff0c;客户交易可以通过到店自提、同城配送、快递发货的方式满足不同场景不同客户购物。 本地客户难以拓展&#xff0c;三公里范围内流量有限&#xff0c;外地无疑是商家拓展市场、客户的绝佳选择&#xff0c;传统电话、微信联系难以信任及选择&…

问题:conda删除虚拟环境,报错no package names supplied

用conda 用 conda remove -n ScratchDet_20200114 删除虚拟 环境ScratchDet_20200114时报错 conda remove -n ScratchDet_20200114CondaValueError: no package names supplied,try "conda remove -h" for more details 解决方法&#xff0c;用下面的命令 conda env…

FPGA的BPSK调制verilog

名称&#xff1a;BPSK调制verilog 软件&#xff1a;Quartus 语言&#xff1a;Verilog 要求&#xff1a; 一、设计说明 BPSK调制广泛应用于卫星通信、移动通信等领域。本题目要求设计一个基于直接数字频率合成技术的BPSK调制器&#xff0c;实现对输入周期数字比特流的BPSK调…

LeetCode 周赛上分之旅 #47 前后缀分解结合单调栈的贡献问题

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架&#xff0c;你的思考越抽象&#xff0c;它能覆盖的问题域就越广&#xff0c;理解难度…

【深度学习实验】前馈神经网络(一):使用PyTorch构建神经网络的基本步骤

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入库 1. 定义x,w,b 2. 计算净活性值z 3. 实例化线性层并进行前向传播 4. 打印结果 5. 代码整合 一、实验介绍 本实验使用了PyTorch库来构建和操作神经网络模型&#xff0c;主要是关…

ExcelServer EXCEL服务器使用- 用户、角色权限配置

Excel文件服务器搭建 搭建Excel服务器 1、登录 默认 用户名 Admin 密码 3 2、角色管理 添加修改角色 角色配置在 系统管理->角色.fexm文件夹下 可以像修改excel文件一样 修改角色 3、用户管理 添加修改用户 用户的修改在 系统管理->用户.fexm 可以像excel一样编辑用户…

人工智能轨道交通行业周刊-第61期(2023.9.18-9.24)

本期关键词&#xff1a;焊线机器人、智能综合运维管理系统、信号平面图、铁路部门架构、书生浦语大模型 1 整理涉及公众号名单 1.1 行业类 RT轨道交通人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟VSTR铁路与城市轨道交通…

【SpringBoot】-SpringBoot配置文件

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【Framework】 主要内容&#xff1a;.properties 配置文件和 .yml 配置文件中 配置信息的设置和获取。关于IDEA乱码的解决。.yml 配置文件的 方式语法分析和演示。 .yml配置文件 …

中秋国庆内卷之我爱学习C++

文章目录 前言Ⅰ. 内联函数0x00 内联函数和宏的比较0x01 内联函数的概念0x02 内联函数的特性 Ⅱ. auto&#xff08;C 11)0x00 auto的概念0x01 auto的用途 Ⅲ. 范围for循环(C11)0x00 基本用法0x01 范围for循环(C11)的使用条件 Ⅳ. 指针空值nullptr(C11)0x00 概念 前言 亲爱的夏…

Linux指令(ls、pwd、cd、touch、mkdir、rm)

whoami who pwd ls ls -l clearls指令 ls ls -l ls -a :显示当前目录下的隐藏文件&#xff08;隐藏文件以.开头&#xff09;ls -a -l 和 ls -l -a 和 ls -la 和 ls -al &#xff08;等价于ll&#xff09; pwd命令 显示用户当前所在的目录 cd指令 mkdir code &#xff08;创建…

Spring Boot的新篇章:探索2.0版的创新功能

文章目录 引言1. Spring Boot 2.0的响应式编程2. 自动配置的改进3. Spring Boot 2.0的嵌入式Web服务器4. Spring Boot 2.0的Actuator端点5. Spring Boot 2.0的Spring Data改进6. Spring Boot 2.0的安全性增强7. Spring Boot 2.0的监控和追踪8. Spring Boot 2.0的测试改进结论 &…

【Verilog 教程】4.4Verilog 语句块

关键词&#xff1a;顺序块&#xff0c;并行块&#xff0c;嵌套块&#xff0c;命名块&#xff0c;disable Verilog 语句块提供了将两条或更多条语句组成语法结构上相当于一条一句的机制。主要包括两种类型&#xff1a;顺序块和并行块。 顺序块 顺序块用关键字 begin 和 end 来表…

AIGC(生成式AI)试用 6 -- 桌面小程序

生成式AI&#xff0c;别人用来写作&#xff0c;我先用来写个桌面小程序。 桌面小程序&#xff1a;计算器 需求 Python开发图形界面&#xff0c;标题&#xff1a;计算器 - * / 基本运算计算范围&#xff1a;-999999999 ~ 999999999** 乘方计算&#xff08;例&#xff0c;2*…

Android Kotlin 基础详解

1,基础语法 1.1 可变变量与不可变变量 可以多次赋值的变量是可变变量&#xff0c;用关键字var表示&#xff1a; var <标识符> : <类型> <初始化值> 注意&#xff0c;在kotlin中成员变量不会赋默认值&#xff0c;不像java一样&#xff0c;必须手动添加默…

Mybatis-MyBatis的缓存

Mybatis-MyBatis的缓存 一、MyBatis的一级缓存二、MyBatis的二级缓存二级缓存的相关配置 三、MyBatis缓存查询的顺序 一、MyBatis的一级缓存 一级缓存是SqlSession级别的&#xff0c;通过同一个SqlSession查询的数据会被缓存&#xff0c;下次查询相同的数据&#xff0c;就 会从…

【已解决】qt死活不响应鼠标移动到按钮事件

本博文源于笔者正在研究的内容&#xff0c;这个问题大概捣鼓了一个下午&#xff0c;问题是这样子&#xff1a;我有一个按钮&#xff0c;我应用程序运行时&#xff0c;我鼠标放到按钮上&#xff0c;按钮就会被填充图标。怀揣着这样一个想法&#xff0c;我搜啊搜&#xff0c;整啊…