哈希 :哈希冲突、负载因子、哈希函数、哈希表、哈希桶

文章目录

  • 哈希
  • 哈希(散列)函数
    • 常见的哈希函数
    • 字符串哈希函数
  • 哈希冲突
    • 闭散列(开放地址法)
    • 开散列(链地址法/拉链法)
    • 负载因子以及增容
      • 对于闭散列
    • 对于开散列结构
  • 具体实现
    • 哈希表(闭散列)
      • 创建
      • 插入
      • 查找
      • 删除
      • 完整代码
    • 哈希桶(开散列)
      • 插入
      • 查找
      • 删除
      • 完整代码


哈希

哈希(散列)是一种数据结构,通过散列算法将元素值转换为散列值进行存储。使得元素存储的位置与元素本身建立起了映射关系,如果要查、改数据,就可以直接到对应的位置去,使得时间复杂度达到了 O(1) ,原因如下:

若结构中存在和 关键字K 相等的记录,则必定在 f(K) 的存储位置上。由此,不需比较便可直接取得所查记录。

称上面的对应关系 f散列函数(Hash function),按这个映射关系事先建立的表为散列表,这一映象过程称为 散列造表散列 ,最终所得的存储位置称 散列地址


哈希(散列)函数

哈希函数用来建立元素与其存储位置的映射关系。

对于哈希函数来说,必须具有以下特点:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0m-1 之间。
  • 哈希函数计算出来的地址能均匀分布在整个空间中(防止产生密集的哈希冲突)

哈希冲突大量出现往往都是因为哈希函数设计的不够合理,但是即使再优秀的哈希函数,也只能尽量减少哈希冲突的次数,无法避免哈希冲突。


常见的哈希函数

  1. 直接定址法(常见)
    哈希函数:Hash(Key) = A * Key + B
    这是最简单的哈希函数,直接取关键字本身或者他的线性函数来作为散列地址。

  2. 除留余数法(常见)
    哈希函数 :Hash(key) = key % capacity
    几乎是最常用的哈希函数,用一个数来对 key 取模,一般来说这个数都是容量。

  3. 平方取中法
    对关键字进行平方,然后取中间的几位来作为地址。

  4. 折叠法
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和(去除进位),并按散列表表长,取后几位作为散列地址。
    折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况.

  5. 随机数法
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key),其中 random 为随机数函数。通常应用于关键字长度不等时。

  6. 数学分析法
    分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。


字符串哈希函数

因为哈希函数的常用方法如直接定址、除留余数、平方取中等方法需要用的 key值整型,而大部分时候我们的 key 都是 string,由于无法对 string 进行算数运算,所以需要考虑新的方法。

常见的字符串哈希算法有 BKDSDBRS 等,这些算法大多通过一些公式来对字符串每一个 字符的 ascii值 或者 字符串的大小 进行计算,来推导出一个不容易产生冲突的 key值

例如 BKDHash

struct _Hash<std::string>
{const size_t& operator()(const std::string& key){// BKDR字符串哈希函数size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 131;hash += key[i];}return hash;}
};

这里推荐两篇文章,一篇具体对比各类字符串哈希函数的效率,一篇是实现:

  • 字符串Hash函数对比
  • 各种字符串Hash函数

哈希冲突

对不同的关键字可能得到同一散列地址,即 key1≠key2 ,而 f(key1)=f(key2) ,这种现象称碰撞(哈希冲突)

哈希冲突使得多个数据映射的位置相同,但是每个位置又只能存储一个数据,所以就需要通过某种方法来解决哈希冲突。

查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高;产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:

  1. 散列函数是否均匀;
  2. 处理冲突的方法;
  3. 散列表的负载因子(装填因子)。

第一点取决于上面讲过的哈希函数,不再赘述,下面详细讲一下2、3点。

处理冲突的方法可分为两类:开散列方法( open hashing,也称为拉链法,separate chaining )闭散列方法( closed hashing,也称为开地址方法,open addressing )。这两种方法的不同之处在于:开散列法把发生冲突的关键码存储在散列表主表之外,而闭散列法把发生冲突的关键码存储在表中另一个槽内。


闭散列(开放地址法)

因为闭散列是顺序的结构,所以可以通过遍历哈希表,来将冲突的数据放到空的位置上。

  1. 线性探测
    线性探测即为从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
    这种方法实现起来极为简单,但是效率也不高,因为如果同一位置产生了大量的哈希冲突,就会导致每次都在同一个位置进行探测,例如我在10这里连续冲突100次,此时所有探测的次数加起来就会高达100!

  2. 二次探测
    二次探测即为从发生冲突的位置开始,每次往后探测 ±k2(k<=m/2,m为散列表长) 个位置,如:12,-(12),22,-(22) 等。
    这样的话就将每次探测的效率从 O(N) 提升到了 O(logN) ,即使有着大量的冲突堆积,也不会导致效率过低。

  3. 伪随机数探测
    这种方法并不常见,实现方法是:创建一个伪随机数序列,根据序列内容决定每次往后探测的长度。


开散列(链地址法/拉链法)

先用哈希函数计算每个数据的散列地址,把具有相同地址的元素归于同一个集合之中,把该集合处理为一个链表,链表的头节点存储于哈希表之中。

在这里插入图片描述

链地址法在每一个映射位置都建立起一个链表(数据过多时可能会转为建立红黑树),将每次插入的数据都直接连接上这个链表,这样就不会像闭散列一样进行大量的探测,但是如果链表过长也会导致效率低下。


负载因子以及增容

哈希冲突出现的较为密集,往往代表着此时数据过多,而能够映射的地址过少,而要想解决这个问题,就需要通过 负载因子(装填因子) 的判断来进行增容。

负载因子的大小 = 表中数据个数 / 表的容量(长度)

对于闭散列

对于闭散列来说,因为其是一种线性的结构,所以一旦负载因子过高,就很容易出现哈希冲突的堆积,所以当负载因子达到一定程度时就需要进行增容,并且增容后,为了保证映射关系,还需要将数据重新映射到新位置。

经过算法科学家的计算, 负载因子应当严格的控制在 0.7-0.8 以下,所以一旦负载因子到达这个范围,就需要进行增容。

因为除留余数法等方法通常是按照表的容量来计算,所以科学家的计算,当对一个质数取模时,冲突的几率会大大的降低,并且因为增容的区间一般是 1.5-2 倍,所以算法科学家列出了一个增容质数表,按照这样的规律增容,冲突的几率会大大的降低。

这也是 STLunordered_map/unordered_set 使用的增容方法。

//算法科学家总结出的一个增容质数表,按照这样增容的效率更高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};

hashmap 的负载因子为什么默认是 0.75

比如说当前的容器容量是 16,负载因子是 0.75,16*0.75=12,也就是说,当容量达到了 12 的时候就会进行扩容操作。而负载因子定义为 0.75 的原因是:

  • 当负载因子是 1.0 的时候,也就意味着,只有当散列地址全部填充了,才会发生扩容。意味着随着数据增长,最后势必会出现大量的冲突,底层的红黑树变得异常复杂。虽然空间利用率上去了,但是查询时间效率降低了。
  • 负载因子是 0.5 的时候,这也就意味着,当数组中的元素达到了一半就开始扩容。虽然时间效率提升了,但是空间利用率降低了。 诚然,填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。但是,这时候空间利用率就会大大的降低,原本存储 1M 的数据,现在就意味着需要 2M 的空间。

对于开散列结构

因为哈希桶是开散列的链式结构,发生了哈希冲突是直接在对应位置位置进行头插,而桶的个数是固定的,而插入的数据会不断增多,随着数据的增多,就可能会导致某一个桶过重,使得效率过低。

所以最理想的情况,就是每个桶都有一个数据。这种情况下,如果往任何一个地方插入,都会产生哈希冲突,所以当数据个数与桶的个数相同时,也就是负载因子为 1 时就需要进行扩容。


具体实现

哈希表(闭散列)

创建

对于闭散列,我们需要通过状态来记录一个数据是否在表中,所以这里会使用枚举来实现。

enum State
{EMPTY,//空EXITS,//存在DELETE,//已经删除
};template<class T>
struct HashData
{HashData(const T& data = T(), const State& state = EMPTY): _data(data), _state(state){}T _data;State _state;
};

插入

插入的思路很简单,计算出映射的地址后,开始遍历判断下面几种状态:

  • 如果映射位置已存在数据,并且值与当前数据不同,则说明产生冲突,继续往后查找
  • 如果映射位置的数据与插入的数据相同,则说明此时数据已经插入过,此时就不需要再次插入
  • 如果映射位置的状态为删除或者空,则代表着此时表中没有这个数据,在这个位置插入即可
bool Insert(const T& data)
{KeyOfT koft;//判断此时是否需要增容//当装填因子大于0.7时增容if (_size * 10 / _table.size() >= 7){//增容的大小按照别人算好的近似两倍的素数来增,这样效率更高,也可以直接2倍或者1.5倍。std::vector<HashData> newTable(getNextPrime(_size));for (size_t i = 0; i < _table.size(); i++){//将旧表中的数据全部重新映射到新表中if (_table[i]._state == EXITS){//如果产生冲突,则找到一个合适的位置size_t index = HashFunc(koft(_table[i]._data));while (newTable[i]._state == EXITS){i++;if (i == _table.size()){i = 0;}}newTable[i] = _table[i];}}//最后直接将数据进行交换即可,原来的数据会随着函数栈帧一起销毁_table.swap(newTable);}//用哈希函数计算出映射的位置size_t index = HashFunc(koft(data));//从那个位置开始探测, 如果该位置已经存在时,有两种情况,一种是已经存在,一种是冲突,这里使用的是线性探测while (_table[index]._state == EXITS){//如果已经存在了,则说明不用插入if (koft(_table[index]._data) == koft(data)){return false;}else{index++;index = HashFunc(index);}}//如果走到这里,说明这个位置是空的或者已经被删除的位置,可以在这里插入_table[index]._data = data;_table[index]._state = EXITS;_size++;return true;
}

查找

查找也分几种情况

  • 如果映射的位置为空,则说明查找失败。
  • 如果映射的位置的数据不同,则说明产生冲突,继续向后查找
  • 如果映射的位置的数据相同,如果状态为删除,则说明数据已经删除,查找失败;而如果数据为存在,则说明查找成功。
HashData* Find(const K& key)
{KeyOfT koft;size_t index = HashFunc(key);//遍历,如果查找的位置为空,则说明查找失败while (_table[index]._state != EMPTY){//此时判断这个位置的数据是否相同,如果不同则说明出现哈希冲突,继续往后查找if (koft(_table[index]._data) == key){//此时有两个状态,一种是数据已经被删除,一种是数据存在。if (_table[index]._state == EXITS){return &_table[index];}else if (_table[index]._state == DELETE){return nullptr;}}index++;//如果index越界,则归零if (index == _table.size()){index = 0;}}return nullptr;
}

删除

直接遍历查找数据,如果找不到则说明已经被删除,如果找到了则直接将状态改为删除即可。

bool Erase(const K& key)
{HashData* del = Find(key);//如果找不到则说明已经被删除if (del == nullptr){return false;}else{//找到了则直接更改状态即可del->_state = DELETE;_size--;return true;}
}

完整代码

#pragma once
#include<vector>namespace lee
{//算法科学家总结出的一个增容质数表,按照这样增容的效率更高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};enum State{EMPTY,EXITS,DELETE,};template<class T>struct HashData{HashData(const T& data = T(), const State& state = EMPTY): _data(data), _state(state){}T _data;State _state;};template<class K, class T, class KeyOfT>class HashTable{public:typedef HashData<T> HashData;HashTable(size_t capacity = 10): _table(capacity), _size(0){}size_t getNextPrime(size_t num){size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){//返回比那个数大的下一个质数 if (primeList[i] > num){return primeList[i];}}//如果比所有都大,还是返回最后一个,因为最后一个已经是32位最大容量return primeList[PRIMECOUNT - 1];}//除留余数法size_t HashFunc(const K& key){return key % _table.size();}bool Insert(const T& data){KeyOfT koft;//判断此时是否需要增容//当装填因子大于0.7时增容if (_size * 10 / _table.size() >= 7){//增容的大小按照别人算好的近似两倍的素数来增,这样效率更高,也可以直接2倍或者1.5倍。std::vector<HashData> newTable(getNextPrime(_size));for (size_t i = 0; i < _table.size(); i++){//将旧表中的数据全部重新映射到新表中if (_table[i]._state == EXITS){//如果产生冲突,则找到一个合适的位置size_t index = HashFunc(koft(_table[i]._data));while (newTable[i]._state == EXITS){i++;if (i == _table.size()){i = 0;}}newTable[i] = _table[i];}}//最后直接将数据进行交换即可,原来的数据会随着函数栈帧一起销毁_table.swap(newTable);}//用哈希函数计算出映射的位置size_t index = HashFunc(koft(data));//从那个位置开始探测, 如果该位置已经存在时,有两种情况,一种是已经存在,一种是冲突,这里使用的是线性探测while (_table[index]._state == EXITS){//如果已经存在了,则说明不用插入if (koft(_table[index]._data) == koft(data)){return false;}else{index++;index = HashFunc(index);}}//如果走到这里,说明这个位置是空的或者已经被删除的位置,可以在这里插入_table[index]._data = data;_table[index]._state = EXITS;_size++;return true;}HashData* Find(const K& key){KeyOfT koft;size_t index = HashFunc(key);//遍历,如果查找的位置为空,则说明查找失败while (_table[index]._state != EMPTY){//此时判断这个位置的数据是否相同,如果不同则说明出现哈希冲突,继续往后查找if (koft(_table[index]._data) == key){//此时有两个状态,一种是数据已经被删除,一种是数据存在。if (_table[index]._state == EXITS){return &_table[index];}else if (_table[index]._state == DELETE){return nullptr;}}index++;//如果index越界,则归零if (index == _table.size()){index = 0;}}return nullptr;}bool Erase(const K& key){HashData* del = Find(key);//如果找不到则说明已经被删除if (del == nullptr){return false;}else{//找到了则直接更改状态即可del->_state = DELETE;_size--;return true;}}private:std::vector<HashData> _table;size_t _size;};
};

哈希桶(开散列)

开散列也叫哈希桶,桶为每一个映射的位置,桶一般用链表或者红黑树实现(这里我用的是链表)。当我们通过映射的地址,找到存放数据的桶,再对桶进行插入或者删除操作即可。

插入

通过计算映射位置找到对应的桶,再判断数据是否存在后将数据头插进去即可(也可以尾插)

bool Insert(const T& data)
{KeyofT koft;/*因为哈希桶是开散列的链式结构,发生了哈希冲突是直接在对应位置位置进行头插,而桶的个数是固定的,而插入的数据会不断增多,随着数据的增多,就可能会导致某一个桶过重,使得效率过低。所以最理想的情况,就是每个桶都有一个数据。这种情况下,如果往任何一个地方插入,都会产生哈希冲突,所以当数据个数与桶的个数相同时,也就是负载因子为1时就需要进行扩容。*/if (_size == _table.size()){//按照素数表来增容size_t newSize = getNextPrime(_table.size());size_t oldSize = _table.size();std::vector<Node*> newTable(newSize);_table.resize(newSize);//接着将数据重新映射过去for (size_t i = 0; i < oldSize; i++){Node* cur = _table[i];while (cur){//重新计算映射的位置size_t pos = HashFunc(koft(cur->_data));//找到位置后头插进对应位置Node* next = cur->_next;cur->_next = newTable[pos];newTable[pos] = cur;cur = next;}//原数据置空_table[i] == nullptr;}//直接和新表交换,交换过去的旧表会和函数栈帧一块销毁。_table.swap(newTable);}size_t pos = HashFunc(koft(data));Node* cur = _table[pos];//因为哈希桶key值唯一,如果已经在桶中则返回falsewhile (cur){if (koft(cur->_data) == koft(data)){return false;}else{cur = cur->_next;}}//检查完成,此时开始插入,这里选择的是头插,这样就可以减少数据遍历的次数。Node* newNode = new Node(data);newNode->_next = _table[pos];_table[pos] = newNode;++_size;return true;
}

查找

直接根据映射的位置到桶中查找数据即可

Node* Find(const K& key)
{KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];while (cur){if (koft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr;
}

删除

bool Erase(const K& key)
{KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];Node* prev = nullptr;while (cur){if (koft(cur->_data) == key){//如果要删除的是第一个节点,就让下一个节点成为新的头节点,否则直接删除。if (prev == nullptr){_table[pos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_size;return true;}else{prev = cur;cur = cur->_next;}}return false;
}

完整代码

#pragma once
#include<vector>
#include<string>namespace lee
{//算法科学家总结出的一个增容质数表,按照这样增容的效率更高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};/*因为哈希函数的常用方法如直接定地、除留余数、平方取中等方法需要用的key值为整型,而大部分时候我们的key都是string,或者某些自定义类型,这个时候就可以提供一个仿函数的接口给外部,让他自己处理如何将key转换成我们需要的整型*/template<class K>struct Hash{const K& operator()(const K& key){return key;}};template<>struct Hash<std::string>{const size_t & operator()(const std::string& key){//BKDR字符串哈希函数size_t hash = 0;for (size_t i = 0; i < key.size(); i++){hash *= 131;hash += key[i];}return hash;}};template<class T>struct HashNode{HashNode(const T& data = T()): _data(data), _next(nullptr){}T _data;HashNode<T>* _next;};template<class K, class T, class KeyofT, class Hash = Hash<K>>class HashBucket{public:typedef HashNode<T> Node;HashBucket(size_t capacity = 10): _table(capacity), _size(0){}~HashBucket(){Clear();}size_t getNextPrime(size_t num){size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){//返回比那个数大的下一个质数 if (primeList[i] > num){return primeList[i];}}//如果比所有都大,还是返回最后一个,因为最后一个已经是32位最大容量return primeList[PRIMECOUNT - 1];}size_t HashFunc(const K& key){Hash hash;return hash(key) % _table.size();}bool Insert(const T& data){KeyofT koft;/*因为哈希桶是开散列的链式结构,发生了哈希冲突是直接在对应位置位置进行头插,而桶的个数是固定的,而插入的数据会不断增多,随着数据的增多,就可能会导致某一个桶过重,使得效率过低。所以最理想的情况,就是每个桶都有一个数据。这种情况下,如果往任何一个地方插入,都会产生哈希冲突,所以当数据个数与桶的个数相同时,也就是负载因子为1时就需要进行扩容。*/if (_size == _table.size()){//按照素数表来增容size_t newSize = getNextPrime(_table.size());size_t oldSize = _table.size();std::vector<Node*> newTable(newSize);_table.resize(newSize);//接着将数据重新映射过去for (size_t i = 0; i < oldSize; i++){Node* cur = _table[i];while (cur){//重新计算映射的位置size_t pos = HashFunc(koft(cur->_data));//找到位置后头插进对应位置Node* next = cur->_next;cur->_next = newTable[pos];newTable[pos] = cur;cur = next;}//原数据置空_table[i] == nullptr;}//直接和新表交换,交换过去的旧表会和函数栈帧一块销毁。_table.swap(newTable);}size_t pos = HashFunc(koft(data));Node* cur = _table[pos];//因为哈希桶key值唯一,如果已经在桶中则返回falsewhile (cur){if (koft(cur->_data) == koft(data)){return false;}else{cur = cur->_next;}}//检查完成,此时开始插入,这里选择的是头插,这样就可以减少数据遍历的次数。Node* newNode = new Node(data);newNode->_next = _table[pos];_table[pos] = newNode;++_size;return true;}bool Erase(const K& key){KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];Node* prev = nullptr;while (cur){if (koft(cur->_data) == key){//如果要删除的是第一个节点,就让下一个节点成为新的头节点,否则直接删除。if (prev == nullptr){_table[pos] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_size;return true;}else{prev = cur;cur = cur->_next;}}return false;}Node* Find(const K& key){KeyofT koft;size_t pos = HashFunc(key);Node* cur = _table[pos];while (cur){if (koft(cur->_data) == key){return cur;}else{cur = cur->_next;}}return nullptr;}void Clear(){//删除所有节点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;}}private:std::vector<Node*> _table;size_t _size;};
};

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

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

相关文章

C++ 泛型编程(一):模板基础:函数模板、类模板、模板推演成函数的机制、模板实例化、模板匹配规则

文章目录泛型编程函数模板函数模板实例化隐式实例化显式实例化函数模板的匹配规则类模板类模板的实例化泛型编程 泛型编程旨在削减重复工作&#xff0c;如&#xff1a; 将一个函数多次重载不如将他写成泛型。 void Swap(int& left, int& right) {int temp left;lef…

你真的了解静态变量、常量的存储位置吗?

文章目录引言C对内存的划分如何落实在Linux上自由存储区和堆之间的问题栈常量区静态存储区静态局部变量静态局部变量、静态全局变量、全局变量的异同macOS系统的测试结果总结引言 在动态内存的博客中&#xff0c;我提到&#xff1a; 在Linux 内存管理的博客中&#xff0c;我提…

C++ 泛型编程(二):非类型模板参数,模板特化,模板的分离编译

文章目录非类型模板参数函数模板的特化类模板的特化全特化偏特化部分参数特化参数修饰特化模板分离编译解决方法非类型模板参数 模板的参数分为两种&#xff1a; 类型参数&#xff1a; 则是我们通常使用的方式&#xff0c;就是在模板的参数列表中在 class 后面加上参数的类型…

数据结构 | B树、B+树、B*树

文章目录搜索结构B树B树的插入B树的遍历B树的性能B树B树的插入B树的遍历B*树B*树的插入总结搜索结构 如果我们有大量的数据需要永久存储&#xff0c;就需要存储到硬盘之中。但是硬盘的访问速度远远小于内存&#xff0c;并且由于数据量过大&#xff0c;无法一次性加载到内存中。…

MySQL 索引 :哈希索引、B+树索引、全文索引

文章目录索引引言常见的索引哈希索引自适应哈希索引B树索引聚集索引非聚集索引使用方法联合索引最左前缀匹配规则覆盖索引全文索引使用方法索引 引言 为什么需要索引&#xff1f; 倘若不使用索引&#xff0c;查找数据时&#xff0c;MySQL必须遍历整个表。而表越大&#xff0c;…

服装店怎么引流和吸引顾客 服装店铺收银系统来配合

实体店的同城引流和经营是实体经济的一个重要的一环&#xff0c;今天我们来分享服装行业的实体店铺怎么引流和吸引、留住顾客&#xff0c;并实现复购。大家点个收藏&#xff0c;不然划走就再也找不到了&#xff0c;另外可以点个关注&#xff0c;下次有新的更好的招&#xff0c;…

MySQL 锁的相关知识 | lock与latch、锁的类型、简谈MVCC、锁算法、死锁、锁升级

文章目录lock与latch锁的类型MVCC一致性非锁定读&#xff08;快照读&#xff09;一致性锁定读&#xff08;当前读&#xff09;锁算法死锁锁升级lock与latch 在了解数据库锁之前&#xff0c;首先就要区分开 lock 和 latch。在数据库中&#xff0c;lock 和 latch 虽然都是锁&…

MySQL 存储引擎 | MyISAM 与 InnoDB

文章目录概念innodb引擎的4大特性索引结构InnoDBMyISAM区别表级锁和行级锁概念 MyISAM 是 MySQL 的默认数据库引擎&#xff08;5.5版之前&#xff09;&#xff0c;但因为不支持事务处理而被 InnoDB 替代。 然而事物都是有两面性的&#xff0c;InnoDB 支持事务处理也会带来一些…

MySQL 事务 | ACID、四种隔离级别、并发带来的隔离问题、事务的使用与实现

文章目录事务ACID并发带来的隔离问题幻读&#xff08;虚读&#xff09;不可重复读脏读丢失更新隔离级别Read Uncommitted (读未提交)Read Committed (读已提交)Repeatable Read (可重复读)Serializable (可串行化)事务的使用事务的实现Redoundo事务 事务指逻辑上的一组操作。 …

MySQL 备份与主从复制

文章目录备份主从复制主从复制的作用备份 根据备份方法的不同&#xff0c;备份可划分为以下几种类型&#xff1a; 热备(Hot Backup) &#xff1a; 热备指的是在数据库运行的时候直接备份&#xff0c;并且对正在运行的数据库毫无影响&#xff0c;这种方法在 MySQL 官方手册中又…

C++ 流的操作 | 初识IO类、文件流、string流的使用

文章目录前言IO头文件iostreamfstreamsstream流的使用不能拷贝或对 IO对象 赋值条件状态与 iostate 类型输出缓冲区文件流fstream类型文件模式文件光标函数tellg() / tellp()seekg() / seekp()向文件存储内容/读取文件内容string流istringstreamostringstream前言 我们在使用 …

C++ 右值引用 | 左值、右值、move、移动语义、引用限定符

文章目录C11为什么引入右值&#xff1f;区分左值引用、右值引用move移动语义移动构造函数移动赋值运算符合成的移动操作小结引用限定符规定this是左值or右值引用限定符与重载C11为什么引入右值&#xff1f; C11引入了一个扩展内存的方法——移动而非拷贝&#xff0c;移动较之拷…

且谈关于最近软件测试的面试

前段时间有新的产品需要招人&#xff0c;安排和参加了好几次面试&#xff0c;下面就谈谈具体的面试问题&#xff0c;在面试他人的同时也面试自己。 面试问题是参与面试同事各自设计的&#xff0c;我也不清楚其他同事的题目&#xff0c;就谈谈自己设计的其中2道题。 过去面试总是…

C++ 多态 | 虚函数、抽象类、虚函数表

文章目录多态虚函数重写重定义&#xff08;参数不同&#xff09;协变&#xff08;返回值不同&#xff09;析构函数重写&#xff08;函数名不同&#xff09;final和override重载、重写、重定义抽象类多态的原理虚函数常见问题解析虚函数表多态 一种事物&#xff0c;多种形态。换…

C++ 运算符重载(一) | 输入/输出,相等/不等,复合赋值,下标,自增/自减,成员访问运算符

文章目录输出运算符<<输入运算符>>相等/不等运算符复合赋值运算符下标运算符自增/自减运算符成员访问运算符输出运算符<< 通常情况下&#xff0c;输出运算符的第一个形参是一个 非常量ostream对象的引用 。之所以 ostream 是非常量是因为向流写入内容会改变…

C++ 重载函数调用运算符 | 再探lambda,函数对象,可调用对象

文章目录重载函数调用运算符lambdalambda等价于函数对象lambda等价于类标准库函数对象可调用对象与function可调用对象function函数重载与function重载函数调用运算符 函数调用运算符必须是成员函数。 一个类可以定义多个不同版本的调用运算符&#xff0c;互相之间应该在参数数…

C++ 运算符重载(二) | 类型转换运算符,二义性问题

文章目录类型转换运算符概念避免过度使用类型转换函数解决上述问题的方法转换为 bool显式的类型转换运算符类型转换二义性重载函数与类型转换结合导致的二义性重载运算符与类型转换结合导致的二义性类型转换运算符 概念 类型转换运算符&#xff08;conversion operator&#…

分布式理论:CAP、BASE | 分布式存储与一致性哈希

文章目录分布式理论CAP定理BASE理论分布式存储与一致性哈希简单哈希一致性哈希虚拟节点分布式理论 CAP定理 一致性&#xff08;Consistency&#xff09;&#xff1a; 在分布式系统中的所有数据副本&#xff0c;在同一时刻是否一致&#xff08;所有节点访问同一份最新的数据副…

分布式系统概念 | 分布式事务:2PC、3PC、本地消息表

文章目录分布式事务2PC&#xff08;二阶段提交协议&#xff09;执行流程优缺点3PC&#xff08;三阶段提交协议&#xff09;执行流程优缺点本地消息表&#xff08;异步确保&#xff09;分布式事务 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分…

数据结构算法 | 单调栈

文章目录算法概述题目下一个更大的元素 I思路代码下一个更大元素 II思路代码132 模式思路代码接雨水思路算法概述 当题目出现 「找到最近一个比其大的元素」 的字眼时&#xff0c;自然会想到 「单调栈」 。——三叶姐 单调栈以严格递增or递减的规则将无序的数列进行选择性排序…