【C++】用哈希桶模拟实现unordered_set和unordered_map

目录

  • 一、哈希介绍
    • 1.1 哈希概念
    • 1.2 哈希冲突解决
      • 1.2.1 闭散列
      • 1.2.2 开散列
  • 二、哈希桶
    • 2.1 实现哈希桶
      • 2.1.1 构造节点和声明成员变量
      • 2.1.2 构造与析构
      • 2.1.3 仿函数
      • 2.1.4 查找
      • 2.1.5 插入
      • 2.1.6 删除
    • 2.2 kv模型哈希桶源代码
  • 三、改造哈希桶
    • 3.1 begin+end
    • 3.2 迭代器
      • 3.2.1 前置++
    • 3.3 改造后哈希桶与迭代器源代码
  • 四、模拟实现unordered_set
  • 五、模拟实现unordered_map

一、哈希介绍

1.1 哈希概念

顺序结构中(数组)查找一个元素需要遍历整个数组,时间复杂度为O(N);树形结构中(二叉搜索树)查找一个元素,时间复杂度最多为树的高度次logN。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

主要有3种操作:

  • 插入——根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 查找——根据要搜索的元素的关键码,用函数计算出存储位置,取该位置的元素关键码进行比较,如果相等,查找成功
  • 删除——根据待删除元素的关键码计算出该元素的存储位置,如果改元素存在,则进行删除

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

哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快

如果在上面的例子中插入元素44会怎样?44%10也是4,与原来元素4的位置冲突了。那么这个新插入的44应该如何放置呢?

1.2 哈希冲突解决

首先要知道哈希冲突的原因——哈希函数设计不够合理
哈希函数设计原则:

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

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

下面介绍两种常见的哈希函数:

  1. 闭散列
  2. 开散列

1.2.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,这里的下一个可能也有元素,所以可能继续重复前面的操作,直到遇到空位置。

线性探测:
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
在这里插入图片描述
44%4=4,与元素4的位置冲突,到它的下一个位置,位置5也有元素,继续下一个,直到位置8没有存储元素,就把44存储到位置8中。

1.2.2 开散列

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

在这里插入图片描述

开散列中每个桶中放的都是发生哈希冲突的元素。

二、哈希桶

2.1 实现哈希桶

2.1.1 构造节点和声明成员变量

哈希表的每个位置是一个桶,这个桶的结构是单链表,单链表由每个节点组成。节点有数据域和指针域,指针域是用来连接下一个节点的,数据域存放的是节点的值。节点的数据域有两种:k模型和kv模型。这里实现哈希桶的是数据域是kv模型。

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){}
};

成员变量有存放的数据个数和哈希表的每个位置,即每个桶,用头指针进行连接,所以哈希表的每个位置是单链表的头指针。

vector<Node*> _table;//哈希表的每个位置-桶-单链表
size_t _n = 0;//存储的元素个数

2.1.2 构造与析构

1️⃣构造
刚开始给哈希表一定的空间大小,每个位置初始化为空指针。

HashTable(size_t n = 5)
{_table.resize(n, nullptr);
}

2️⃣析构
哈希表的结构是STL库中的vector,当程序结束时,vector会自动调用它的析构来清理哈希表,但是表中的每个位置是单链表,单链表的每个节点是动态开辟出来的,vector的析构不能清理它们。所以要自己写析构函数来清理这些节点。

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

2.1.3 仿函数

哈希函数的计算公式:hash(key) = key % capacity,capacity就是表的空间大小,key必须是整数,但是key值是不确定的,有可能是整形、浮点型或者是字符串,所以要对key值作一些处理,使其变成整数才能进行取模操作。

1️⃣key不是字符串
返回值都转化成无符号整数

template<class K>
struct HashFind
{size_t operator()(const K& key){return key;}
};

2️⃣key是字符串
因为传过来的参数固定就是字符串(string类型),不像前面,可能是int、double等,所以这里可以直接特化处理。定义一个临时变量为无符号整数作为返回值,遍历每个字符加到临时变量里,每个字符会自动转换成ASCII码值,然后再乘上权值131(在《The C Programming Language》书中有解释),保证不会出现year和raey相同的场景。

template<>
struct HashFind<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto& e : s){hash += e;hash *= 131;}return hash;}
};

在后面的操作中使用到哈希函数:hash(key) = key % capacity,都要通过调用仿函数来实现。

2.1.4 查找

查找一个元素是否存在,首先要计算出该元素的位置。假设该元素存在,但是它在某个位置的桶中,通过遍历单链表找到该元素,然后返回它在链表中的节点位置。不存在,返回nullptr

Node* Find(const K& key)
{Hash hs;size_t hashi = hs(key) % _table.size();//表中的位置Node* cur = _table[hashi];//得到当前位置头节点while (cur){if (cur->_kv.first == key)//找到了{return cur;}cur = cur->_next;}//cur为空,不存在这个数据return nullptr;
}

2.1.5 插入

  1. 插入新的元素,不能有重复的,所以先对要插入的值进行查找,如果找到了,说明是重复元素,不能插入,返回失败。
  2. 插入新的数据不是重复元素,计算该元素在哈希表的映射位置,创建一个新节点,用头插法插入。
  3. 如果要插入数据前,哈希表的元素个数与哈希表的空间大小相等,就要扩容。创建一个新的哈希表,扩容的大小可以给原来的两倍,初始化为空。遍历旧表,将旧表的节点移动到新表中。注意,移动的过程中节点在旧表中的位置与新表可能是不对应的,所以还要用哈希函数得到节点在新表的位置,然后插入的话还是头插法。旧表中的每个位置即每个桶移动完成,,就将旧表的该位置置空。最后全部转移完,把旧表和新表进行交换,后面使用的就是新表了。
bool Insert(const pair<K,V>& kv)
{//重复元素不能插入if (Find(kv.first)){return false;}Hash hs;//扩容if (_n == _table.size()){vector<Node*> newTable(2 * _table.size(), nullptr);//新的空间//将旧表的节点移到新表中,再交换for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){size_t hashi = hs(_table[i]->_kv.first) % newTable.size();Node* next = cur->_next;cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTable);}//插入数据size_t hashi = hs(kv.first) % _table.size();//表中的位置Node* newNode = new Node(kv);//新节点//头插法newNode->_next = _table[hashi];_table[hashi] = newNode;++_n;return true;
}

2.1.6 删除

删除某个元素,首先得查找该元素是否存在。如果不存在返回false;存在,通过哈希函数计算该元素的位置(是哪个桶的),得到该位置的头指针(第一个节点),然后遍历单链表,找到后删除。

遍历的过程中要注意两种可能:

  1. 要删除的节点是第一个节点
  2. 要删除的节点是中间某个节点或者最后一个节点
bool Erase(const K& key)
{Hash hs;Node* ret = Find(key);if (ret){size_t hashi = hs(ret->_kv.first) % _table.size();//先找到表的位置Node* cur = _table[hashi];//得到该位置的头节点Node* prev = nullptr;//前面的节点连接while (cur){if (cur->_kv.first == key){if (prev)//第一个节点不是要删除的{prev->_next = cur->_next;}else{_table[hashi] = cur->_next;}delete cur;break;//找到具体节点删除后跳出}prev = cur;cur = cur->_next;}return true;}else//找不到,删除失败{return false;}
}

2.2 kv模型哈希桶源代码

#include <iostream>
#include <vector>
#include <string>
using namespace std;//仿函数
template<class K>
struct HashFind
{size_t operator()(const K& key){return key;}
};template<>
struct HashFind<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto& e : s){hash += e;hash *= 131;}return hash;}
};//节点
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 Hash = HashFind<K>>
class HashTable
{typedef HashNode<K,V> Node;
public://构造HashTable(size_t n = 5){_table.resize(n, nullptr);}//析构~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}//查找Node* Find(const K& key){Hash hs;size_t hashi = hs(key) % _table.size();//表中的位置Node* cur = _table[hashi];//得到当前位置头节点while (cur){if (cur->_kv.first == key)//找到了{return cur;}cur = cur->_next;}//cur为空,不存在这个数据return nullptr;}//插入bool Insert(const pair<K,V>& kv){//重复元素不能插入if (Find(kv.first)){return false;}Hash hs;//扩容if (_n == _table.size()){vector<Node*> newTable(2 * _table.size(), nullptr);//新的空间//将旧表的节点移到新表中,再交换for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){size_t hashi = hs(_table[i]->_kv.first) % newTable.size();Node* next = cur->_next;cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTable);}//插入数据size_t hashi = hs(kv.first) % _table.size();//表中的位置Node* newNode = new Node(kv);//新节点//头插法newNode->_next = _table[hashi];_table[hashi] = newNode;++_n;return true;}//删除bool Erase(const K& key){Hash hs;Node* ret = Find(key);if (ret){size_t hashi = hs(ret->_kv.first) % _table.size();//先找到表的位置Node* cur = _table[hashi];//得到该位置的头节点Node* prev = nullptr;//前面的节点连接while (cur){if (cur->_kv.first == key){if (prev)//第一个节点不是要删除的{prev->_next = cur->_next;}else{_table[hashi] = cur->_next;}delete cur;break;//找到具体节点删除后跳出}prev = cur;cur = cur->_next;}return true;}else//找不到,删除失败{return false;}}private:vector<Node*> _table;size_t _n = 0;
};

三、改造哈希桶

前面的哈希桶的数据类型是固定的kv模型,为了后面方便模拟实现unordered_set和unordered_map,将数据类型改成T,这个T可以是key,也可以是kv。到底是哪个取决于使用的是unordered_set还是unordered_map,用的是unordered_set,模板就用unordered_set的仿函数,另一个同理。

template<class T>
struct HashNode
{HashNode<T>* _next;T _data;HashNode(const T& data):_data(data), _next(nullptr){}
};

Find的返回值修改为迭代器,Insert的返回值修改为pair< iterator, bool>

3.1 begin+end

1️⃣begin
遍历哈希表,一旦遇到有节点的位置,返回该位置的迭代器。迭代器需要传两个参数,该位置的头指针和当前哈希表(与后面实现迭代器类中的成员变量对应)

iterator begin()
{for (size_t i = 0; i < _table.size(); i++){if (_table[i]){return iterator(_table[i], this);}}return end();
}

2️⃣end
返回的是最后一个节点的下一个位置,单链表的最后一个节点的下一个是空指针,所以返回迭代器,参数传的是空指针和当前哈希表。

iterator end()
{return iterator(nullptr, this);
}

3.2 迭代器

虽然哈希表的结构用的是vector,但是每个位置是单链表,++操作不能像数组一样就行,所以要对每个节点的迭代器进行封装,方便++找到下一个节点。

注意:每个位置的桶是单链表,所以没有前置- -

模板参数有KeyOfT和Hash是因为前置++需要数据类型的仿函数和转换为无符号整数的仿函数,这里先说明一下。迭代器的成员有节点应该就可以了,为什么还要哈希表类变量?因为等会前置++的代码中要通过哈希函数计算节点数据的位置,哈希函数要有哈希表的空间大小,没有哈希表类成员,怎么得到哈希表的空间大小。

其他的与之前一样,不作重复叙述了

template<class K, class T, class KeyOfT, class Hash>
struct HashIterator
{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> Ht;typedef HashIterator< K, T, KeyOfT, Hash> Self;Node* _node;Ht* _ht;HashIterator(Node* node, Ht* ht):_node(node),_ht(ht){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self& operator++(){//....}bool operator==(const Self& s){return _node == s._node;}bool operator!=(const Self& s){return _node != s._node;}
};

3.2.1 前置++

分为两种情况:

  1. 当前节点的下一个节点不为空
  2. 当前节点的下一个节点为空

1️⃣情况一:
在这里插入图片描述
直接到下一个节点即可。

2️⃣情况二:
在这里插入图片描述

下一个节点为空,先计算当前节点在哈希表的位置(具体哪个桶),然后在该位置往后开始找,只要遇到有节点的位置,下一个节点就是它;如果后面都没有节点,下一个节点就是空。

Self& operator++()
{if (_node->_next){_node = _node->_next;}else{Hash hs;KeyOfT kot;size_t hashi = hs(kot(_node->_data)) % _ht->_table.size();hashi++;while (hashi < _ht->_table.size()){if (_ht->_table[hashi]){_node = _ht->_table[hashi];break;}hashi++;}if (_ht->_table.size() == hashi){_node = nullptr;}}return *this;
}

3.3 改造后哈希桶与迭代器源代码

#include <iostream>
#include <vector>
#include <string>
using namespace std;//仿函数
template<class K>
struct HashFind
{size_t operator()(const K& key){return key;}
};template<>
struct HashFind<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto& e : s){hash += e;hash *= 131;}return hash;}
};//节点
template<class T>
struct HashNode
{HashNode<T>* _next;T _data;HashNode(const T& data):_data(data), _next(nullptr){}
};//前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
//正向迭代器
template<class K, class T, class KeyOfT, class Hash>
struct HashIterator
{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> Ht;typedef HashIterator< K, T, KeyOfT, Hash> Self;Node* _node;Ht* _ht;HashIterator(Node* node, Ht* ht):_node(node),_ht(ht){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self& operator++(){if (_node->_next){_node = _node->_next;}else{Hash hs;KeyOfT kot;size_t hashi = hs(kot(_node->_data)) % _ht->_table.size();hashi++;while (hashi < _ht->_table.size()){if (_ht->_table[hashi]){_node = _ht->_table[hashi];break;}hashi++;}if (_ht->_table.size() == hashi){_node = nullptr;}}return *this;}bool operator==(const Self& s){return _node == s._node;}bool operator!=(const Self& s){return _node != s._node;}
};template<class K, class T, class KeyOfT, class Hash>
class HashTable
{template<class K, class T, class KeyOfT, class Hash>friend struct HashIterator;typedef HashNode<T> Node;
public:typedef HashIterator <K, T, KeyOfT, Hash> iterator;//迭代器iterator begin(){for (size_t i = 0; i < _table.size(); i++){if (_table[i]){return iterator(_table[i], this);}}return end();}iterator end(){return iterator(nullptr, this);}//构造HashTable(size_t n = 5){_table.resize(n, nullptr);}//析构~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_table[i] = nullptr;}}//查找iterator Find(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _table.size();//表中的位置Node* cur = _table[hashi];//得到当前位置头节点while (cur){if (kot(cur->_data) == key)//找到了{return iterator(cur, this);}cur = cur->_next;}//cur为空,不存在这个数据return end();}//插入pair<iterator, bool> Insert(const T& data){KeyOfT kot;//重复元素不能插入iterator pos = Find(kot(data));if (pos != end()){return make_pair(pos, false);}Hash hs;//扩容if (_n == _table.size()){vector<Node*> newTable(2 * _table.size(), nullptr);//新的空间//将旧表的节点移到新表中,再交换for (size_t i = 0; i < _table.size(); i++){Node* cur = _table[i];while (cur){size_t hashi = hs(kot(_table[i]->_data)) % newTable.size();Node* next = cur->_next;cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newTable);}//插入数据size_t hashi = hs(kot(data)) % _table.size();//表中的位置Node* newNode = new Node(data);//新节点//头插法newNode->_next = _table[hashi];_table[hashi] = newNode;++_n;return make_pair(iterator(newNode, this), true);}//删除bool Erase(const K& key){Hash hs;KeyOfT kot;iterator ret = Find(key);if (ret != end()){size_t hashi = hs(kot(ret._node->_data)) % _table.size();//先找到表的位置Node* cur = _table[hashi];//得到该位置的头节点Node* prev = nullptr;//前面的节点连接while (cur){if (kot(cur->_data) == key){if (prev)//第一个节点不是要删除的{prev->_next = cur->_next;}else{_table[hashi] = cur->_next;}delete cur;break;//找到具体节点删除后跳出}prev = cur;cur = cur->_next;}return true;}else//找不到,删除失败{return false;}}private:vector<Node*> _table;size_t _n = 0;
};

四、模拟实现unordered_set

unordered_set是k模型,它的仿函数就返回key。模板参数有key和Hash,Hash是用来转换key变成无符号整数的。其他接口调用哈希桶的即可。

#include "HashTable.h"
namespace yss
{template <class K, class Hash = HashFind<K>>class unordered_set{//仿函数struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename HashTable<K, const K, SetKeyOfT, HashFind<K>>::iterator iterator;//迭代器iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}//插入pair<iterator, bool> Insert(const K& key){return _ht.Insert(key);}//查找iterator Find(const K& key){return _ht.Find(key);}//删除bool Erase(const K& key){return _ht.Erase(key);}private:HashTable<K, const K, SetKeyOfT, Hash> _ht;};
}

在这里插入图片描述

typename的作用是把HashTable<K, const K, SetKeyOfT, HashFind< K >>::iterator当做一个类型,而不是变量。typedef就是重命名。

五、模拟实现unordered_map

unordered_map是kv模型,仿函数传的是kv中的key。除了查找、插入、删除外,还有operator[],可以进行统计元素等操作

#include "HashTable.h"
namespace yss
{template <class K, class V, class Hash = HashFind<K>>class unordered_map{//仿函数struct MapKeyOfT{const K& operator()(const pair<K,V>& kv){return kv.first;}};public:typedef typename HashTable<K, pair<const K, V>, MapKeyOfT, HashFind<K>>::iterator iterator;//迭代器iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}//插入pair<iterator, bool> Insert(const pair<K, V>& kv){return _ht.Insert(kv);}//查找iterator Find(const K& key){return _ht.Find(key);}//删除bool Erase(const K& key){return _ht.Erase(key);}//operator[]V& operator[](const K& key){pair<iterator, bool> ret = Insert(make_pair(key, V()));return ret.first->second;}private:HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;};
}

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

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

相关文章

【C语言进阶篇】编译和链接

【C语言进阶篇】编译和链接 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;C语言&#x1f353; &#x1f33c;文章目录&#x1f33c; 编译环境与运行环境 1. 翻译环境 2. 编译环境&#xff1a;预编译&#xff08;预处理&#xff09;编…

动态规划标题

题目&#xff1a;猫粮规划 每种食物都有两种状态&#xff0c;记忆化dfs当然可以&#xff0c;但是你是否觉得这个题很想之前讲过的“小A点菜 ”&#xff1f;那道题问的是对于那些菜要花光她的钱&#xff0c;一共有多少方案&#xff1f;这道题问的是一个区间罢了&#xff0c;那么…

Swagger3探索之游龙入海

引言 后端开发中常用的接口调用工具一般使用Postman、ApiPost工具&#xff0c;但后期需要与前端联调&#xff0c;要补充接口文档花费大量时间&#xff0c;此时Swagger3应运而生&#xff0c;大大提高沟通交流的效率。 引用依赖 <!-- Swagger3 调用方式 http://ip:port/swa…

已后悔!为什么不早点用多微信管理工具?

对于有多个微信号的人来说&#xff0c;每次都要在不同微信号和设备之间来回切换&#xff0c;实在是既麻烦又容易搞混。 这时候&#xff0c;只需要一个多微信管理工具——微信管理系统就能解决啦&#xff01; 1、多号同时登陆聚合聊天 它支持多个微信号同时登录在同一个页面上…

【已解决】Vue 3+TS项目,无法找到模块“XXX”的声明文件,项目报错无法找到声明文件

前言 最近在做vue 3 TS项目&#xff0c;创建项目时需要引入vue-router 4&#xff0c;在main根文件中引入路由时出现了无法找到引入文件的报错。 解决 最后发现是创建router文件中的index文件时错误的创建为了.js文件&#xff0c;但是在创建框架时默认使用的是TS。将文件类型…

容器网络隔离验证

结论&#xff0c;可以直接扫描内网路由能通的机器。 1.节点1 192.168.55.6 2.节点2 192.168.55.5 3.非节点3 192.168.55.3

glBindTexture函数的理解

简单的说opengl的设计思想&#xff0c;实现较早&#xff0c;并不是基于对象&#xff0c;所以API操作&#xff0c; 总是以奇怪的enum枚举类型来作为操作对象。形成的编程范式就是先bind绑定到一个named obj&#xff08;其实是个整数&#xff09;并在内部创建一个该target类型对象…

DreamPolisher、InternLM2 、AniArtAvatar、PlainMamba、AniPortrait

本文首发于公众号&#xff1a;机器感知 DreamPolisher、InternLM2 、AniArtAvatar、PlainMamba、AniPortrait DreamPolisher: Towards High-Quality Text-to-3D Generation via Geometric Diffusion We present DreamPolisher, a novel Gaussian Splatting based method wit…

Spring Boot 使用过滤器、拦截器、监听器

前言 作用 过滤器&#xff08;Filter&#xff09;&#xff1a;当有一堆请求&#xff0c;只希望符合预期的请求进来。拦截器&#xff08;Interceptor&#xff09;&#xff1a;想要干涉预期的请求。监听器&#xff08;Listener&#xff09;&#xff1a;想要监听这些请求具体做了…

【数据结构】树、二叉树与堆(长期维护)

下面是关于树、二叉树、堆的一些知识分享&#xff0c;有需要借鉴即可。 一、初识树&#xff08;了解即可&#xff09; 1.树的概念 概念&#xff1a;一种非线性数据结构&#xff0c;逻辑形态上类似倒挂的树 树的构成&#xff1a;由一个根左子树右子树构成&#xff0c;其中子树…

unity学习(70)——编译游戏发生错误2

1.全屏问题其实无所谓&#xff0c;windows用tab可以切出来的。 2.现在主要问题是服务器try了以后虽然不崩溃了&#xff0c;但不再显示2个实例对象了&#xff0c;unity和exe此时都只能看到一个实例对象 2.1把之前报错位置的try-catch先注释掉 2.2 unity中此时登录666账号&…

leetcode代码记录(完全二叉树的节点个数

目录 1. 题目&#xff1a;2. 我的代码&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层…

洛谷_P5019 [NOIP2018 提高组] 铺设道路_python写法

P5019 [NOIP2018 提高组] 铺设道路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) n int(input()) data list(map(int,input().split()))ans 0 for i in range(1,n):if data[i]>data[i-1]:ans data[i]-data[i-1]print(ansdata[0])

python在运行时控制台以表格形式输出结果prettytable.PrettyTable()

使用prettytable库按表格的形式美化输出结果 效果如图&#xff1a; 表格中可接收列表格式的数据&#xff0c;列表中装字符串 # 引入模块 import prettytable as pt# 创建表格与表头&#xff0c;包含五列&#xff0c;分别为train-epoch&#xff0c;class&#xff0c;precisio…

Day02-DDLDMLDQL(定义,操作,查询)(联合查询,子查询,字符集和校对集,MySQL5.7乱码问题)

文章目录 Day02-DDL&DML和DQL学习目标1. SQL语言的组成2. DDL2.1 数据库结构2.2 表结构2.3 约束2.3.1 主键约束(重要)(1)特点(2) 添加主键(3)删除主键(了解) 2.3.2 自增约束(1)特点(2) 添加自增约束(3)删除自增约束(了解) 2.3.3 非空约束(1)添加非空约束(2) 删除非空约束 2…

Midjourney辞典AIGC中英双语图文辞典+Midjourney提示关键词

完整内容下载&#xff1a;https://download.csdn.net/download/u010564801/89042077 完整内容下载&#xff1a;https://download.csdn.net/download/u010564801/89042077 完整内容下载&#xff1a;https://download.csdn.net/download/u010564801/89042077

javaSwing坦克大战游戏

在游戏开发领域&#xff0c;坦克大战是一款经典的游戏&#xff0c;其简单而又耐玩的玩法吸引了无数玩家。而今&#xff0c;在Java编程技术的支持下&#xff0c;我们可以用Java Swing技术轻松实现这款经典游戏。本文将介绍如何使用Java Swing技术编写坦克大战游戏&#xff0c;并…

【Java程序设计】【C00352】基于Springboot的疫情隔壁酒店管理系统(有论文)

基于Springboot的疫情隔壁酒店管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以…

VS code中安装了git插件,报错无法使用怎么办?

你好&#xff0c;我是云桃桃。 一个希望帮助更多朋友快速入门 WEB 前端程序媛。 1枚程序媛&#xff0c;2年时间从1800到月入过万&#xff0c;工作5年买房。 分享成长心得❤️&#xff0c;和你一起慢慢变富。 后台回复“前端工具”可获取开发工具&#xff0c;持续更新中 后台…

如何在Linux Ubuntu系统安装Nginx服务并实现无公网IP远程连接

文章目录 1. 安装Docker2. 使用Docker拉取Nginx镜像3. 创建并启动Nginx容器4. 本地连接测试5. 公网远程访问本地Nginx5.1 内网穿透工具安装5.2 创建远程连接公网地址5.3 使用固定公网地址远程访问 在开发人员的工作中&#xff0c;公网远程访问内网是其必备的技术需求之一。对于…