C++之哈希表、哈希桶的实现

哈希表、哈希桶的实现

  • 哈希概念
  • 哈希冲突
  • 哈希函数
  • 哈希冲突解决
    • 闭散列
    • 哈希表闭散列实现
      • 哈希表的结构
      • 哈希表的插入
      • 哈希表的查找
      • 哈希表的删除
    • 开散列
      • 开散列概念
      • 哈希表的结构
      • 哈希表的插入
      • 哈希表的查找
      • 哈希表的删除

哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放;
  • 搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表。

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述

哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

例如此时需要插入一个44,就会出现下面的问题,44与4的位置是冲突的,这就是哈希冲突问题。
在这里插入图片描述

哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

哈希函数设计原则

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见的哈希函数

  1. 直接定址法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况
  2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
  3. 平方取中法
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
    再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址;
    平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。
  4. 折叠法
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
    折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
  5. 随机数法
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
    通常应用于关键字长度不等时采用此法
  6. 数学分析法
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现,可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。

我们在此主要了解直接定址法除留余数法

哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列开散列

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的"下一个"空位置中去。

  1. 线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

Hi = (H0 + i)%m ( i = 1 , 2 , 3 , . . . )
H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过线性探测后得到的存放位置。
m:表的大小

比如在上面的场景中,现在需要插入元素44,先通过哈希函数计算哈希地址,哈希地址为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
在这里插入图片描述
通过观察可以发现,在插入元素的时候,通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

我们会发现,数据越多,产生哈希冲突的概率就越高,进行查找的效率也就越低,所以哈希表中引入了负载因子的概念:

负载因子 = 表中有效数据个数 / 空间的大小

  • 负载因子越大,产生哈希冲突的概率也就越大,增删查改效率也就越低,空间利用率就越高;
  • 负载因子越小,产生哈希冲突的概率也就越小,增删查改效率也就越高,空间利用率就越低。

对于闭散列(开放定址法)来说,负载因子是特别重要的因素,一般控制在0.7~0.8以下,超过0.8会导致在查表时CPU缓存不命中(cache missing)按照指数曲线上升。
因此,一些采用开放定址法的hash库,如JAVA的系统库限制了负载因子为0.75,当超过该值时,会对哈希表进行增容。

比如我们此时将容量扩大至20,我们就会发现,哈希冲突的概率明显减少:
在这里插入图片描述
我们要注意删除时哈希表中的元素是不能随便进行删除的,因为一个元素的删除可能会影响另一个元素的查找,例如我们将4这个元素删除掉,后就会影响到44的查找,因此线性探测采用标记的伪删除法来删除一个元素。

哈希表闭散列实现

哈希表的结构

为了方便我们对哈希表中数据进行删除与查找,我们可以使用枚举将每个位置的状态标识出来:

enum State
{EMPTY,   //(未存储数据的空位置)EXIST,   //(已存储数据)DELETE   //(数据被删除)
};

当我们对某一数据进行删除以后,我们将其状态标识为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;//哈希表size_t n = 0;//哈希表中有效元素个数
};

哈希表的插入

插入主要分为以下步骤:

  1. 判断哈希表中是否存在该键值对,如果存在,就返回false;
  2. 判断是否需要对哈希表大小进行调整,如果哈希表大小为0或者负载因子过大都需要进行扩容;
  3. 插入键值对;
  4. 哈希表中有效元素+1;

我们需要注意的是,扩容以后,并不是简单的将旧表的数据给挪到新表当中去,而是在新表中重新计算所占位置,然后再依次进行插入,插入具体步骤就是:

  1. 通过哈希函数计算出相应的哈希地址;
  2. 判断该位置状态是否是DELET或者EMPTY;
  3. 将键值对进行插入,改变当前位置状态为EXIST;
bool Insert(const pair<K, V>& kv)
{//如果此处键值对已经存在,就返回falseif (Find(kv.first)){return false;}//判断是需要进行扩容if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7){size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();//创建一个新的哈希表HashTable newHT;newHT._tables.resize(newSize);//将旧表数据重新计算哈希地址插入到新表中for (auto e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}//将旧表数据与新表数据进行交换_tables.swap(newHT._tables);}//计算哈希地址size_t hashi = kv.first % _tables.size();//寻找状态为DELETE和EMPTY位置进行插入while (_tables[hashi]._state == EXIST){hashi++;hashi %= _tables.size();}//插入键值对_tables[hashi]._kv = kv;//改变状态_tables[hashi]._state = EXIST;//元素个数++_size++;return true;
}

哈希表的查找

查找步骤如下:

  1. 先判断哈希表是否为空,为空就赶回nullptr;
  2. 然后用哈希函数计算出哈希地址,在哈希地址处开始查找,如果在EXIST位置找到该元素,则查找成功,如果到空位置,则查找失败;

我们要注意的是必须对哈希地址进行限制,否则就会有越界的风险。

代码如下:

HashData<K, V>* Find(const K& key)
{//如果哈希表为空,就返回falseif (_tables.size() == 0){return nullptr;}//计算哈希地址size_t hashi = key % _tables.size();size_t start = hashi;//在状态不为EMPTY的地方进行查找while (_tables[hashi]._state != EMPTY){//如果此时该位置不是DELETE状态并且key值等于查找的key值,返回该位置地址if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key){return &_tables[hashi];}//去下一个位置进行查找hashi++;hashi %= _tables.size();if (hashi == start){break;}}//找不到就返回空return nullptr;
}

哈希表的删除

删除步骤如下:
判断哈希表中是否存在该值,存在就将该位置状态设置为DELETE,不存在就直接返回false。

bool Erase(const K& key)
{//判断该key值是否存在HashData<K, V>* ret = Find(key);//存在即为真if (ret){//存在就更改该位置状态ret->_state = DELETE;_size--;}//否则就返回falsereturn false;
}

上述情况针对int类型的的数据没有问题,但是如果是string类型的数据,我们就会发现,就无法直接对key值比较进行判断,此时就需要针对string类型创建一个仿函数,如果我们此时传入的是一个string对象,就可以调用仿函数得到val值,然后在就可以就算哈希地址了;

template<class K>
struct HashFunc
{size_t operator()(const K& key){//所有类型都强转为size_t类型return (size_t)key;}
};
//模板特化
template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t val = 0;for (auto ch : key){val *= 131;val += ch;}return val;}
};

代码整体就可以优化为:

namespace CloseHash
{enum State{EMPTY,   //(未存储数据的空位置)EXIST,   //(已存储数据)DELETE   //(数据被删除)};template<class K, class V>struct HashData{pair<K, V> _kv;//每个位置最开始为空State _state = EMPTY;};template<class K>struct HashFunc{size_t operator()(const K& key){//所有类型都强转为size_t类型return (size_t)key;}};//模板特化template<>struct HashFunc<string>{size_t operator()(const string& key){size_t val = 0;for (auto ch : key){val *= 131;val += ch;}return val;}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:bool Insert(const pair<K, V>& kv){//如果此处键值对已经存在,就返回falseif (Find(kv.first)){return false;}//判断是需要进行扩容if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7){size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();//创建一个新的哈希表HashTable<K, V, Hash> newHT;newHT._tables.resize(newSize);//将旧表数据重新计算哈希地址插入到新表中for (auto e : _tables){if (e._state == EXIST){newHT.Insert(e._kv);}}//将旧表数据与新表数据进行交换_tables.swap(newHT._tables);}Hash hash;//计算哈希地址size_t hashi = hash(kv.first) % _tables.size();//寻找状态为DELETE和EMPTY位置进行插入while (_tables[hashi]._state == EXIST){hashi++;hashi %= _tables.size();}//插入键值对_tables[hashi]._kv = kv;//改变状态_tables[hashi]._state = EXIST;//元素个数++_size++;return true;}HashData<K, V>* Find(const K& key){//如果哈希表为空,就返回falseif (_tables.size() == 0){return nullptr;}Hash hash;//计算哈希地址size_t hashi = hash(key) % _tables.size();size_t start = hashi;//在状态不为EMPTY的地方进行查找while (_tables[hashi]._state != EMPTY){//如果此时该位置不是DELETE状态并且key值等于查找的key值,返回该位置地址if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key){return &_tables[hashi];}//去下一个位置进行查找hashi++;hashi %= _tables.size();if (hashi == start){break;}}//找不到就返回空return nullptr;}bool Erase(const K& key){//判断该key值是否存在HashData<K, V>* ret = Find(key);//存在即为真if (ret){//存在就更改该位置状态ret->_state = DELETE;_size--;}//否则就返回falsereturn false;}void Print(){for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){printf("[%d:%d] ", i, _tables[i]._kv.first);}else{printf("[%d:*] ", i);}}cout << endl;}private:vector<HashData<K, V>> _tables;//哈希表size_t _size = 0;//哈希表中有效元素个数};
  1. 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:

Hi = (H0 + i²)%m ( i = 1 , 2 , 3 , . . . )
H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过线性探测后得到的存放位置。
m:表的大小
在这里插入图片描述

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

开散列

开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

例如,我们用除留余数法将下图序列插入到表长为10的哈希表中,当发生哈希冲突时我们采用开散列的形式,将哈希地址相同的元素都链接到同一个哈希桶下,插入过程如下:
在这里插入图片描述

哈希表的结构

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

template<class K, class V>
class HashNode
{pair<K, V> _kv;HashNode<K, V>* next;//构造函数HashNode(const pair<K, V>& kv):_kv(kv),next(nullptr){}
};

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

template<class K, class V>
class HashTable
{typedef HashNode<K, V> Node;
public://
private:vector<Node*> _tables;size_t size = 0;
};

我们还需在这儿提供一个哈希表的析构函数,用于哈希表中单链表结点的释放:

~HashTable()
{for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->next;delete cur;cur = next;}_tables[i] = nullptr;}
}

哈希表的插入

开散列的哈希表插入与闭散列插入思想大致相同,都需要计算哈希地址出哈希地址,但是开散列的哈希表扩容以后,只是将旧表的结点移动到新表当中,并不是复用旧表数据,当开散列的负载因子为1时,我们就创建一个新的哈希表,将旧表结点移动至新表中,在交换旧表与新表数据即可;

bool Insert(const pair<K, V>& kv)
{//如果该键值对存在,就返回falseif (Find(kv.first)){return false;}//如果负载因子为1就扩容if (_size == _tables.size()){//创建一个新的哈希表vector<Node*> newTables;size_t newSizes = _size == 0 ? 10 : 2 * _tables.size();//将每个元素初始化为空newTables.resize(newSizes, nullptr);//将旧表结点插入到新表当中for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){//记录cur的下一个结点Node* next = cur->_next;//计算相应的哈希桶编号size_t hashi = cur->_kv.first % newTables.size();//将旧表结点移动值新表cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}//计算哈希桶编号size_t hashi = kv.first % _tables.size();//插入结点Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;//元素个数++_size++;return true;
}

哈希表的查找

查找步骤如下:

  1. 先判断哈希表大小是否为0,为0就返回nullptr;
  2. 通过哈希函数计算出对应的哈希地址;
  3. 通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可。
Node* Find(const K& key)
{//哈希表为空就返回空if (_tables.size() == 0){return nullptr;}//计算哈希地址size_t hashi = key % _tables.size();Node* cur = _tables[hashi];//遍历哈希桶while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}

哈希表的删除

查找步骤如下:

  1. 先判断哈希表大小是否为0,为0就返回false;
  2. 通过哈希函数计算出对应的哈希地址;
  3. 通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行删除即可。
bool Erase(const K& key)
{//哈希表大小为0,删除失败if (_tables.size() == 0){return false;}//计算哈希地址size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];//遍历哈希桶,寻找删除结点是否存在while (cur){if (cur->_kv.first == key){if (prev){prev->next = cur->next;}else{_tables[hashi] = cur->next;}//删除该结点delete cur;_size--;return true;}prev = cur;cur = cur->next;}//删除结点不存在,返回falsereturn false;
}

同样,我们也可以针对string提供一个仿函数,代码整体优化如下:

namespace HashBucket
{template<class K, class V>struct HashNode{pair<K, V> _kv;HashNode<K, V>* _next;//构造函数HashNode(const pair<K, V>& kv):_kv(kv),_next(nullptr){}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{typedef HashNode<K, V> Node;public:~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}bool Insert(const pair<K, V>& kv){//如果该键值对存在,就返回falseif (Find(kv.first)){return false;}Hash hash;//如果负载因子为1就扩容if (_size == _tables.size()){//创建一个新的哈希表vector<Node*> newTables;size_t newSizes = _size == 0 ? 10 : 2 * _tables.size();//将每个元素初始化为空newTables.resize(newSizes, nullptr);//将旧表结点插入到新表当中for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){//记录cur的下一个结点Node* next = cur->_next;//计算相应的哈希桶编号size_t hashi = hash(cur->_kv.first) % newTables.size();//将旧表结点移动值新表cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}//计算哈希桶编号size_t hashi = hash(kv.first) % _tables.size();//插入结点Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;//元素个数++_size++;return true;}//查找Node* Find(const K& key){//哈希表为空就返回空if (_tables.size() == 0){return nullptr;}Hash hash;//计算哈希地址size_t hashi = hash(key) % _tables.size();Node* cur = _tables[hashi];//遍历哈希桶while (cur){if ((cur->_kv.first) == key){return cur;}cur = cur->_next;}return nullptr;}//删除bool Erase(const K& key){//哈希表大小为0,删除失败if (_tables.size() == 0){return false;}Hash hash;//计算哈希地址size_t hashi = hash(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];//遍历哈希桶,寻找删除结点是否存在while (cur){if (hash(hash(cur->_kv.first)) == key){if (prev){prev->_next = cur->_next;}else{_tables[hashi] = cur->_next;}//删除该结点delete cur;_size--;return true;}prev = cur;cur = cur->_next;}//删除结点不存在,返回falsereturn false;}private:vector<Node*> _tables;size_t _size = 0;};
}

我们在查看stl库中源码可以发现,哈希桶的扩容方式的大小都为素数,这是为了减小哈希冲突所设计出来的,研究发现,哈希表扩容大小设置为素数,极大地减少了哈希冲突。我们将一系列素数存储在一个数组当中,然后根据情况在进行扩容即可:

inline size_t __stl_next_prime(size_t n)
{static const size_t __stl_num_primes = 28;static const size_t __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};for (size_t i = 0; i < __stl_num_primes; ++i){if (__stl_prime_list[i] > n){return __stl_prime_list[i];}}return -1;
}

最终插入代码扩容就可以优化为:

newTables.resize(__stl_next_prime(_tables.size()), nullptr);

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

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

相关文章

git everything up-to-date解决方法

git push origin master 总是提示git everything up-to-date,原因是远端没有分支 . 操作如下:(1)查看当前分支名 git branch -av(2)git push --set-upstream origin (当前分支名)

Linux-相关操作

2.2.2 Linux目录结构 /&#xff1a;根目录&#xff0c;一般根目录下只存放目录&#xff0c;在Linux下有且只有一个根目录。所有的东西都是从这里开始。当你在终端里输入“/home”&#xff0c;你其实是在告诉电脑&#xff0c;先从/&#xff08;根目录&#xff09;开始&#xf…

P2239 [NOIP2014 普及组] 螺旋矩阵

题目链接&#xff1a; 找到矩阵元素与 n , i , j \rm n, i, j n,i,j之间的关系&#xff0c;然后直接输出 #include <bits/stdc.h>using namespace std;int find(int n, int i, int j) {if (i 1) return j;if (j n) return i n - 1;if (i n) return 3 *n - 1 - j;if …

广州xx策划公司MongoDB恢复-2023.09.09

2023.09.08用户的MongoDB数据库被勒索病毒攻击&#xff0c;数据全部被清空。 提示&#xff1a; mongoDB的默认端口为27017&#xff0c;黑客通常通过全网段扫描27017是否开放判断是否是MongoDB服务器。一旦发现27017开放&#xff0c;黑客就会用空密码、弱密码尝试连接数据库。黑…

Mojo-SDK详细安装教程

Mojo-SDK安装 运行环境&#xff1a;windows11wsl2&#xff08;ubuntu1804&#xff09; 官方推荐&#xff1a;wsl2&#xff08;ubuntu2204&#xff09;&#xff0c;我下面是wsl2&#xff08;ubuntu1804&#xff09;&#xff0c;发现有些问题&#xff0c;不知道是不是ubuntu版本问…

opencv dnn模块 示例(16) 目标检测 object_detection 之 yolov4

博客【opencv dnn模块 示例(3) 目标检测 object_detection (2) YOLO object detection】 测试了yolov3 及之前系列的模型&#xff0c;有在博客【opencv dnn模块 示例(15) opencv4.2版本dnn支持cuda加速&#xff08;vs2015异常解决&#xff09;】 说明了如何使用dnn模块进行cuda…

kafka 3.5 主题分区ISR伸缩源码

ISR(In-sync Replicas)&#xff1a;保持同步的副本 OSR(Outof-sync Replicas)&#xff1a;不同步的副本。最开始所有的副本都在ISR中&#xff0c;在kafka工作的过程中&#xff0c;如果某个副本同步速度慢于replica.lag.time.max.ms指定的阈值&#xff0c;则被踢出ISR存入OSR&am…

(1)输入输出函数:cin和cout(2)数学函数:sqrt、pow、sin、cos、tan等

输入输出函数&#xff1a;cin 和 cout 在C编程语言中&#xff0c;为了与用户进行交互和显示程序的结果&#xff0c;我们使用了两个非常重要的函数&#xff1a;cin 和 cout。这两个函数分别用于输入和输出。 cin是C中的标准输入流对象&#xff0c;它用于从键盘接收用户的输入。…

SQL12 高级操作符练习(2)

描述 题目&#xff1a;现在运营想要找到学校为北大或GPA在3.7以上(不包括3.7)的用户进行调研&#xff0c;请你取出相关数据&#xff08;使用OR实现&#xff09; 示例&#xff1a;user_profile iddevice_idgenderageuniversitygpa12138male21北京大学3.423214male复旦大学4.03…

六、不root不magisk不xposed lsposed frida原生修改定位

前言常用风控APP检测1.Aida64检测2.momo检测3.微霸检测4.cellular-z检测 厂商测试总结 前言 不root不戴面具 不xposed lsposed frida&#xff0c;不分身&#xff0c;不多开&#xff0c;最完美的原生修改定位。 常用风控APP检测 先看效果再说原理&#xff0c;先过一遍环境 1.Ai…

聚类分析 | MATLAB实现基于SOM自组织特征映射聚类可视化

聚类分析 | MATLAB实现基于SOM自组织特征映射聚类可视化 目录 聚类分析 | MATLAB实现基于SOM自组织特征映射聚类可视化效果一览基本介绍程序设计参考资料 效果一览 基本介绍 基于自组织特征映射聚类算法(SOM)的数据聚类可视化 可直接运行 注释清晰 Matlab语言 1.多特征输入&…

Python 可迭代对象、迭代器、生成器

可迭代对象 定义 在Python的任意对象中&#xff0c;只要它定义了可以返回一个迭代器的 __iter__ 魔法方法&#xff0c;或者定义了可以支持下标索引的 __getitem__ 方法&#xff0c;那么它就是一个可迭代对象&#xff0c;通俗的说就是可以通过 for 循环遍历了。Python 原生的列…

爬虫 — 正则案例

目录 一、需求二、页面分析三、代码实现 一、需求 目标网站&#xff1a;http://www.weather.com.cn/weather/101010700.shtml 需求&#xff1a;获取日期&#xff0c;天气&#xff0c;温度&#xff0c;风力数据 二、页面分析 1、确定 url&#xff0c;静态加载 url&#xff1a;ht…

Mybatis的mapper.xml批量插入、修改sql

今天要有个功能&#xff0c;要进行一批数据的插入和修改&#xff0c;为了不频繁调用数据库&#xff0c;所以想到了批量插入和修改&#xff0c;因为从毕业后&#xff0c;就没写过批量插入和批量修改&#xff0c;所以在这里记录一下&#xff0c;避免后续再遇到忘记怎么写了 批量…

【小记录】jupyter notebook新版本

手欠升级 &#x1f605;今天手贱&#xff0c;在anaconda navigator里面更新了最新版本的spyder&#xff0c;然后莫名奇妙地jupyter notebook就打不开了&#x1f605;&#xff0c;报错说缺少模块”ModuleNotFoundError: No module named jupyter_server.contents“&#xff0c;…

Python分享之对象的属性

Python一切皆对象(object)&#xff0c;每个对象都可能有多个属性(attribute)。Python的属性有一套统一的管理方案。 属性的__dict__系统 对象的属性可能来自于其类定义&#xff0c;叫做类属性(class attribute)。类属性可能来自类定义自身&#xff0c;也可能根据类定义继承来的…

docker挂载目录权限问题

虽然是root身份进入docker但是依然有些权限是没有的&#xff01; 一、docker权限参数 可以解决挂载目录操作权限低 使用–privilegedtrue和-u参数来给Docker容器授权 docker run -it --privilegedtrue -uroot --namemysqlTest -v /root/data:/root/data_container mysql:5.7…

从0到1学会Git(第三部分):Git的远程仓库链接与操作

写在前面:前面两篇文章我们已经学会了git如何在本地进行使用&#xff0c;这篇文章将讲解如何将本地的git仓库和云端的远程仓库链接起来并使用 为什么要使用远程仓库:因为我们需要拷贝我们的代码给别人以及进行协同开发&#xff0c;就需要有一个云端仓库进行代码的存储和同步&a…

常见的HTTP请求方式

目录 GET 请求 POST 请求 PUT 请求 DELETE 请求 PATCH 请求 HEAD 请求 OPTIONS 请求 HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是一种用于传输数据的协议&#xff0c;它在互联网中扮演了至关重要的角色。HTTP请求方式定义了客户端与服务器之间的通信方式…

【数据结构】C++实现AVL平衡树

文章目录 1.AVL树的概念2.AVL树的实现AVL树结点的定义AVL树的插入AVL树的旋转左单旋右单旋左右双旋右左双旋插入代码 AVL树的验证AVL树的查找AVL树的修改AVL树的删除AVL树的性能 AVL树的代码测试 1.AVL树的概念 二叉搜索树虽然可以提高我们查找数据的效率&#xff0c;但如果插…