yo!这里是STL::unordered系列简单模拟实现
目录
前言
相关概念介绍
哈希概念
哈希冲突与哈希函数
闭散列
框架
核心函数
开散列
框架
核心函数
哈希表(开散列)的修改
迭代器实现
细节修改
unordered系列封装
后记
前言
我们之前了解过map和set知道,map、set的底层结构是红黑树,插入查询等操作效率相对较高,但是当树中的节点非常多时,查询的效率也是很好,我们希望呢,最好进行较少的查询就能找到元素。因此,在c++11中,stl又提供了unordered_map和unordered_set等相关关联式容器,使用方法与map、set基本一样,重点是底层结构不同。从名字也可以看出,unordered系列容器是不做排序的,想想也是,很多查询情况下也是不需要排序的。所以下面让我们看看它们的神奇之处吧!
相关概念介绍
对于undered_map与unordered_set的使用不多赘述,否则偏离本篇文章的标题,使用细节可参考cplusplus.com/reference/
https://cplusplus.com/reference/,也可以通过刷相关题来加深印象。在简单介绍完相关概念之后,我们重点介绍底层的实现并且手把手实现一番。
-
哈希概念
有无这样一种理想的搜索方法,可以不经过任何比较,一次直接从表中得到要搜索的元素? 如果构造一种存储结构,通过某种映射函数使元素的存储位置与元素之间能够建立一一对应的关系,那么在查找时通过该函数可以很快找到该元素。
使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希(散列)表(Hash Table),比如说,有元素集{1,7,14,9,22,68},哈希函数是hashi=Hash(key)%capacity,映射关系如下图
当我们在查找某个元素时,只需要也通过Hash函数计算找到对应位置就能查询到位,不需要进行多次比较。
除以上概念,还一个较为重要的概念就是载荷因子,定义为α=填入到表中的元素个数/散列表的长度,这个概念在扩容机制中会使用到,要重点关注记忆。
-
哈希冲突与哈希函数
在上面的例子当中,当我们再次插入元素24时会发生什么?插入不进去,因为24%10=4的地方已经存放了元素14了。那我们称这种现象叫做哈希冲突(碰撞),即不同关键字通过相同哈希哈数计算出相同的哈希地址,同时把具有不同关键码而具有相同哈希地址的数据元素称为同义词。
引起哈希冲突的一个原因可能是哈希函数设计不够合理,根据数据集合的特点选择正确的哈希函数,哈希函数设计原则:
①哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间;
②哈希函数计算出来的地址能均匀分布在整个空间中;
③哈希函数应该比较简单。
常见哈希函数:
①直接定址法:取关于关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B;优点在于简单、均匀;缺点是需要事先知道关键字的分布情况,适合查找比较小且连续的元素集合;
②除留余数法:设散列表的容量是m,取一个不大于m但最接近或者等于m的质数p作为除数,根据哈希函数Hash(key) = key%p,将key转换成哈希地址,
除了以上两种常用的之外,还有许多不常用的,了解即可,如平方取中法、折叠法、随机数法、数学分析法。
针对于哈希冲突我们得有解决的办法,常见的方法有闭散列和开散列,下面重点介绍解决这两种方法。在stl中实现哈希表使用的就是开散列,之后我们实现时也是用这种方式,但闭散列的方法也相当经典,我们也着重介绍一下。
闭散列
闭散列,也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置,若找不到下一个空位置说明哈希表已经满了,则需要扩容。
找下一个空位置的方法有两种,一种是线性探测法,即从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止;另一种是二次探测法,就是步长呈平方式的向后探测(或者左右探测),比如:
对于线性探测法,当插入24时,计算hashi=4遇到14冲突了,则向后找到hashi=5的空位置存放元素24;对于二次探测法,当再次插入34时,计算hashi=4遇到14冲突了,则计算(hashi+1^2)%10=5遇到24再次冲突,计算(hashi+2^2)%10=8遇到68再次冲突,计算(hashi+3^2)%10=3不冲突,存放到下标为3的位置。插入是如此,查询也是如此,先计算哈希值,遇到冲突了根据解决冲突的方法查询下一个位置,直到遇到空位置说明未查询成功。
思想如上,但实现起来彷佛有点麻烦,比如有两个同义词先后被插入,插入第二个同义词之后,删除第一个插入的同义词,之后查询第二个同义词,那是不是直接遇到空返回查询失败呢?很明显不对,第二个同义词是在的,因此我们在实现时引入一个标识元素状态的state,具体实现如下。
-
框架
我们知道,哈希表的数据结构是顺序表,所以这里使用vector,那里面的元素放什么呢?首先必然要放一个pair结构体来放对应的key和value,除此之外可以看到下方代码实现中还多了一个state属性,这个是为了标识当前下标元素的状态,其中包括,EMPTY表示该下标没有元素,EXIST表示该下标存在元素,DELETE表示该下标的元素已被删除,为什么要标识DELETE呢?
举个例子,根据hash函数找到了对应下标,但是这个下标没有元素,那就一定代表所查找的关键字就不存在了嘛,不一定,有可能当前元素遇到了冲突被放到了其他位置,之后呢当前这个hash值的位置的元素被删除了,那就得标识DELETE,表示当前对应下标位置得元素不存在,但你要根据解决冲突得办法继续向后找,直到找到EMPTY为止。
所以下方的HashData结构体就是哈希表的元素,状态属性用枚举实现;除此之外呢,可以看到HashFunc类模板,这个是解决不同类型关键字如何转化成下标的问题,比如说,一个int类型做关键字,很好理解,将其取模即可,但是遇到string做关键字如何应对呢?那我们就可以通过这个HashFunc类模板实现将string转化成整型的逻辑(比如字符相加或者相乘等等),重点在于要尽可能具有唯一性,如下实现的逻辑得到的整型更加合适(大佬研究所得)。
将想要作为关键字转化成整型的逻辑实现在HashFunc之后,通过类模板参数传进哈希表的类中使用,如下代码中的class Hash=HashFunc(),这里是将此类模板作为了缺省值,之后在insert、find函数中实例化出对象即可使用。
代码:
enum State
{EXIST,DELETE,EMPTY
};template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};template<class T>
struct HashFunc
{size_t operator()(const T& t){return (size_t)t;}
};
//特化
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t sum = 0;for (auto i : s){sum *= 131;sum += i;}return sum;}
};template<class K, class V, class Hash=HashFunc<K>>
class HashTable
{
public://...
private:vector<HashData<K, V>> _table;size_t _size = 0; //存储有效数据的个数
};
-
核心函数
核心函数在这里我们介绍insert插入函数,find查找函数和erase删除函数,其他接口函数都较为简单,不再赘述。
对于insert,首先是去重,即如果表中存在当前key,就直接插入失败,不再插入;其次就是扩容,当达到你想要的载荷因子(表元素/表容量)就选择扩容,这里我们选择使用复用的方法去扩容,即新实例化出一个哈希表对象,遍历旧表元素,复用insert函数将其插入到新表中,之后将旧表和新表的vector调换即可,新表就没用了,函数结束后自动析构掉;最后呢就是关键插入部分代码,通过hash函数映射出对应下标,如果冲突则使用线性探测法(后面介绍二次探测法)向后探测可以插入的位置,即遇到EXIST就继续找,遇到DELETE或EMPTY就插入,注意在向后找的过程中,记得将遍历下标取模,因为遍历到最后还需要回到开头继续找,找到之后设置好state及size即可。二次探测法与线性探测法相似,只不过线性探测法是顺次一个一个向后找,而二次探测法是平方着向后找(比如hashi+0²、hashi+1²、hashi+2²......,也比如hashi+0²、hashi+1²、hashi-1²、hashi+2²、hashi-2²......),实现逻辑很简单,下方代码可参考。
对于find函数,实现逻辑也是很简单,通过hash函数映射出对应下标,遇到EMPTY就查找失败,遇到EXIST或DELETE就向后找,当遇到EXIST时判断是否与被查找key相等,注意查找成功时返回元素地址,失败时返回null。
对于erase函数,相比之下更简单,使用find函数找到对应元素,将其状态改为DELETE及size-1即可,查找失败对应着删除失败。
代码:
bool insert(const pair<K, V>& kv){//去重if (find(kv.first))return false;//载荷因子大于0.7需要扩容if (_table.size() == 0 || 10 * _size / _table.size() >= 7){size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;HashTable<K,V,Hash> newHT;newHT._table.resize(newsize);for (auto& i : _table){if (i._state == EXIST){newHT.insert(i._kv);}}_table.swap(newHT._table);}Hash hash;size_t Hashi = hash(kv.first) % _table.size();//线性探测while (_table[Hashi]._state == EXIST) //元素存在就++,遇到删除或空就插入{Hashi++;Hashi %= _table.size();}_table[Hashi]._kv = kv;_table[Hashi]._state = EXIST;_size++;//二次探测//size_t i = 0;//while (_table[Hashi + i * i]._state == EXIST)//{// i++;// Hashi %= _table.size();//}//_table[Hashi + i * i]._kv = kv;//_table[Hashi + i * i]._state = EXIST;//_size++;return true;}HashData<K, V>* find(const K& key){if (_size == 0 || _table.size() == 0)return nullptr;Hash hash;size_t Hashi = hash(key) % _table.size();while (_table[Hashi]._state != EMPTY){if (_table[Hashi]._state == EXIST && _table[Hashi]._kv.first == key){return &_table[Hashi];}Hashi++;Hashi %= _table.size();}return nullptr;}bool erase(const K& key){HashData<K, V>* ret = find(key);if (!ret)return false;ret->_state = DELETE;--_size; //注意控制哈希表属性sizereturn true;}
开散列
开散列,又叫做链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过单链表链接起来,各链表的头结点存储在哈希表中,因此每一个桶中放的都是发生哈希冲突的元素,比如有集合{1,34,22,65,77,24,71,69,9,0},哈希函数是hashi=Hash(key)%capacity,映射关系如下图:
思路如上,很好理解,并且实现起来不是很麻烦,个人认为开散列的性价比比闭散列要高,至少比它节省空间,下面看看具体实现。
-
框架
首先,哈希桶也是使用vector实现,因为放入的元素是由链表组成,所以vector的元素应当是一个指针变量,这里我们封装一个HBnode结构体,存储key+value组成的pair结构以及指向下一节点的next指针,除了属性之外,还包括节点的构造函数,用来创建节点以及初始化属性,因此,vector中存储的就是HBnode类型的指针。
其次依旧是在哈希表中提到的HashFunc类模板,用以解决不同类型关键字如何转化成下标的问题,介绍如上。
代码:
template<class K, class V>
struct HBnode
{pair<K, V> _kv;HBnode<K, V>* _next;HBnode(const pair<K,V>& kv):_kv(kv),_next(nullptr){}
};template<class T>
struct HashFunc
{size_t operator()(const T& t){return (size_t)t;}
};
//特化
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t sum = 0;for (auto i : s){sum *= 131;sum += i;}return sum;}
};template<class K, class V, class Hash=HashFunc<K>>
class HashBucket
{typedef HBnode<K, V> node;
public://...
private:vector<node*> _buckets;size_t _size = 0;
};
-
核心函数
对于insert函数,思路与哈希表的insert函数实现大体一样。扩容方面,不同于哈希表中存储了多少值,就会占用多少下标位置,哈希桶存储了多少值,与占用下标位置数没有关系,因为元素可以连接在同一个下标位置上,也就是形成一个桶,所以这里我们设置触发扩容的条件是元素个数达到vector容量;在哈希表中的扩容部分我们是使用了复用的方式,但是对于这种节点链表也适合吗?不太行,因为旧表上的节点可以重复利用,也就将其拆卸下来装到新表上,所以我们初始化一个新表HashBucket,遍历旧表,将旧表的节点拆卸下来再通过hash函数映射到新表上,之后交换旧表与新表的vector即可;插入方面就是创建新的节点,主要难点还是在于将其插入到链表中,但因其是旧知识,这里也不多赘述,实现可参考下方代码。
对于find函数,相比之下比较简单,遍历表中的各个链表,找到对应节点,将其pair结构体的地址返回,找不到则返回null。
对于erase函数,不能像哈希表中的erase实现一样,先调用find函数找到对应关键字再删除,注意这里删除是删除一个链表节点,我们知道删除链表节点必须找到当前节点的前一个节点,因此我们需要像find函数一样去遍历vector中的每一个链表,找到所要删除的节点,同时记录当前节点的前一个节点,下面就是链表节点的删除操作,只要着重注意一下头删和中间删的操作的区别,其他都是基操,代码参考下方。
代码:
bool insert(const pair<K, V>& kv){Hash hash;if (find(kv.first))return false;if (_size == _buckets.size()){size_t newsize = _buckets.size() == 0 ? 10 : _buckets.size() * 2;HashBucket newHB;newHB._buckets.resize(newsize);for (size_t i = 0; i < _buckets.size(); i++){node* cur = _buckets[i];while (cur){node* curnext = cur->_next;size_t newHashi = hash(cur->_kv.first) % newHB._buckets.size();if (newHB._buckets[newHashi]){cur->_next = newHB._buckets[newHashi];newHB._buckets[newHashi] = cur;}else{newHB._buckets[newHashi] = cur;}cur = curnext;}}_buckets.swap(newHB._buckets);}size_t Hashi = hash(kv.first) % _buckets.size();node* Node = new node(kv);if (_buckets[Hashi]){Node->_next = _buckets[Hashi];_buckets[Hashi] = Node;}else{_buckets[Hashi] = Node;}_size++;return true;}pair<K, V>* find(const K& key){if (_size == 0 || _buckets.size() == 0)return nullptr;Hash hash;size_t Hashi = hash(key) % _buckets.size();node* cur = _buckets[Hashi];while (cur){if (cur->_kv.first == key)return &cur->_kv;cur = cur->_next;}return nullptr;}bool erase(const K& key){if (_size == 0 || _buckets.size() == 0)return false;Hash hash;size_t Hashi = hash(key) % _buckets.size();if (!_buckets[Hashi])return false;node* prev = _buckets[Hashi];if (prev->_kv.first == key) //头删{_buckets[Hashi] = prev->_next;delete prev;_size--;return true;}node* cur = prev->_next;while (cur){if (cur->_kv.first == key) //中间删{prev->_next = cur->_next;delete cur;_size--;return true;}prev = cur;cur = cur->_next;}return false;}
哈希表(开散列)的修改
-
迭代器实现
首先考虑其成员变量需要包括节点指针以及指向此哈希表的指针,因为在实现迭代器++时,需要找到下一个元素,如果没有当前哈希表的信息,就无法找到下一个元素,所以需要一个指向哈希表类的指针,也是正因为如此,迭代器的实现需要哈希表,但是哈希表的实现也需要迭代器,造成了一个“你中有我,我中有你”的局面,需要使用前置声明的方法打破,因此在迭代器的实现之前前置声明了哈希表。
对于构造函数,定义时需传入一个节点指针和当前哈希表指针用以初始化;对于解引用,得到节点中的数据;对于->,得到节点数据的地址;对于关系运算符,重点是比较迭代器的节点;对于迭代器++操作(这里仅强调前置++,后置++类似,并且迭代器只有++操作,无--操作,因为哈希表的迭代器是单向迭代器),根据当前迭代器节点的下一节点的存在情况进行讨论,若下一节点存在则直接将下一节点赋值给迭代器,若下一节点不存在,则通过哈希表指针遍历到当前桶的下一个桶,将下一个桶的第一个节点赋值给迭代器,若后面没有桶了,则给迭代器节点赋值空,代码参考下方。
代码:
//前置声明
template<class K, class T, class Hash, class KeyOfT>
class HashBucket;template<class K, class T, class Hash, class KeyOfT>
struct __HashIterator
{typedef HBnode<T> node;typedef HashBucket<K, T, Hash, KeyOfT> HB;typedef __HashIterator<K, T, Hash, KeyOfT> Self;node* _node;HB* _hb; //向上找不到HB的声明定义,需要HB前置声明一下//那为什么不把迭代器定义在HB下面,因为HB中也有使用迭代器,是一个“你中有我,我中有你”的关系,需要前置声明打破一下__HashIterator(node* pnode, HB* phb):_node(pnode), _hb(phb){}T& operator*(){return _node->_data;}T* operator->(){return &(_node->_data);}bool operator==(const Self& iterator) const{return _node == iterator._node;}bool operator!=(const Self& iterator) const{return _node != iterator._node;}Self& operator++(){if (_node->_next){_node = _node->_next;}else{Hash hash;KeyOfT kot;size_t hashi = hash(kot(_node->_data)) % _hb->_buckets.size();hashi++;while (hashi < _hb->_buckets.size() && !_hb->_buckets[hashi]){hashi++;}if (hashi == _hb->_buckets.size()){_node = nullptr;}else{_node = _hb->_buckets[hashi];}}return *this;}
};
-
细节修改
首先我们先加上对于迭代器最基本的begin()、end()。其中begin()是返回哈希桶中的第一个桶的第一个节点所构造的迭代器,实现逻辑很简单,即从头遍历表,找到第一个桶(很多地方需要用到哈希表类的成员属性_buckets,所以将迭代器类设置成哈希表类的友元,用以访问哈希表类的_buckets),end()是返回空迭代器,值得注意的是,构造迭代器时传入的哈希表指针可以使用this指针。
其次就是加上迭代器后部分核心函数的修改,
①insert函数返回值变成了pair<iterator,bool>;
②find函数返回值变成了iterator;
③加入了哈希表的析构函数,即将所有桶的节点给释放掉,因为自带的析构函数只会析构掉vector,不会释放内部元素指向的节点。
代码:
template<class K, class T, class Hash, class KeyOfT>
class HashBucket
{typedef HBnode<T> node;template<class K, class T, class Hash, class KeyOfT>friend struct __HashIterator;
public:typedef __HashIterator<K, T, Hash, KeyOfT> iterator;iterator begin(){for (size_t i = 0; i < _buckets.size(); i++){if (_buckets[i])return iterator(_buckets[i], this);}return end();}iterator end(){return iterator(nullptr, this);}~HashBucket(){for (size_t i = 0; i < _buckets.size(); i++){node* cur = _buckets[i];while (cur){node* next = cur->_next;delete cur;_size--;cur = next;}_buckets[i] = nullptr;}}pair<iterator,bool> insert(const T& data){Hash hash;KeyOfT kot;iterator ret = find(kot(data));if (ret != end())return make_pair(ret, false);if (_size == _buckets.size()){size_t newsize = _buckets.size() == 0 ? 10 : _buckets.size() * 2;HashBucket newHB;newHB._buckets.resize(newsize);for (size_t i = 0; i < _buckets.size(); i++){node* cur = _buckets[i];while (cur){node* curnext = cur->_next;size_t newHashi = hash(kot(cur->_data)) % newHB._buckets.size();if (newHB._buckets[newHashi]){cur->_next = newHB._buckets[newHashi];newHB._buckets[newHashi] = cur;}else{newHB._buckets[newHashi] = cur;}cur = curnext;}}_buckets.swap(newHB._buckets);}size_t Hashi = hash(kot(data)) % _buckets.size();node* Node = new node(data);if (_buckets[Hashi]){Node->_next = _buckets[Hashi];_buckets[Hashi] = Node;}else{_buckets[Hashi] = Node;}_size++;return make_pair(iterator(Node, this), true);}iterator find(const K& key){if (_size == 0 || _buckets.size() == 0)return end();Hash hash;KeyOfT kot;size_t Hashi = hash(key) % _buckets.size();node* cur = _buckets[Hashi];while (cur){if (kot(cur->_data) == key)return iterator(cur, this);cur = cur->_next;}return end();}bool erase(const K& key){if (_size == 0 || _buckets.size() == 0)return false;Hash hash;size_t Hashi = hash(key) % _buckets.size();if (!_buckets[Hashi])return false;node* prev = _buckets[Hashi];if (kot(prev->_data) == key) //头删{_buckets[Hashi] = prev->_next;delete prev;_size--;return true;}node* cur = prev->_next;while (cur){if (kot(cur->_data) == key) //中间删{prev->_next = cur->_next;delete cur;_size--;return true;}prev = cur;cur = cur->_next;}return false;}
private:vector<node*> _buckets;size_t _size = 0;
};
unordered系列封装
这里介绍unordered_map封装,unordered_set封装情况一致,代码参考如下。
首先考虑成员属性包括一个哈希表实例化的一个对象,注意key依旧是key,但value却是一个pair结构体,unordered_set也是一样,key依旧是key,但value确实一个key,这是因为统一成使用kv模型中的v来存储值,而k仅用来索引,为什么要把key单独拿出来作为一个类模板的参数呢,因为一些地方还是需要用到key的(比如[]操作符需要传进一个key类型的一个对象,是需要用到key类型的);进而也需要一个KeyOfT的仿函数来返回key,unordered_map是传出pair结构体的first,而unordered_set仅是为了保持一致,传出key即可。
其次,对于begin()、end()、insert()是调用了成员属性哈希表的成员函数,无需过多封装;[]操作符功能是传进key返回value,若表中不存在此key,则插入,对应value使用匿名对象进行默认构造,若存在则直接返回对象value,代码参考如下。
代码(unordered_map):
template<class K, class V, class Hash = HashFunc<K>>
class Unordered_map
{struct mapKeyOfT{const K& operator()(const pair<K,V>& kv){return kv.first;}};
public:typedef typename HashBucket<K, pair<K, V>, Hash, mapKeyOfT>::iterator iterator;iterator begin(){return _HB.begin();}iterator end(){return _HB.end();}pair<iterator,bool> insert(const pair<K,V>& data){return _HB.insert(data);}V& operator[](const K& key){pair<iterator, bool> ret = _HB.insert(make_pair(key, V()));return ret.first->second;}
private:HashBucket<K, pair<K,V>, Hash, mapKeyOfT> _HB;
};
代码(unordered_set):
template<class K, class Hash = HashFunc<K>>
class Unordered_set
{struct setKeyOfT{const K& operator()(const K& key){return key;}};
public:typedef typename HashBucket<K, K, Hash, setKeyOfT>::iterator iterator;iterator begin(){return _HB.begin();}iterator end(){return _HB.end();}pair<iterator,bool> insert(const K& data){return _HB.insert(data);}
private:HashBucket<K, K, Hash, setKeyOfT> _HB;
};
后记
unordered系列容器是c++11提出的,完美地弥补了map与set在多元素情况下查询效率慢的缺点,其使用与它们并无太大的差别,但实现难度上个人认为比set和map底层的红黑树实现要容易许多。本篇重点讲解了底层实现,其使用方法也不可小视,在一些笔试题、oj题上都需要对这些容器的熟练使用,同时两手抓才能将知识点学的扎实,好了,unordered系列容器介绍就是这样,拜拜!
相关文章:

yo!这里是STL::unordered系列简单模拟实现
目录 前言 相关概念介绍 哈希概念 哈希冲突与哈希函数 闭散列 框架 核心函数 开散列 框架 核心函数 哈希表(开散列)的修改 迭代器实现 细节修改 unordered系列封装 后记 前言 我们之前了解过map和set知道,map、set的底层结构是…...

基础课25——业务流程分析
1.流程的定义&作用 业务流程是企业中一系列创造价值的活动的组合,它是企业运营的基础,也是企业提高效率、优化资源配置的重要手段。通过优化业务流程,企业可以更好地满足客户需求,提高客户满意度,同时也可以提高自…...

快速实现一个企业级域名 SSL 证书有效期监控巡检系统
Why 现在对于企业来说,HTTPS 已经不是可选项,已经成为一个必选项。HTTPS 协议采用 SSL 协议,采用公开密钥的技术,提供了一套 TCP/IP 传输层数据加密的机制。SSL 证书是一种遵守 SSL 协议的服务器数字证书,一般是由权威…...

[SSD综述 1.5] SSD 主控和固件核心功能详解(万字)
依公知及经验整理,原创保护,禁止转载。 1. 主控概述1.1 主控作用 2. 主控的硬件功能和实现2.1 主控处理器2.2 闪存、主机接口2.3 主控纠错2.4 断电保护 3 固件功能3.1 FTL3.2 预留空间(Over-provisioning)3.3 Trim3.4 写入放大(Write amplification)3.5 …...

Mybatis-Plus前后端分离多表联查模糊查询分页
数据准备 数据库配置: /*Navicat Premium Data TransferSource Server : localhost_3306Source Server Type : MySQLSource Server Version : 80100 (8.1.0)Source Host : localhost:3306Source Schema : test01Target Server Type : MySQLT…...

【Ruoyi管理后台】用户登录强制修改密码
近期有个需求,就是需要调整Ruoyi管理后台:用户如果三个月(长时间)未修改过密码,需要在登录时强制修改密码,否则不能登录系统。 一、后端项目调整 从需求来看,我们需要在用户表增加一个字段,用于标记用户最…...

计算机网络基础知识1
1、tcp三次握手? SYN,标志位,用于建立TCP连接的握手过程中的标志位。 ACK,确认位,用于说明整个包是确认报文。 TCP/IP协议是传输层的一个面向连接提供可靠安全的传输协议。第一次握手有客户端发起,客户端向…...

人机交互中的多/变尺度态势感知
人机交互是指在人与计算机之间进行信息交换和任务完成的过程中,通过各种界面和交互方式来实现人机之间的有效沟通和协作。多尺度上下文是人机交互中一个重要的概念,它指的是在不同层次或不同尺度的信息之间建立联系,以便更好地理解和处理信息…...

命名管道原理(和匿名管道的对比),mkfifo(命令行,函数),命名管道模拟实现代码+与多个子进程通信代码
目录 命名管道 引入 原理 和匿名管道的对比 使用 -- mkfifo 命令行指令 创建 文件类型p 使用 函数 函数原型 模拟实现 头文件 客户端代码 服务端代码 运行情况 模拟实现 -- 与多个子进程 介绍 服务端代码: 运行情况 命名管道 引入 匿名管道只能用于父子进程…...

pytest全局变量的使用
这里重新阐述下PageObject设计模式: PageObject设计模式是selenium自动化最成熟,最受欢迎的一种模式,这里用pytest同样适用 这里直接提供代码: 全局变量 conftest.py """ conftest.py 全局变量,主要实…...

FreeRTOS源码阅读笔记2--list.c
list.c中主要完成列表数据结构的操作,有列表和列表项的初始化、列表的插入和移除。 2.1列表初始化vListInitialise() 2.1.1函数原型 void vListInitialise( List_t * const pxList ) pxList:列表指针,指向要初始化的列表。 2.1.2函数框架…...

杂货铺 | citespace的使用
安装教程 【CiteSpace保姆级教程1】文献综述怎么写? 📚数据下载 1. 新建文件夹 2. 数据下载 知网高级检索 数据选中导出 :一次500 导出后重命名为download_xxx.txt,放到input文件里 3. 数据转换 把output里的数据复制到data里…...

C++ 静态成员变量初始化规则
每一天一个小trick!! 为什么静态成员不能在类内初始化? 在C中,类的静态成员(static member)必须在类内声明,在类外初始化,像下面这样。 class A { private: static int count …...

Docker安装、卸载,以及各种操作
docker是一个软件,是一个运行与linux和windows上的软件,用于创建、管理和编排容器;docker平台就是一个软件集装箱化平台,是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中…...

深入理解 C 语言的内存管理
文章目录 引言内存管理的重要性C语言内存布局C语言内存管理堆和栈内存的区别和用途内存分配和释放的过程C语言动态内存分配的概念和原因malloc()、calloc() 和 realloc() 等函数的使用悬挂指针和野指针内存泄漏和如何避免结论 引言 C语言是充满力量且灵活的编程语言࿰…...

利用Caddy实现http反向代理
利用Caddy实现http反向代理 1 Caddy是什么 Caddy是一个开源的,使用Golang编写的,支持HTTP/2的Web服务端。它的一个显著特征就是默认启用HTTPS。 和nginx类似。 2 多个后端服务 假如现在有3个后端http服务:分别在启动在 app1 http://10…...

【Qt之QVariant】使用
介绍 QVariant类类似于最常见的Qt数据类型的联合。由于C禁止联合类型包括具有非默认构造函数或析构函数的类型,大多数有趣的Qt类不能在联合中使用。如果没有QVariant,则QObject::property()和数据库操作等将会受到影响。 QVariant对象同时持有一个单一…...

xv6实验课程--xv6的写时复制fork(2023)
7. xv6实验课程--xv6的写时拷贝(COW)(2021) 7. xv6实验课程--xv6懒惰分页分配(lazy)(2020) 本文来源: https://mp.weixin.qq.com/s/XJkhjrlP232ZDsRyXd0oHQ 已完成的实验代码可以从下列网站获取: git clone https://gitee.com/lhwhit196…...

在Windows或Mac上安装并运行LLAMA2
LLAMA2在不同系统上运行的结果 LLAMA2 在windows 上运行的结果 LLAMA2 在Mac上运行的结果 安装Llama2的不同方法 方法一: 编译 llama.cpp 克隆 llama.cpp git clone https://github.com/ggerganov/llama.cpp.git 通过conda 创建或者venv. 下面是通过conda 创建…...

Spring底层原理学习笔记--第七讲--(初始化与销毁)
初始化与销毁 Spring提供了多种初始化和销毁手段它们的执行顺序 A07Application.java package com.lucifer.itheima.a07;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springfram…...

基于斑马算法的无人机航迹规划-附代码
基于斑马算法的无人机航迹规划 文章目录 基于斑马算法的无人机航迹规划1.斑马搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要:本文主要介绍利用斑马算法来优化无人机航迹规划。 1.斑马搜索算法 …...

干货 | 接口自动化测试分层设计与实践总结
接口测试三要素: 参数构造 发起请求,获取响应 校验结果 一、原始状态 当我们的用例没有进行分层设计的时候,只能算是一个“苗条式”的脚本。以一个后台创建商品活动的场景为例,大概流程是这样的(默认已经是登录状态下)&#…...

【Linux】服务器与磁盘补充知识,硬raid操作指南
服务器硬件 cpu 主板 内存 硬盘 网卡 电源 raid卡 风扇 远程管理卡 1.硬盘尺寸: 目前生产环境中主流的两种类型硬盘 3.5寸 和2.5寸硬盘 2.5寸硬盘可以通过使用硬盘托架后适用于3.5寸硬盘的服务器 但是3.5寸没法转换成2.5寸 2.如何在服务器上制作raid 华为服务器为例子做…...

【java】实现自定义注解校验——方法二
自定义注解校验的实现步骤: 1.创建注解类,编写校验注解,即类似NotEmpty注解 2.编写自定义校验的逻辑实体类,编写具体的校验逻辑。(这个类可以实现ConstraintValidator这个接口,让注解用来校验) 3.开启使用自定义注解进…...

算法通关村第六关|白银|二叉树的层次遍历【持续更新】
1.二叉树基本的层序遍历 仅仅遍历并输出全部元素。 List<Integer> simpleLevelOrder(TreeNode root) {if (root null) {return new ArrayList<Integer>();}List<Integer> res new ArrayList<Integer>();LinkedList<TreeNode> queue new Lin…...

vue中通过js控制scss变量
<!--* Description:* Author: 李大玄* Date: 2022-07-28 20:34:43* FilePath: /web-framework-demo/src/views/layout.vue* LastEditors: 李大玄* LastEditTime: 2022-11-01 09:25:31 --> <template><div height"100%" class"b"><inp…...

深度学习理论知识入门【EM算法、VAE算法、GAN算法】和【RBM算法、MCMC算法、HMC算法】
目录 深度学习理论知识入门首先,让我们了解第一个流程:现在,让我们看看第二个流程: EM算法GMM(高斯混合模型) 深度学习理论知识入门 首先,让我们了解第一个流程: EM(Exp…...

Java8实战-总结47
Java8实战-总结47 CompletableFuture:组合式异步编程让代码免受阻塞之苦使用定制的执行器 对多个异步任务进行流水线操作 CompletableFuture:组合式异步编程 让代码免受阻塞之苦 使用定制的执行器 就这个主题而言,明智的选择似乎是创建一个…...

功能: 在web应用程序中、读取文件
通过使用文件 API,web 内容可以要求用户选择本地文件,然后读取这些文件的内容。这种选择可以通过使用 HTML <input type"file"> 元素或通过拖放来完成。 1.通过 click() 方法使用隐藏的文件 input 元素 你可以隐藏公认难看的文件 <…...

TDD、BDD、ATDD以及SBE的概念和区别
在软件开发或是软件测试中会遇到以下这些词:TDD 、BDD 、ATDD以及SBE,这些词代表什么意思呢? 它们之间有什么关系吗? TDD 、BDD 、ATDD以及SBE的基本概念 TDD:(Test Driven Development)是一种…...