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/icon-default.png?t=N7T8https://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系列容器介绍就是这样,拜拜!


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

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

相关文章

电脑软件:推荐一款电脑多屏幕管理工具DisplayFusion

下载https://download.csdn.net/download/mo3408/88514558 一、软件简介 DisplayFusion是一款多屏幕管理工具&#xff0c;它可以让用户更轻松地管理连接到同一台计算机上的多个显示器。 二、软件功能 2.1 多个任务栏 通过在每个显示器上显示任务栏&#xff0c;让您的窗口管理更…

MATLAB绘图中文显示为方框

MATLAB绘图中文显示为方框 MATLAB显示英文和字母没有问题&#xff0c;但是当显示中文时会显示乱码&#xff0c;中文显示为方框&#xff0c;如下图&#xff1a; 可以在绘图命令中添加如下代码&#xff1a; set(gca,Fontname,Monospaced); 例如&#xff1a; % 滤波器系数%低通…

16 DNS协议详解

1、DNS的由来 很难记住网站的 IP 地址&#xff0c;因而也需要一个地 址簿&#xff0c;就是DNS 服务器。DNS 在日常生活中非常重要。每个人上网&#xff0c;都需要访问它&#xff0c;因此一旦DNS出现故障&#xff0c;是非常可怕的。因而&#xff0c;DNS 服务器&#xff0c;一定…

装修服务预约小程序的内容如何

大小装修不断&#xff0c;市场中大小品牌也比较多&#xff0c;对需求客户来说&#xff0c;可以线下咨询也可以线上寻找品牌&#xff0c;总是可以找到满意的服务公司&#xff0c;而对装修公司来说如今线下流量匮乏&#xff0c;很多东西也难以通过线下方式承载&#xff0c;更需要…

【Java 进阶篇】Java Filter 快速入门

欢迎来到这篇有关 Java Filter 的快速入门指南&#xff01;如果你是一名 Java 开发者或者正在学习 Java Web 开发&#xff0c;Filter 是一个强大的工具&#xff0c;可以帮助你管理和控制 Web 应用程序中的请求和响应。本文将向你解释 Filter 的基本概念&#xff0c;如何创建和配…

84 柱状图中的最大的矩形(单调栈)

题目 柱状图中的最大的矩形 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 示例 1: 输入&#xff1a;heights [2,1,5,6,2,3] 输出&#xff1a;10 …

nginx图片资源管理转发

目标: 服务器上面 /home/images 里面作为文件资源管理器 代码: server {listen 80;server_name hello.world.cn;#apple-app和接口的关联文件location ~.*(images/miniapp)*\.(gif|jpg|jpeg|png)$ {root /home/;try_files $uri $uri/ 404;add_header Cache-Control &q…

(六)库存超卖案例实战——使用mysql分布式锁解决“超卖”问题

前言 本节内容是关于使用分布式锁解决并发访问“超卖”问题的最终篇&#xff0c;在前面的章节中我们介绍了使用mysql的行锁、乐观锁、悲观锁解决并发访问导致的超卖问题&#xff0c;存在的问题是行锁、乐观锁、悲观锁不太灵活&#xff0c;需要和具体的业务耦合到一起&#xff…

再获5G RedCap能力认证!宏电5G RedCap工业智能网关通过中国联通5G物联网OPENLAB开放实验室测试验证

​近日&#xff0c;中国联通5G物联网OPENLAB开放实验室携手宏电股份完成5G RedCap工业智能网关端到端的测试验证&#xff0c;并颁发OPENLAB实验室面向RedCap终端的认证证书&#xff0c;为RedCap产业规模推广、全行业赋能打下坚实基础。 中国联通5G物联网OPENLAB开放实验室是中国…

掌动智能:云可观测性的主要特点及应用场景

云全景可观测性安全平台是一个跨架构、跨平台的可观测性方案&#xff0c;实现对云环境下的细粒度数据可视化&#xff0c;满足安全部门对云内部安全领域的多场景诉求&#xff0c;包括敏感数据动态监管、云网攻击回溯分析、攻击横移风险监控、云异常流量分析。本文将介绍掌动智能…

kubernetes集群编排(8)

k8s资源监控 资源限制 上传镜像 [rootk8s2 limit]# vim limit.yaml apiVersion: v1 kind: Pod metadata:name: memory-demo spec:containers:- name: memory-demoimage: stressargs:- --vm- "1"- --vm-bytes- 200Mresources:requests:memory: 50Milimits:memory: 100…

04-react基础知识-路由

一、react路由环境安装 使用指令&#xff1a;npm i --save react-router-dom type/react-router-dom进行react路由环境安装 二、引入路由 在main.jsx文件中引入该语句&#xff1a; import { createBrowserRouter, RouterProvider } from react-router-dom 定义一个变量rou…

【Java笔试强训】Day9(CM72 另类加法、HJ91 走方格的方案数)

CM72 另类加法 链接&#xff1a;另类加法 题目&#xff1a; 给定两个int A和B。编写一个函数返回AB的值&#xff0c;但不得使用或其他算数运算符。 题目分析&#xff1a; 代码实现&#xff1a; package Day9;public class Day9_1 {public int addAB(int A, int B) {// wr…

「Verilog学习笔记」使用子模块实现三输入数的大小比较

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 分析 题目要求编写子模块实现两个输入数的大小比较并输出较小值&#xff0c;可以使用if-else语句实现。同时要求在主模块中实现三个输入数值的大小比较&#xff0c;假设三个…

【配置】如何在打包Spring Boot项目时按需使用日常、测试、预发、正式环境的配置文件

文章目录 前言1. 创建5个配置文件2. 在pom.xml文件中如下配置3. 在application.properties中加入环境变量 前言 在我们开发项目的时候&#xff0c;一般有四套环境&#xff1a;日常、测试、预发、正式。日常环境作为我们开发环境&#xff1b;测试环境给测试同学测试功能&#x…

​软考-高级-信息系统项目管理师教程 第四版【第21章-项目管理科学基础-思维导图】​

软考-高级-信息系统项目管理师教程 第四版【第21章-项目管理科学基础-思维导图】 课本里章节里所有蓝色字体的思维导图

【elasticsearch+kibana基于windows docker安装】

创建网络&#xff1a;es和kibana容器互联 docker network create es-net加载镜像 docker pull elasticsearch:7.12.1运行 docker run -d --name es -p 9200:9200 -p 9300:9300 -e "discovery.typesingle-node" -e ES_JAVA_OPTS"-Xms512m -Xmx512m" -v $…

uni-app基于vue实现商城小程序

一、前言 参考“网易严选”小程序 项目采用传统vue项目结构&#xff0c;即uni-app打包和运行成小程序&#xff0c;使用HBuilder开发工具开发项目&#xff0c;通过运行启动“微信开发者工具”完成项目启动。 二、功能效果图 1.首页 2.分类 3.活动 4.我的 5.商品详情 6.购物车…

React中组件之间如何通信?

一、是什么 我们将组件间通信可以拆分为两个词&#xff1a; 组件通信 回顾Vue系列的文章&#xff0c;组件是vue中最强大的功能之一&#xff0c;同样组件化是React的核心思想 相比vue&#xff0c;React的组件更加灵活和多样&#xff0c;按照不同的方式可以分成很多类型的组件…

第五届泰迪杯数据分析技能赛B题源码图片分享

需要B题源码以及第六届带队”指导“请私信本人&#xff0c;团队包含技能赛双一等&#xff0c;数学建模省一&#xff0c;泰迪杯挖掘国一&#xff0c;研究生队友。 去年一等作品可视化图如下&#xff0c;私信获取源码