【DS】哈希表,哈希桶的实现

目录

  • 哈希概念
  • 哈希冲突
  • 哈希函数
  • 负载因子
  • 哈希冲突的解决
    • 闭散列
    • 开散列
  • 哈希表闭散列的实现
    • 哈希表的结构
    • 哈希函数
    • 构造函数
    • 查找
    • 插入
    • 删除
  • 哈希表开散列的实现
    • 哈希表的结构
    • 查找
    • 插入
    • 删除
  • 哈希表的表长建议是素数

平衡二叉树的学习中,学习及模拟实现了AVL树和红黑树,得益于其结构,查找效率可以达到惊人的 O ( l o g N ) O(logN) O(logN),但是平衡树中调平衡的开销及学习的成本也是不低的。于是今天再来学习一个同样高效,甚至更优的哈希表(桶),其实现难度也没有AVL树和红黑树高;而且 unordered系列的关联式容器的底层就是采用了哈希结构,unordered系列的关联式容器比inordered系列的关联式容器(map,set,multi…)效率甚至更优;之所以效率比较高,是因为其底层使用了哈希结构。

哈希概念

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

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

例如:

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

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

  • 哈希是一种概念,哈希表(桶)才是数据结构。

如以下例子:

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小
示例

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。而且插入操作也简单高效。

既然哈希方法的效率如此高,那么它有什么缺陷吗?

哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) == Hash( k j k_j kj),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

如:

哈希冲突

哈希函数

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

哈希函数设计原则:

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

常用哈希函数——除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址

  • p一般为表长,即==m

哈希函数设计的核心就是尽量减少不同值经哈希函数计算后映射在同一哈希地址上,由此基础再尽量符合人们的认知即可。

  • 注意:哈希函数的设计只能降低哈希冲突发生的可能,但是无法避免哈希冲突
  • 借助负载因子,减少哈希冲突。

负载因子

定义:负载因子是指哈希表中已存储元素数量与哈希表总容量之间的比率。
计算公式:负载因子 λ = (已存储元素数量) / (哈希表总容量)。

性能影响:

  • 当负载因子较低时,哈希表相对空闲,有较多的空闲槽位可供使用,这有助于减少哈希冲突,提高查找和插入操作的效率。
  • 当负载因子较高时,哈希表的槽位大部分被占用,这可能会导致哈希冲突的增加,进而影响哈希表的性能。

空间利用率:

  • 负载因子越高,空间利用率越高,但可能牺牲一定的性能。
  • 负载因子越低,空间利用率越低,但性能可能更优。

同时,负载因子也可以是哈希表(桶)扩容的判断标志。不同实现方式的负载因子设定也不同。

哈希冲突的解决

对于哈希冲突这一问题,常用的解决方案有以下两种。

闭散列

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

那如何寻找下一个空位置呢?

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

还是上述情况,此时再插入一个44,由于哈希地址为4的已经有元素了,此时采用线性探测,一直往后检测寻找可插入位置,最终找到位置并插入。
哈希冲突

  • 在开散列中,使用负载因子来减少哈希冲突的产生,同时使用负载因子来控制表的大小,所以在插入一个元素时,表一定是有空闲位置的。所以不必担心冲突的元素没位置放的情况
  • 对于开放定址法,负载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cachemissing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了负载因子为0.75,超过此值将调整表的大小,从而减少哈希冲突。

采用线性探测来解决哈希冲突的元素实际上是一种“摆烂”行为,别人占了我的位置,我就去占别人的位置,长期看来也不是一种解决问题的办法,而且在元素范围集中的时候,会造成局部拥堵;可能会造成大量元素都不在哈希函数映射的位置,那这样实际上也失去了哈希的意义。

不考虑负载因子扩容的情况下:
线性探测

当然你还可以采用别的方案解决哈希冲突的问题,但是基于闭散列这个思想下本质和线性探测是一致的。

开散列

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

哈希桶

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素,因此,有人也把开散列的哈希表叫做哈希桶

哈希桶中的负载因子:
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容,也就是所开散列中的负载因子设为1比较合适

开散列与闭散列比较

应用链地址法处理哈希冲突,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。所以开散列的构思是更优的。

哈希表闭散列的实现

哈希表的结构

之后将进行关联式容器的模拟实现,所以直接使用pair存放数据。除此之外:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
哈希冲突
所以哈希表的数据类型为类HashData,其中包括一对pair,和状态标志State ;枚举类State 设有三种状态:EMPTY),删除DELETE),存在EXIST

enum State
{EXIST,EMPTY,DELETE
};template<class K,class V>
struct HashData
{pair<K,V> _kv;State _state = EMPTY;
};

哈希表的底层通常是一个数组,所以直接使用vector。而且库中的哈希表也是这样实现的。再加一个记录存储个数的_size
哈希表stl

template<class K,class V,class Hash = HashFunc<K>>
class HashTable
{
public://functionprivate:vector<HashData<K, V>> _table;size_t _size;
};

哈希函数

对于哈希表而言,其实现难度之一就是如何处理数据类型不是整型的问题

哈希表的底层是数组,哈希函数采用除留余数法时,需要K值支持%的操作,从而才能映射出有效的哈希地址(数组的下标只支持整型)

所以,在哈希表结构介绍时模板的第三个参数为一个仿函数,也就是我们所需的哈希函数来解决取整的问题。这个哈希函数只需要将传入的类型强转为整型即可。

  • 只要K值能映射出有效的哈希地址(返回一个整型)即可进行映射插入操作,如:浮点数经哈希函数处理后只会保留整数部分,虽然不是原原本本地按K值映射(浮点数也不能映射),但是起码可以将浮点数转为接近的整型先映射到表中。查找时是会进行比对的,所以不需要的担心能否找回对应的K值。
  • 这里的整型为无符号整型:数组下标也不支持负数。
template<class K>
struct HashFunc
{size_t operator()(const K&key){return (size_t)key;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto& e : key){hash *= 31;hash += e;}return hash;}
};
  • string可以视为常用类型,所以这里走个模板特化,当K类型string时匹配这个string特化版的哈希函数

字符串的哈希算法具体可以参考——字符串哈希算法
开散列的哈希表的哈希函数也是如此。

构造函数

哈希表的成员为一个vector和一个在声明时给了默认值的内置类型,理论上来说是不需要手写构造函数的,这里我们手写一个的目的是开空间。哈希表的的底层是数组,实现时我们直接使用了一个vector会更加省事。那么哈希表的大小是用vector的size还是capacity呢?答案是size,因为vector的[]访问元素的下标的只能访问有效的数据个数,即size,所以使用size作为表的大小

闭散列中负载因子设为0.7,达到0.7时则进行扩容

	HashTable(){_table.resize(10);//初始大小为10(是vector的size,不是capacity)}

查找

查找操作为其他操作的基础,所以还是很重要的;其实现步骤如下:

  1. 将查找的值通过除留余数法计算出对应的哈希地址
  2. 由于哈希冲突的原因,哈希地址上的值不一定就是查找的值,需要借助一个循环往后检测
  3. 先确认_state的状态,只有标签为EXIST的值才是存在的,再通比对K值检测是否存在,存在则返回其地址;最后找不到则返回nullptr

注意:由于是按照线性探测的方式解决哈希冲突的问题,所以冲突的值一定会在计算出的哈希地址的下一个空位置(即状态为EMPTY)。

	HashData<K, V>* Find(const K& key)//返回数据类型{Hash hs;//哈希函数取Ksize_t hashi = hs(key) % _table.size();//计算哈希地址while (_table[hashi]._state != EMPTY)//闭散列,真正的值可能在后面{//如果状态为delete,则不存在if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key){return &_table[hashi];}++hashi;hashi %= _table.size();}return nullptr;}

插入

插入步骤如下:

  1. 去重,不支持重复的元素。
  2. 检测是否需要扩容,扩容则需要将旧表中的元素重新映射到新表。
    • 扩容后可能可以缓解哈希冲突,所以需要重新映射到已经扩容的新表。
    • 这个重新映射和插入操作是一致的。
  3. 插入新元素
    1. 除留余数法计算哈希地址
    2. 检测对应位置是否已存在元素(_state == EXIST),存在则往后推,直到找到空位置
    3. 在表中插入对应元素,并把状态设为EXIST
    4. _size记得++

以下实现的一个妙点就是在扩容时,直接开个已扩好容的新表,此时将旧表的元素重新映射到新表的方法是:调用新表的Insert,也就是复用了下面的插入逻辑,最后交换新旧两表即可

bool Insert(const pair<K, V>& kv){//去重if (Find(kv.first)){return false;}//检查是否需要扩容if (_size * 10 / _table.size() >= 7)//负载因子设为0.7;{//扩容,需要重新映射。HashTable<K, V> newtable;newtable._table.resize(_table.size() * 2);//二倍扩for (const auto& e : _table)//将旧的移到新的。{if (e._state == EXIST){newtable.Insert(e._kv);//插入逻辑和下面一致}}_table.swap(newtable._table);//交换新旧两表}Hash hs;//取Ksize_t hashi = hs(kv.first) % _table.size();//除留余数法算出下标while (_table[hashi]._state == EXIST)//若对应位置已有,则往后推{++hashi;hashi %= _table.size();}_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_size;return true;}

删除

注意:上面的查找函数的返回值为K值对应节点的地址,所以可以直接借助查找函数实现删除操作。
步骤如下:

  1. 调用Find并接受其返回值
  2. 若节点存在,则直接将该节点中的_state改为DELETE即算完成删除。
  3. 记得_size- -

闭散列的哈希表的增删查操作对数据的状态的处理都是接洽的,所以可以直接使用改变状态来进行伪删除从而达到删除效果。

bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;_size--;return true;}return false;}

哈希表开散列的实现

开散列的哈希表也有称为哈希桶的

哈希表的结构

在开散列的哈希表中,哈希表的每个位置实际上存储的是一个节点,即每个哈希桶中存储的数据实际上是一个节点类型,该节点类型除了存储所给数据之外,还需要存储一个节点指针用于指向下一个节点。也可以理解为vector中的每一个元素都为一串单链表。

template<class K, class V>
struct HashNode
{pair<K, V> _kv;HashNode<K, V>* _next;HashNode(const pair<K,V>& kv):_next(nullptr),_kv(kv){}
};template<class K, class V, class Hash = HashFunc<K>>
class HashBucket
{
public://functionprivate:vector<Node*> _bucket;size_t _size;
};

开散列的哈希函数,构造函数和上面闭散列的实现是一致的。

查找

步骤:

  1. 计算得出哈希地址
  2. 查看该哈希地址上的单链表是否为空
  3. 单链表不为空,则遍历并比对K值

实现逻辑和闭散列是相同的,由于不需要状态标识符,实现起来更简单。

	Node* Find(const K& key)//返回数据类型{Hash hs;size_t hashi = hs(key) % _bucket.size();Node* cur = _bucket[hashi];while (cur)//存在则进行比对{if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;}

插入

步骤:

  1. 去重,不支持相同K值的元素
  2. 检查是否需要扩容
    • 开散列的负载因子设为1,即插入元素等于桶数时才扩容
    • 现在哈希表下挂的是单链表,扩容时直接将单链表的每一个节点重新映射到新表中的做法效率会更优,这样可以减少新表重新开节点和旧表释放节点这样多此一举的事情,直接将原有节点拿过来挂在新表上。
  3. 插入新节点,_size记得++

这里的插入为头插,尾插也行,看个人喜好。该过程比较重要的点就是在重新映射到新表时直接将旧表中的节点挂去新表,减少了重新开/释放节点的消耗。其余的就是单链表相关的操作,不熟悉的可参考——单链表的实现

bool Insert(const pair<K, V>& kv){//去重if (Find(kv.first)){return false;}//检查是否需要扩容if (_size == _bucket.size())//负载因子为1;{//扩容,需要重新映射。vector<Node*> newbucket;//此时开的是vectornewbucket.resize(_bucket.size() * 2);//扩容for (size_t i = 0; i < _bucket.size(); i++){if (_bucket[i])//非空说明有数据{Node* cur = _bucket[i];//获取当前节点Node* next = nullptr;Hash hs;size_t newhashi;while (cur){next = cur->_next;//先记录旧表的下一个节点newhashi = hs(cur->_kv.first) % newbucket.size();//计算在新表的位置cur->_next = newbucket[newhashi];//串联新表的下一节点newbucket[newhashi] = cur;//头插cur = next;//遍历旧表}}}_bucket.swap(newbucket);//交换新旧两表}//插入过程Hash hs;size_t hashi = hs(kv.first) % _bucket.size();Node* newnode = new Node(kv);//头插newnode->_next = _bucket[hashi];_bucket[hashi] = newnode;++_size;return true;}

了解过哈希表的插入之后,可以知道哈希表是无法保证表中元素是有序的,这也就是为什么unordered系列容器是无法保证容器中元素是有序的原因,因为其底层就是哈希表。无法做到像红黑树那样保证元素是有序的。

删除

步骤:

  1. 计算获取哈希地址
  2. 查找并判断节点是否为哈希表中桶的第一个节点
    • 若删除节点为桶的第一个节点,需要改动表中的节点
    • 若不是,直接让prev指向cur的next。
  3. 删除节点,_size- -
    bool Erase(const K& key){Hash hs;size_t hashi = hs(key) % _bucket.size();Node* prev = nullptr;//前一节点Node* cur = _bucket[hashi];//当前节点while (cur){if (cur->_kv.first == key){//需要检测是不是当前桶的头节点if (prev == nullptr)//为头节点{_bucket[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}

哈希表的表长建议是素数

减少冲突概率:

  • 均匀分布:当哈希表的长度(即模数)为素数时,能够更均匀地分布哈希值,减少不同键产生相同哈希值的概率,即减少冲突。这是因为素数在取模运算中具有较少的因子,使得哈希值在哈希表中分布更加均匀。
  • 避免周期性:质数作为除数可以减少哈希值的周期性,避免哈希值在某些位置上过于集中,从而进一步减少冲突

如:
假设有一个数列A: 1, 3, 5, 7, 9, 11, 13, 15,如果采用除留余数法构造哈希函数,并选取8作为模数,则这些数的哈希值将分别为1, 3, 5, 7, 1, 3, 5, 7,可以看出有明显的周期性,且由于2是8的因子,这些数之间的间隔(2)也导致了哈希值的冲突。而如果选取7作为模数(7是素数),则哈希值将分别为1, 3, 5, 0, 2, 4, 6,分布更加均匀,冲突减少

表长为8:

01234567
1357
9111315

表长为7:

0123456
719311513
15

所以为了提高哈希表的效率,在扩容时就不能简单的扩两倍了,这样的表长就不是素数了,所以提前存好一个间隔差距近似两倍的素数数组。如以下数组,其成员都为素数,且都以近似二倍的增长。

		const int PRIMECOUNT = 28;const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};

将其封为一个函数,等到扩容时再来取

    size_t GetNextPrime(size_t prime){//素数序列const int PRIMECOUNT = 28;const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};//size_t i = 0;for (size_t i = 0; i < PRIMECOUNT; i++){if (primeList[i] > prime)return primeList[i];}//return primeList[i];//理论上一定会在循环中有结果}

使用时在resize中调用即可。

		//newbucket.resize(_bucket.size() * 2);//二倍增长newbucket.resize(GetNextPrime(_bucket.size()));//素数数组

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

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

相关文章

python+selenium工具UI自动化全功能介绍(包括工具本身及配合RobotFramework框架和pytest框架应用)

文章较长&#xff0c;各位志同道合的朋友们&#xff0c;感谢关注收藏。 书山有路勤为径&#xff0c;学海无涯苦作舟。 ——韩愈&#xff0c;以山川学海比喻学习的艰辛与努力的方向。 明天的我们&#xff0c;必将会感谢昨日的自己。 1 UI自动化测试 UI自动化测试&#xff08…

长三角月度10m植被指数(NDVI) 数据集(2019-2023年)

长三角月度10m植被指数&#xff08;NDVI) 数据集&#xff08;2019-2023年&#xff09; 数据介绍 植被指数数据是区域可持续研究的重要参考指标。长江三角洲地区是我国发展最快的城市群之一&#xff0c;环境容量在城市建设的过程中被挤压&#xff0c;生态压力逐年增大。归一化植…

【Java】集合中单列集合详解(一):Collection与List

目录 引言 一、Collection接口 1.1 主要方法 1.1.1 添加元素 1.1.2 删除元素 1.1.3 清空元素 1.1.4 判断元素是否存在 1.1.5 判断是否为空 1.1.6 求取元素个数 1.2 遍历方法 1.2.1 迭代器遍历 1.2.2 增强for遍历 1.2.3 Lambda表达式遍历 1.2.4 应用场景 二、…

PostgreSQL Windows系统初始化、登录、创建用户及数据库

文章目录 PostgreSQL初始化PostgreSQL登录 PostgreSQL初始化 initdb 到安装目录下&#xff0c;找到目录E:\postgresql\bin&#xff08;自己的安装目录&#xff09;&#xff0c;在该目录下使用管理员方式打开cmd窗口。 initdb.exe -D "E:\postgresql\bin" E:\postgre…

Android系統Audio hal

一.Android系統Audio hal简介 Android系统的音频硬件抽象层(HAL)是系统与硬件之间的桥梁,允许音频应用和服务访问底层音频硬件,而无需直接与硬件交互。 主要组件: 音频 HAL 接口:定义了应用和服务如何调用音频硬件的规范。典型的音频操作包括播放、录制、音量控制等。 …

【AIGC】解锁高效GPTs:ChatGPT-Builder中系统提示词Prompt的设计与应用

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;系统提示词系统提示词的作用与重要性系统提示词在构建GPTs中的作用结论 &#x1f4af;ChatGPT-Builder系统提示词的详细解读OpenAI为Builder编写的系统提示词系统提示词对…

高质量SCI论文撰写及投稿丨论文选题、文献调研、实验设计、数据分析、论文结构及语言规范等----AI强大功能

科学研究的核心在于将复杂的思想和实验成果通过严谨的写作有效地传递给学术界和工业界。对于研究生、青年学者及科研人员&#xff0c;如何高效撰写和发表SCI论文&#xff0c;成为提升学术水平和科研成果的重要环节。系统掌握从选题到投稿的全过程&#xff0c;提高论文撰写效率与…

在Openshift(K8S)上通过EMQX Operator部署Emqx集群

EMQX Operator 简介 EMQX Broker/Enterprise 是一个云原生的 MQTT 消息中间件。 我们提供了 EMQX Kubernetes Operator 来帮助您在 Kubernetes 的环境上快速创建和管理 EMQX Broker/Enterprise 集群。 它可以大大简化部署和管理 EMQX 集群的流程&#xff0c;对于管理和配置的知…

医护人员排班|基于springBoot的医护人员排班系统设计与实现(附项目源码+论文+数据库)

私信或留言即免费送开题报告和任务书&#xff08;可指定任意题目&#xff09; 目录 一、摘要 二、相关技术 三、系统设计 四、数据库设计 五、核心代码 六、论文参考 七、源码获取 一、摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息…

B树的原理与CPP实现

B树是一种自平衡的多叉树数据结构&#xff0c;广泛应用于数据库系统和文件系统中。其设计初衷是为了在存储设备上实现高效的读写操作&#xff0c;特别是在磁盘存储或其他大规模存储场景下。B树的每个节点可以有多个子节点&#xff0c;这与二叉树不同&#xff0c;B树能有效减少树…

深入学习二叉树(BinaryTree)(纯小白进)

目录&#xff1a; 一、 前言二、 正文2.1、 树的概念2.1.1、 树的结构2.1.2、 树的小知识 2.2、 认识二叉树2.2.1、 二叉树的概念2.2.2、 特殊的二叉树 2.3、 实现二叉树2.3.1、 结构2.3.2、 节点数2.3.3、 树深度2.3.4、 前、中、后序遍历 销毁2.3.4.1、 前序遍历2.3.4.2、 中…

《数据结构》课程综合设计(zzu校园导航)(迪杰斯特拉算法)

一、系统&#xff08;问题&#xff09;描述 目前根据郑州大学主校区面积区域的广大&#xff0c;以及南、北核心教学楼的教室分布密集且较多&#xff1b;另外&#xff0c;多数地图软件无法精细导航到一个具体的地点&#xff0c;容易造成原地转圈的烦恼。但是&#xff0c;我们转…

js中map,filter,find,foreach的用法介绍

js中map&#xff0c;filter&#xff0c;find&#xff0c;foreach的用法介绍 在 JavaScript 中&#xff0c;数组提供了一些常用的迭代方法&#xff0c;如 map、filter、find 和 forEach&#xff0c;这些方法允许你对数组中的每个元素进行操作&#xff0c;下面是它们的用法和区别…

Python爬虫实战:利用青果代理IP获取跨境电商数据

文章目录 一、跨境电商数据的作用1.1 市场趋势预测与洞察1.2 消费者行为分析1.3 库存管理优化1.4 定价策略制定 二、爬取目标三、环境准备四、代理IP获取4.1 为什么爬虫要用代理IP&#xff1f;4.2 为什么选择青果代理IP&#xff1f;4.3 青果代理IP领取4.4 利用代码获取IP 五、爬…

excel 表格中url转图片

待处理的单元格通过如下公式获取目标格式&#xff1a; "<table><img src"&A4&" height20></table>" 然后下拉后获取多列的单元格转换结果&#xff0c; 然后将这些转换后的结果拷贝到纯文本文档中&#xff0c; 然后再将纯文本…

新基建下的园区智慧化变革 | 科技驱动未来开放式智慧园区

在数智化浪潮席卷之下&#xff0c;千行百业的数字化转型步伐加快。智慧园区建设借助创新的数字化、智能化技术&#xff0c;对园区内的人、机、物、事进行建模和重构&#xff0c;克服传统园区数据割裂、分散管理、无集成、体验差的问题&#xff0c;构建更智慧的管理方式、服务体…

unity学习-雾的渲染

在Light面板下的Other Settings中勾选fog就会让场景中生成雾气 Coloer&#xff1a;颜色 Mode&#xff1a;预设 Density&#xff1a;密度 当Mode调整为Linear模式会多出两个选项 Start&#xff1a;往前从多少米开始 End&#xff1a;到多少米有雾气 Start&#xff1a;设置…

[单master节点k8s部署]41.部署springcloud项目

在之前的文章中我们配置了mysql和harbor&#xff0c;现在我们可以将一个springcloud部署在k8s集群中了。 项目概述 这个springcloud项目将采用maven进行打包部署。首先安装maven&#xff1a; yum install java-1.8.0-openjdk maven-3.0.5* -y 然后将该项目上传到k8s集群的m…

c#编写的各类应用程序

001 课程简介&#xff0c;C# 语言简介&#xff0c;开发环境准备 (yuque.com)https://www.yuque.com/yuejiangliu/dotnet/timothy-csharp-001 一个Solution里包含多个Project 一、见识 C# 编写的各类应用程序 二、类库的引用&#xff08;黑/白盒引用&#xff09; 1、黑盒引用&a…

C++从入门到起飞之——(multi)set与(multi)map的的使用 全方位剖析!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C从入门到起飞 &#x1f516;克心守己&#xff0c;律己则安 目录 1. 序列式容器和关联式容器 2. set系列的使⽤ 2.1 set和multiset参考⽂档 2.2 set类的介绍 2.3 se…