C++ - 哈希

   在顺序结构以及平衡树中,由于元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较;比如顺序表中需要从表头开始依次往后比对寻找,查找时间复杂度为 O(N),平衡树中需要从第一层开始逐层往下比对寻找,查找时间复杂度为 O(logN);即搜索的效率取决于搜索过程中元素的比较次数。

   尽管平衡树的查找方式已经很快了,但我们仍然认为该方法不够极致,我们最理想的查找方式:不像二叉树那样经过层层比较,能够用O(1)的时间复杂度直接查询到我们需要的元素。

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

一.哈希思想

 1.1 哈希思想的概念 

    构造一种存储结构我们通过某种方法使元素的存储位置与其查找的元素建立某种映射关系,从而利用O(1)的时间复杂度一次性查找到我们需要的元素,这就是哈希思想。

   当向该结构中:

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

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

  ps:我们上面提到的不管是顺序搜索、平衡树搜索还是哈希搜索,其 key 值都是唯一的,也就是说,搜索树中不允许出现相同 key 值的节点,哈希表中也不允许出现相同 key 值的元素,我们下文所进行的所有操作也都是在这前提之上进行的。

假设  数据集合 {1 , 7 , 6 , 4 , 5 , 9} ; 存储空间为10

哈希函数设置为: hash(key)   = key % capacity ;

capacity 为存储元素底层空间总的大小。

 

我们通过 查找元素与下标关系 构建一个数组 ,当我们查找1 时,可以直接定位到下标1这个位置,实现O(1)时间内的查找。 

 1.2 哈希函数

哈希函数有如下设计原则

  1. 哈希函数应该要满足待插入的所有元素使用,其值域如果在0到m-1之间,那么他就必须有m个空间。
  2. 哈希函数计算出来的地址要尽量能均匀分布在整个空间中;
  3. 哈希函数应该比较简单

我们有几个比较常见的哈希函数:
1.2.1 直接定址法 

直接定址法是最简单的哈希函数,顾名思义,直接定址就是根据 key 值直接得到存储位置,最多再进行一个简单的常数之间的转换,其定义如下:

Hash(Key)= A*Key + B (A B 均为常数)

 这个如果不懂,假设A=0,B=0,Key为常数,即根据Key值直接确定存储位置。

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

直接定址法不适用于数据范围分散的情况,因为这样会导致哈希表的空间利用率很低,会浪费很多空间 

比如:int arr[] = { 123, 126, 125, 138, 122331, 1}; 假设A,B都为0

 Hash(1)= 1;

Hash(12231)=12231;

那么我们根据哈希函数的定义,至少要用12231个int大小的数组来建立哈希表,那么它就会占用很多存储空间。

1.2.2 除留余数法 (最常用)

  为了应对数据范围分散的情况,有人设计出了除留余数法 – 设哈希表中允许的地址数为m,取一个不大于m,但最接近或者等于m的素数p作为除数。

Hash(key) = key % p (p<=m)

  简单来说就是用 key 值除以哈希表的大小得到的余数作为哈希映射的地址,将 key 保存到该地址中;除留余数的优点是可以处理数据范围分散的数据,缺点是会引发哈希冲突(下文会提及).

例如对于数据集合 {1,7,6,4,5,9},存储空间为10 ,它的哈希表如下:

ps: 接下来我们在文章中如果提到哈希函数,默认为除留余数法。 

1.3 哈希冲突

  如何有两个元素 x!=y,但是 hash(x)==hash(y),这种明明不同的元素,但是经过哈希函数计算后得到的结果一致,我们就将这种情况称为哈希冲突。

哈希冲突有两种常见的解决办法:

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

  
  开散列 (链地址法):首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码 (哈希冲突) 归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中;也就是说,当发生哈希冲突时,把 key 直接链接在该位置的下面。

假设我们在 插入一个11和21 :

二、闭散列法的哈希表

  闭散列也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的 “下一个” 空位置中去;那如何寻找下一个空位置呢?有两种方法 – 线性探测法(常用)和二次探测法。

2.1线性探测法

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

插入11和21后: 

2.2 哈希表的基本框架

//通过空和存在判断插入,通过删除状态判断查找
enum State {EXIST,EMPTY,DELETE
};template<class K,class V>
struct HashData {pair<K, V> _kv; //每个节点存储KV结构State _state = EMPTY;  //默认为空
};template<class K, class V>
class HashTable {typedef HashData<K, V> Data;HashTable(): _n(0){//将哈希表的大小默认给为10_tables.resize(10);}
private://把哈希函数封装一下size_t Hashifunction(const K& key){//这里我们采用除留余数法return key% _tables.size();}
private:vector<Data> _tables;size_t _n;  //记录表中有效数据的个数
};

  如上,为了方便,在哈希表中我们使用了 vector 来存储数据,并增加了一个变量 n 来记录表中有效数据的个数,这是我们哈希表的底层实现。

 同时,我们在哈希表的每个位置的数据中还增加了一个 state 变量来记录该位置的状态,但为什么要有三个变量呢?我们不是只需要存在或者不存在不就足够了吗?这是为了以后哈希表的查找做基础。

2.3 哈希表的插入

  • 插入:通过哈希函数得到余数即数组下标,如果该下标的状态为删除或为空则插入,如果为存在则向后寻找下一个状态为删除/空的位置进行插入。(扩容一会单独讲)
	bool Insert(const pair<K,V> & kv){if (find(kv.first)){//先判断是否查找到,因为不能存储相同的结构,如果查找到返回falsereturn false;}//根据除留余数法判断该插入数组的哪个位置size_t hashi = Hashifunction(kv.first);while (_tables[hashi]._state == EXIST) {++hashi;hashi = hashi % _tables.size();  //如果探测到末尾则从头开始重新探测}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}

2.4 哈希表的查找

  查找:通过哈希函数得到余数即数组下标,取出小标位置的key与目标key进行比较,相等就返回该位置的地址,不相等就继续往后查找,如果查找到状态为空的下标位置就返回 nullptr;注意:这里有三个细节:


  当遇到状态为空的下标位置才返回 nullptr,而不是遇到状态为 删除的位置就返回 nullptr,因为你要查找的数据可能在已删除位置的后面,这也是我们需要添加一个删除状态的原因,如果你把一个位置的元素删除并设置为EMPTY状态,那么我们该如何进行查找呢?我们插入时,其元素可能在删除元素后面,例如:
  

如果是这样,那么你该如何查找呢? 

  
  将查找函数返回值定义为 Data*,而不是 bool,这样可以方便我们进行删除和修改 (修改 key 对应的 value) – 查找到之后直接通过指针解引用来修改 value 与 state;


  哈希表经过不断插入删除,最终可能会出现一种极端情况 – 哈希表中元素的状态全为 EXIST 和 DELETE,此时如果我们找空就会造成死循环,所以我们需要对这种情况单独进行处理.

	Data* find(const K& key) {size_t hashi = Hashifunction(key);//增加一个数字 如果查找一周 返回falsesize_t start = hashi;while (_tables[hashi]._state!= EMPTY)//如果不为空则继续查找{if (_tables[hashi]._state==EXIST&&_tables[hashi]._kv.first == key){return &_tables[hashi];}hashi++;//此时已经查找了一圈,退出循环if (hashi == start) {break;}hashi %= _tables.size();}//为空查找失败return nullptr;}

 2.5 哈希表的删除

 删除:这里直接复用查找函数,查找到就通过查找函数的返回值将小标位置数据的状态置为 删除,找不到就返回 false。

  这里采用伪删除法,将状态改为DELETE就好,毕竟我们重新插入时也是只看位置的状态,伪删除法还减少了删除消耗。

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

2.6 哈希表的扩容

   哈希表的扩容和顺序表的不同,它并不是存储空间满了的时候才开始扩容,而是依据负载因子

 其本质原因就是因为有哈希冲突的存在,当我们插入时,如果发生了哈希冲突,那么我们的插入,查找,删除效率最低都可以达到O(n)级别,那就将我们哈希表的各种优势都变得很微弱,甚至是变成劣势(相比红黑树等来说,有可能退化为顺序表)。

  因此,我们引入了负载因子这一概念,其本质为一个阈值,定义为:

负载因子=散列表中的元素个数/散列表的长度

    这里我们定义我们的负载因子为0.7,并且当负载因子每次超过这个值时,我们都将其容量扩大为2倍。

  同时,注意我在代码区编写的注意事项

代码如下:

	void HashExpansion(const size_t& fac){//开始扩容if (fac >= 7){//我们直接采用老板思维,创建一个长度为2*n的新表,然后一个个插入,最后直接交换//因为扩容后,数组长度发生变化,哈希函数也会变化,哈希表对应的位置也会变化//因此不能单纯的把数组长度变为2倍,需要一个个借助新表的插入函数,重新建立映射关系HashTable<K, V> newData;newData._tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newData.Insert(_tables[i]._kv);}}_tables.swap(newData._tables);}}

更改后的insert代码:

	bool Insert(const pair<K,V> & kv){if (find(kv.first)){//先判断是否查找到,因为不能存储相同的结构,如果查找到返回falsereturn false;}//根据除留余数法判断该插入数组的哪个位置size_t hashi = Hashifunction(kv.first);//这里我们通过一点小学知识将0.7 化为整数7HashExpansion(_n * 10 / _tables.size());while (_tables[hashi]._state == EXIST) {++hashi;hashi = hashi % _tables.size();  //如果探测到末尾则从头开始重新探测}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}

2.7 哈希表的仿函数

 我们的哈希表就已经完成的差不多了,还有很多细节问题,比如说,我们如果key类型是一个string类型的对象,我们该如何经过除留余数法,得到我们应该对应的下标呢?

 我们这里建议创建几个仿函数,分别对应不同的类型,得到不同的求解方法。

 关于整形,我们的仿函数模板如下,直接放回其对应的整形值:

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){// BKDR转换方法size_t hash = 0;for (auto e : key){hash *= 31;hash += e;}//cout << key << ":" << hash << endl;return hash;}
};

  但是请注意,其他类型,不同的值可能会经过转换的结果是相同的,这是因为int或者其它整形一共就几十个bit位,而string等等类型我只能说无穷无尽也。

  因此,建议用一些专用的转换方法,这些是专业string转换整形方法的链接:各种字符串Hash函数 - clq - 博客园

模板更改和函数更改: 

2.8 测试代码 

 打印函数:

	void Print(){for (size_t i = 0; i < _tables.size(); i++){if(_tables[i]._state==EXIST){//cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;printf("_tables._state[%d] == ", i);cout << _tables[i]._kv.first <<"-> "<< _tables[i]._kv.second<< endl;}else if (_tables[i]._state == DELETE) {printf("_tables._state[%d] == DELETE\n", i);}else {printf("_tables._state[%d] == EMPTY\n", i);}}cout << endl << endl;}

测试代码1:
 


void TestHT1()
{HashTable<int, int> ht;int a[] = { 4,14,24,34,5,7,1 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(3, 3));ht.Insert(make_pair(-3, -3));ht.Print();ht.Erase(3);ht.Print();if (ht.find(3)){cout << "3存在" << endl;}else{cout << "3不存在" << endl;}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(23, 3));ht.Print();
}

结果为:
 

 

 

测试代码2:
 

void TestHT2()
{string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };//HashTable<string, int, HashFuncString> ht;HashTable<string, int> ht;for (auto& e : arr){//auto ret = ht.Find(e);HashData<string, int>* ret = ht.find(e);if (ret){ret->_kv.second++;}else{ht.Insert(make_pair(e, 1));}}ht.Print();ht.Insert(make_pair("apple", 1));ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("abc", 1));ht.Insert(make_pair("acb", 1));ht.Insert(make_pair("aad", 1));ht.Print();
}

代码结果为:

2.9 完整代码 

#pragma once
#include<iostream>
#include<vector>
using namespace std;template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};// 11:46继续
//HashFunc<string>
template<>
struct HashFunc<string>
{size_t operator()(const string& key){// BKDRsize_t hash = 0;for (auto e : key){hash *= 31;hash += e;}//cout << key << ":" << hash << endl;return hash;}
};//通过空和存在判断插入,通过删除状态判断查找
enum State {EXIST,EMPTY,DELETE
};template<class K,class V>
struct HashData {pair<K, V> _kv; //每个节点存储KV结构State _state = EMPTY;  //默认为空
};//template<class K, class V>
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {typedef HashData<K, V> Data;
public:HashTable(): _n(0){//将哈希表的大小默认给为10_tables.resize(10);}bool Insert(const pair<K,V> & kv){if (find(kv.first)){//先判断是否查找到,因为不能存储相同的结构,如果查找到返回falsereturn false;}//根据除留余数法判断该插入数组的哪个位置size_t hashi = Hashifunction(kv.first);//这里我们通过一点小学知识将0.7 化为整数7HashExpansion(_n * 10 / _tables.size());while (_tables[hashi]._state == EXIST) {++hashi;hashi = hashi % _tables.size();  //如果探测到末尾则从头开始重新探测}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;}Data* find(const K& key) {size_t hashi = Hashifunction(key);//增加一个数字 如果查找一周 返回falsesize_t start = hashi;while (_tables[hashi]._state!= EMPTY)//如果不为空则继续查找{if (_tables[hashi]._state==EXIST&&_tables[hashi]._kv.first == key){return &_tables[hashi];}hashi++;//此时已经查找了一圈,退出循环if (hashi == start) {break;}hashi %= _tables.size();}//为空查找失败return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = find(key);if(ret){ret->_state = DELETE;--_n;return true;}else {return false;}}void HashExpansion(const size_t& fac){//开始扩容if (fac >= 7){//我们直接采用老板思维,创建一个长度为2*n的新表,然后一个个插入,最后直接交换//因为扩容后,数组长度发生变化,哈希函数也会变化,哈希表对应的位置也会变化//因此不能单纯的把数组长度变为2倍,需要一个个借助新表的插入函数,重新建立映射关系HashTable<K, V> newData;newData._tables.resize(_tables.size() * 2);for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newData.Insert(_tables[i]._kv);}}_tables.swap(newData._tables);}}void Print(){for (size_t i = 0; i < _tables.size(); i++){if(_tables[i]._state==EXIST){//cout << "[" << i << "]->" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;printf("_tables._state[%d] == ", i);cout << _tables[i]._kv.first <<"-> "<< _tables[i]._kv.second<< endl;}else if (_tables[i]._state == DELETE) {printf("_tables._state[%d] == DELETE\n", i);}else {printf("_tables._state[%d] == EMPTY\n", i);}}cout << endl << endl;}private://把哈希函数封装一下size_t Hashifunction(const K& key){//这里我们采用除留余数法Hash kot;return kot(key)% _tables.size();}
private:vector<Data> _tables;size_t _n;  //记录表中有效数据的个数
};void TestHT1()
{HashTable<int, int> ht;int a[] = { 4,14,24,34,5,7,1 };for (auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(3, 3));ht.Insert(make_pair(-3, -3));ht.Print();ht.Erase(3);ht.Print();if (ht.find(3)){cout << "3存在" << endl;}else{cout << "3不存在" << endl;}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(23, 3));ht.Print();
}void TestHT2()
{string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };//HashTable<string, int, HashFuncString> ht;HashTable<string, int> ht;for (auto& e : arr){//auto ret = ht.Find(e);HashData<string, int>* ret = ht.find(e);if (ret){ret->_kv.second++;}else{ht.Insert(make_pair(e, 1));}}ht.Print();ht.Insert(make_pair("apple", 1));ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("abc", 1));ht.Insert(make_pair("acb", 1));ht.Insert(make_pair("aad", 1));ht.Print();
}

三. 开散列表法的哈希表

  开散列法又叫 链地址法 (开链法),首先对关键码集合用散列函数计算散列地址,即 key 映射的下标位置,具有相同地址的关键码 (哈希冲突) 归于同一子集合,每一个子集合称为一个桶 (哈希桶),各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中;也就是说,当发生哈希冲突时,把 key 作为一个节点直接链接到通过哈希函数转换后对应下标的哈希桶中。

3.1 哈希表的基本框架

//两个仿函数,直接CV
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//HashFunc<string>
template<>
struct HashFunc<string>
{size_t operator()(const string& key){// BKDRsize_t hash = 0;for (auto e : key){hash *= 31;hash += e;}//cout << key << ":" << hash << endl;return hash;}
};//这里就不用状态表示了,因为我们无论节点是否存在,都可以进行插入
/*enum State {EXIST,EMPTY,DELETE
};*///节点定义
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;
private://把哈希函数封装一下size_t Hashifunction(const K& key) {//这里我们采用除留余数法Hash kot;return kot(key) % _tables.size();}
private:vector<Node*> _tables;  //指针数组size_t _n;  //表中有效数据的个数
};

3.2 哈希表的插入函数

  开散列插入的前部分和闭散列一样,根据哈希函数得到映射的下标位置,与闭散列不同的是,由于哈希表中每个下标位置都是一个哈希桶,即一个单链表,那么对于发现哈希冲突的元素我们只需要将其链接到哈希桶中即可,这里一共有两种链接方式:

  将元素链接到单链表的末尾,即尾插;
  将元素链接到单链表的开头,即头插。


这里显然是选择元素进行头插,因为尾插还需要找尾,会导致效率降低,插入部分代码如下:
 

	bool Insert(const pair<K, V>& kv) {if (find(kv.first)) {return false;}//这里待会需要补一个扩容size_t hashi = Hashifunction(kv.first);//哈希桶头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}

 3.3 开散列的查找

  开散列的查找也很简单,根据哈希函数找到下标,由于下标位置存储的是链表首元素地址,所以我们只需要取出首元素地址,然后顺序遍历单链表即可:

Node* find(const K& key) {size_t  hashi = Hashifunction(key);Node* cur = _tables[hashi];while (cur) {if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}

3.4 开散列的删除

  开散列的删除不能单纯的依靠查找函数来进行直接删除,因为在删除函数中,我们不仅要对本应查找到的节点进行删除,还要改变其父节点的指向,让他指向删除节点的下一个节点。

	bool erase(const K& key) {size_t  hashi = Hashifunction(key);Node* cur = _tables[hashi];//记录上一个节点的父节点Node* prev = nullptr;while (cur) {if (cur->_kv.first == key) {if (cur == _tables[hashi]) {_tables[hashi] = cur->_next;}else {prev->_next = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}

3.5 开散列的扩容

开散列的扩容可以和闭散列的扩容一样借用insert函数,但是我们有更好的方法。

方法一:
  借用insert函数。

方法二:

   将原本链表挨个头插入新的哈希表。 

	if (fac > 7) {//这里我们有两种方法//一是借用 以前我们开散列中的insert插入方法//此种实现比较简单,但相比第二种有其对应的缺点//实现/*	HashTable<K, V> newTable;newTable._tables.resize(_tables.size() * 2,nullptr);for (int i = 0; i < _tables.size(); i++) {Node* cur = _tables[i];while (cur) {newTable.Insert(cur);cur = cur->_next;}}_tables.swap(newTable._tables);}*///二是直接把原先的哈希表中的每个单链表 按照新的哈希函数//直接头插进新链表//这个方法我们可以看出,少借用insert的一部分//也就是说,我们没有创建节点的消耗。//是真正的空间转移 因此,效率比第一种方法高很多,我们以后用这个方法vector<Node*> newtable;newtable.resize(_tables.size() * 2, nullptr);for (int i = 0; i < _tables.size(); i++) {Node* cur = _tables[i];while (cur) {Node* next = cur->_next;size_t hashi = Hashifunction(cur->_kv.first, newtable);cur->_next = newtable[hashi];newtable[hashi] = newtable;cur = next;}_tables[i] = nullptr;}_tables.swap(newtable);}
}

 3.6 开散列的测试代码

print函数和以前也要有所不同。

void Print() {for (int i = 0; i < _tables.size(); i++) {Node* cur = _tables[i];cout << i << ":";while (cur) {cout << "[" << cur->_kv.first << "-> " << cur->_kv.second << "]" << "    ";cur = cur->_next;}cout << endl;}cout << endl;}

两段测试代码: 

	void TestHT1(){HashTable<int, int> ht;int a[] = { 4,14,24,34,5,7,1 };for(auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(3, 3));ht.Insert(make_pair(-3, -3));ht.Print();ht.erase(3);ht.Print();if (ht.find(3)){cout << "3存在" << endl;}else{cout << "3不存在" << endl;}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(23, 3));ht.Print();
}void TestHT2()
{string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };//HashTable<string, int, HashFuncString> ht;HashTable<string, int> ht;for (auto& e : arr){//auto ret = ht.Find(e);HashNode<string, int>* ret = ht.find(e);if (ret){ret->_kv.second++;}else{ht.Insert(make_pair(e, 1));}}ht.Print();ht.Insert(make_pair("apple", 1));ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("abc", 1));ht.Insert(make_pair("acb", 1));ht.Insert(make_pair("aad", 1));ht.Print();
}

代码一结果:

测试代码二结果:

3.7 完整代码 

#pragma once
#include<iostream>
#include<vector>
using namespace std;namespace BucketHash {//两个仿函数,直接CVtemplate<class K>struct HashFunc{size_t operator()(const K& key){return (size_t)key;}};//HashFunc<string>template<>struct HashFunc<string>{size_t operator()(const string& key){// BKDRsize_t hash = 0;for (auto e : key){hash *= 31;hash += e;}//cout << key << ":" << hash << endl;return hash;}};//这里就不用状态表示了,因为我们无论节点是否存在,都可以进行插入/*enum State {EXIST,EMPTY,DELETE};*///节点定义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():_n(0){_tables.resize(10, nullptr);}bool Insert(const pair<K, V>& kv) {if (find(kv.first)) {return false;}HashExpansion(_n * 10 / _tables.size());//这里待会需要补一个扩容size_t hashi = Hashifunction(kv.first,_tables.size());//哈希桶头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}Node* find(const K& key) {size_t  hashi = Hashifunction(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) {size_t  hashi = Hashifunction(key,_tables.size());Node* cur = _tables[hashi];//记录上一个节点的父节点Node* prev = nullptr;while (cur) {if (cur->_kv.first == key) {if (cur == _tables[hashi]) {_tables[hashi] = cur->_next;}else {prev->_next = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}void HashExpansion(const size_t& fac){if (fac > 7) {//这里我们有两种方法//一是借用 以前我们开散列中的insert插入方法//此种实现比较简单,但相比第二种有其对应的缺点//实现/*	HashTable<K, V> newTable;newTable._tables.resize(_tables.size() * 2,nullptr);for (int i = 0; i < _tables.size(); i++) {Node* cur = _tables[i];while (cur) {newTable.Insert(cur);cur = cur->_next;}}_tables.swap(newTable._tables);}*///二是直接把原先的哈希表中的每个单链表 按照新的哈希函数//直接头插进新链表//这个方法我们可以看出,少借用insert的一部分//也就是说,我们没有创建节点的消耗。//是真正的空间转移 因此,效率比第一种方法高很多,我们以后用这个方法//但是哈希函数就不好处理了,这里我建议直接把哈希函数改一下,直接传入表的长度vector<Node*> newtable;newtable.resize(_tables.size() * 2, nullptr);for (int i = 0; i < _tables.size(); i++) {Node* cur = _tables[i];while (cur) {Node* next = cur->_next;size_t hashi = Hashifunction(cur->_kv.first, newtable.size());cur->_next = newtable[hashi];newtable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtable);}}void Print() {for (int i = 0; i < _tables.size(); i++) {Node* cur = _tables[i];cout << i << ":";while (cur) {cout << "[" << cur->_kv.first << "-> " << cur->_kv.second << "]" << "    ";cur = cur->_next;}cout << endl;}cout << endl;}private://把哈希函数封装一下size_t Hashifunction(const K& key,size_t size) {//这里我们采用除留余数法Hash kot;return kot(key) % size;}private:vector<Node*> _tables;  //指针数组size_t _n;  //表中有效数据的个数};void TestHT1(){HashTable<int, int> ht;int a[] = { 4,14,24,34,5,7,1 };for(auto e : a){ht.Insert(make_pair(e, e));}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(3, 3));ht.Insert(make_pair(-3, -3));ht.Print();ht.erase(3);ht.Print();if (ht.find(3)){cout << "3存在" << endl;}else{cout << "3不存在" << endl;}ht.Insert(make_pair(3, 3));ht.Insert(make_pair(23, 3));ht.Print();
}void TestHT2()
{string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };//HashTable<string, int, HashFuncString> ht;HashTable<string, int> ht;for (auto& e : arr){//auto ret = ht.Find(e);HashNode<string, int>* ret = ht.find(e);if (ret){ret->_kv.second++;}else{ht.Insert(make_pair(e, 1));}}ht.Print();ht.Insert(make_pair("apple", 1));ht.Insert(make_pair("sort", 1));ht.Insert(make_pair("abc", 1));ht.Insert(make_pair("acb", 1));ht.Insert(make_pair("aad", 1));ht.Print();
}
}

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

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

相关文章

快速登录界面关于如何登录以及多账号列表解析以及config配置文件如何读取以及JsLogin模块与SdoLogin模块如何通信(4)

1、### Jslogin模块与前端以及JsLogin模块与Sdologin的交互 配置文件的读取: <CompanyIdForQq value"301"/> <CompanyIdForWx value"300"/><CompanyIdForWb value"302"/><qq value"https://graph.qq.com/oauth2.0/au…

git clone 命令

git clone 是一个用于克隆&#xff08;clone&#xff09;远程 Git 仓库到本地的命令。 git clone 可以将一个远程 Git 仓库拷贝到本地&#xff0c;让自己能够查看该项目&#xff0c;或者进行修改。 git clone 命令&#xff0c;你可以复制远程仓库的所有代码和历史记录&#xf…

阿里云SLB的使用总结

一、什么是SLB 实现k8s的服务service的一种推荐方式&#xff0c;也是服务上云后&#xff0c;替代LVS的一个必选产品。 那么它有什么作用呢&#xff1f; 1、负载均衡&#xff0c;是它与生俱来的。可以配置多个服务器组&#xff1a;包括虚拟服务器组、默认服务器组、主备服务器…

JUnit 之初体验

文章目录 1.定义2.引入1&#xff09;使用 Maven 工具2&#xff09;使用 Gradle 工具3&#xff09;使用 Jar 包 2.样例0&#xff09;前提1&#xff09;测试类2&#xff09;测试方法3&#xff09;测试断言4&#xff09;实施 总结 1.定义 JUnit 是一个流行的 Java 单元测试框架&a…

H5ke14--1--拖放

介绍drag,drop 一.被拖动元素,目标(释放区) 元素要设置dragable属性:true,false,auto 被拖动元素上面有三个事件,drag,dragend,按下左键,移动种,鼠标松,这三个事件一般只用获取我们的被拖动元素 冒泡:event是可以继承的,mouseevent鼠标事件,dragevent拖放事件,前面都是一个…

Python基础(一、安装环境及入门)

一、安装 Python 访问 Python 官方网站 并点击 "Downloads"&#xff08;下载&#xff09;。 在下载页面中&#xff0c;你会看到最新的 Python 版本。选择与你的操作系统相对应的 Windows 安装程序并下载。 双击下载的安装程序&#xff0c;运行安装向导。 在安装向…

Redis KEY*模糊查询导致速度慢、阻塞其他 Redis 操作

Redis KEY*模糊查询导致交互速度慢、阻塞其他 Redis 操作 查询速度慢的原因 在Redis中&#xff0c;使用通配符 KEYS 命令进行键的模糊匹配&#xff08;比如 KEYS key*&#xff09;可能会导致性能问题&#xff0c;尤其是在数据集较大时。这是因为 KEYS 命令的实现需要遍历所有…

mybatis和mybatisplus中对 同namespace 中id重复处理逻辑源码解析

一、背景 同事在同一个mapper.xml &#xff08;namespace相同&#xff09;&#xff0c;复制了一个sql没有修改id&#xff0c;正常启动项目。但是我以前使用mybatis的时候如果在namespace相同情况下&#xff0c;id重复&#xff0c;项目会报错无法正常启动&#xff0c;后来看代码…

用户帐户限制(例如,时间限制)会阻止你登录。请与系统管理员或技术支持联系以获取帮助。

用户帐户限制(例如&#xff0c;时间限制)会阻止你登录。请与系统管理员或技术支持联系以获取帮助。 在Windows11远程连接Windows10时提示【用户帐户限制(例如&#xff0c;时间限制)会阻止你登录。请与系统管理员或技术支持联系以获取帮助。】我们该如何解决&#xff1a; 1、在…

React聚焦渲染速度

目录 一、引言 二、React.js的渲染速度机制 虚拟DOM Diff算法 三、优化React.js的渲染速度 避免不必要的重新渲染 使用合适的数据结构和算法 使用React Profiler工具进行性能分析 四、实际案例分析 五、总结 一、引言 在当今的Web开发领域&#xff0c;React.js无疑是…

C语言——螺旋矩阵(注释详解)

一、前言&#xff1a; 螺旋矩阵是指一个呈螺旋状的矩阵&#xff0c;它的数字由第一行开始到右边不断变大&#xff0c;向下变大&#xff0c;向左变大&#xff0c;向上变大&#xff0c;如此循环。 二、市面解法&#xff08;较难理解,代码长度短&#xff09;&#xff1a; 根据阶数…

销售技巧培训之如何提高建材销售技巧

建材销售市场竞争也日趋激烈。在这个充满挑战与机遇的市场中&#xff0c;掌握一定的销售技巧对于一个建材销售人员来说至关重要。本文将结合实际案例&#xff0c;探讨一些实用的建材销售技巧&#xff0c;帮助你更好地拓展业务。 一、了解客户需求 在销售过程中&#xff0c;首先…

【深度学习】一维数组的 K-Means 聚类算法理解

刚看了这个算法&#xff0c;理解如下&#xff0c;放在这里&#xff0c;备忘&#xff0c;如有错误的地方&#xff0c;请指出&#xff0c;谢谢 需要做聚类的数组我们称之为【源数组】 需要一个分组个数K变量来标记需要分多少个组&#xff0c;这个数组我们称之为【聚类中心数组】…

IO多路转接之select

IO多路转接之select 1. IO多路转接&#xff08;复用&#xff09;2. select2.1 函数原型2.2 细节描述 3. 并发处理3.1 处理流程3.2 通信代码 原文链接 1. IO多路转接&#xff08;复用&#xff09; IO多路转接也称为IO多路复用&#xff0c;它是一种网络通信的手段&#xff08;机…

【目标检测算法】IOU、GIOU、DIOU、CIOU

目录 参考链接 前言 IOU(Intersection over Union) 优点 缺点 代码 存在的问题 GIOU(Generalized Intersection over Union) 来源 GIOU公式 实现代码 存在的问题 DIoU(Distance-IoU) 来源 DIOU公式 优点 实现代码 总结 参考链接 IoU系列&#xff08;IoU, GIoU…

WPF使用WebBrowser报脚本错误问题处理

前言 WPF使用WebBrowser报脚本错误问题处理,我们都知道WPF自带的WebBrowser都用的IE内核,但是在特殊的条件下我们还需要用到它,比如展示纯html简单的页面。再展示主流页面的时候比如用到Jquery高级库或者VUE等当前主流站点时经常就会报JS脚本错误,在Winform里面我们一句代…

【精选】设计模式——工厂设计模式

工厂设计模式是一种创建型设计模式&#xff0c;其主要目的是通过将对象的创建过程封装在一个工厂类中来实现对象的创建。这样可以降低客户端与具体产品类之间的耦合度&#xff0c;也便于代码的扩展和维护。 工厂设计模式&#xff1a; 以下是Java中两个常见的工厂设计模式示例…

C++ 关于结构体struct的一些总结

文章目录 一、 结构体(struct)是什么&#xff1f;&#xff08;1&#xff09;概念&#xff08;2&#xff09;struct 与 calss 的区别 二、定义、声明与初始化&#xff08;1&#xff09;三种定义结构体的方法&#xff1a;&#xff08;2&#xff09;结构体变量初始化 三、结构体嵌…

C++实现进程端口网络数据接收系统设计示例程序

一、问题描述 最近做了一道简单的系统设计题&#xff0c;大概描述如下&#xff1a; 1.一个进程可以绑定多个端口&#xff0c;用于监听接收网络中的数据&#xff0c;但是一个端口只能被一个进程占用 2.1 < pid < 65535, 1 < port < 100000, 1 < topNum < 5, …

C++:vector增删查改模拟实现

C:vector增删查改模拟实现 前言一、迭代器1.1 非const迭代器&#xff1a;begin()、end()1.2 const迭代器&#xff1a;begin()、end() 二、构造函数、拷贝构造函数、赋值重载、析构函数模拟实现2.1 构造函数2.1.1 无参构造2.1.2 迭代器区间构造2.1.3 n个值构造 2.2 拷贝构造2.3 …