在2022年的最后一天我学会了哈希表

本文介绍了C++STL中的unordered_set和unordered_map容器,详细阐述了它们的使用方法和底层哈希表的工作原理。哈希表通过哈希函数实现快速查找,但存在哈希冲突问题。文章探讨了闭散列(开放地址法)和开散列(链地址法)两种处理冲突的策略,并展示了模拟实现这两个容器的代码,包括插入、查找和删除操作。此外,还提到了迭代器的实现以及如何使用哈希表构建unordered_set和unordered_map的模拟实现。
摘要由CSDN通过智能技术生成

前言

首先先提前祝贺大家新年快乐!本文是农历2022年的最后一篇博客。而今天我们介绍的也是STL里面重要的一个数据结构---->哈希表 哈希数据结构的优势在于能够在查找数据的平均时间复杂度为O(1).我们会从几个角度对哈希表进行解析:

1.STL里面的对应的容器的使用
2.容器底层结构相关介绍及模拟
3.STL里面相关容器的模拟实现

STL相关容器

首先,STL里面是对应的哈希的数据结构。这两个数据结构一个叫做unordered_set,一个叫做unordered_map。需要注意的是,这两个容器都是在C++11以后才引入标准库的! 下面我们就来看一看这两个容器的具体使用。

unordered_set

首先,我们还是通过文档来学习对应容器的使用。unordered_set的使用手册的文档请点击 ->unordered_set使用手册
在这里插入图片描述

/*unordered_set,unordered_map容器的使用
 * */
#include<iostream>
#include<unordered_set>
#include<unordered_map>
//unordered_set的使用
void testus1()
{ 
  //构造unordered_set
  std::unordered_set<int> us;
  //使用insert插入元素 
  us.insert(1);
  us.insert(3);
  us.insert(2);
  us.insert(4);
  us.insert(1);
  //可以使用迭代器遍历 
  std::unordered_set<int>::iterator it=us.begin();
  while(it!=us.end())
  {
     std::cout<<*it<<" "; 
     ++it;
  }
  std::cout<<std::endl;
}
int main()
{
    ::testus1();
  return 0;
}

在这里插入图片描述
可以看到,虽然我们插入了两次1,实际进到容器里面的只有一个1!也就是说,对应的unordered_set也是有去重的功能。并且打印的结果也是无序的!这就是为什么叫做unordered_set的原因。

void testus2()
{

  std::unordered_set<int> us;
  //使用insert插入元素 
  us.insert(1);
  us.insert(3);
  us.insert(2);
  us.insert(4);
  //使用erase删除元素,有两种方式 
  //1.根据迭代器删除,使用find找相应的元素,然后删除 
  std::unordered_set<int>::iterator pos=us.find(4);
  //要保证不是end才能删除!
  if(pos!=us.end())
  {
     us.erase(pos);
  }
  //支持迭代器就能支持范围for 
  for(int e:us)
  {
    std::cout<<e<<" ";
  }
  std::cout<<'\n';
}

在这里插入图片描述
但是这种方式相对来说非常繁琐。实际上,我们还可以根据对应元素来进行删除

void testus3()
{

  std::unordered_set<int> us;
  //使用insert插入元素 
  us.insert(1);
  us.insert(3);
  us.insert(2);
  us.insert(4);
  us.erase(2);
  //删除不存在的元素
  us.erase(5);
  for(int e:us)
  {
    std::cout<<e<<" ";
  }
  std::cout<<std::endl;
}

在这里插入图片描述
使用这种方式的删除,底层先会自动调用find判断元素是否存在,所以这种情况可以避免出现元素不存在时带来的越界问题!值得一提的是,这个函数的返回值是size_t,其实和set那块一样,也是为了multi版本准备的!所以这个返回值通常是0或者1
unordered_set就暂时介绍到这里,接下来我们来看unordered_map容器的使用

unordered_map

我们还是一样通过官方的文档手册来看如何使用unordered_map容器是如何使用的 -> unordered_map的使用手册
在这里插入图片描述
接下来我们就写几段代码来看一看如何使用unordered_map

void testum1()
{
  std::unordered_map<std::string,std::string> words;
  //插入元素
  words.insert(std::make_pair("sort","排序"));
  words.insert(std::make_pair("left","左边"));
  words.insert(std::make_pair("right","右边"));
  words.insert(std::make_pair("left","剩余"));
  for(const auto& kv:words)
  {
     std::cout<<kv.first<<" to chinese is "<<kv.second<<std::endl;
  }
  std::cout<<'\n';
}

在这里插入图片描述
可以看出,和map类似,对于同样的关键字。unordered_map也是不会重复插入的。insert 和 erase方法也和unordered_set类似,这里就不再演示了。接下来我们来看一看unordered_map的opreator[]

void testum2()
{
  
  std::unordered_map<std::string,std::string> words;
  //插入元素
  words.insert(std::make_pair("sort","排序"));
  words.insert(std::make_pair("left","左边"));
  words.insert(std::make_pair("right","右边"));
  words.insert(std::make_pair("left","剩余"));
  words["learn"];
  words["left"]="剩余";
  for(const auto& kv:words)
  {
     std::cout<<kv.first<<" to chinese is "<<kv.second<<std::endl;
  }
}

和map类似,opreator[]如果不存在就会插入,存在了就会返回对应位置的引用。而剩下的 接口由于不是特别常用所以就不一一进行演示了。下面我们来讲一讲这两个数据结构的底层实现---->哈希表

哈希表

首先回顾set和map,它们查找高效的原因是因为底层的红黑树,可以使得查找的时间复杂度在logn级别。而实际上理想的查找的方式:通过某种特定的映射关系,便可以在常数的时间内查找到我们对应想要的元素。这就是哈希表的底层工作原理。 而求取映射关系的函数我们称之为哈希函数
所以我们可以搭建如下的哈希表数据结构的骨架

//三种状态码
enum State
{
   EMPTY,
   EXIST,
   DELETE 
};
//哈希数据
template<typename K,typename V>
struct HashData
{ 
  pair<K,V> _kv;
  State _state;
  HashData()
    :_state(EMPTY)
  {}
};
template<typename K,typename V,typename HashFunc>
class HashTable
{
private:
    typedef HashData<K,V> Data;
public:
//插入
bool Insert(const pair<K,V>& kv);
//查找
Data* Find(const K& key);
//删除
bool Erase(const K& key);
private:
    vector<Data> _table;
    int _n=0; //负载因子

};

哈希冲突

前面我们知道哈希表的底层工作原理是通过哈希函数映射位置,但是元素一多,总会出现不同的元素通过相同的哈希函数映射到同一个位置,这就是所谓的哈希冲突。 无论在怎么好的哈希算法,终究难逃出现哈希冲突的情况。所以人们就需要处理哈希冲突的元素。有如下的处理方法

闭散列

闭散列也叫做开放地址法, 只要哈希表尚未装满,我们就可以把冲突的元素放入对应的空的表的地方,那么接下来的问题就转换成如何寻找空的位置。这里有很多方法,我们主要实现的是线性探测的方式:
线性探测的示意图就是如下:
在这里插入图片描述
接下来我们就可以写出如下的代码:

//线性探测插入元素
    bool insert(const pair<K,V>& kv)
    {   
      //计算映射位置
        size_t starti=kv.first % _table.size();
        size_t hashi=starti;
        size_t i=1;
        //找到第一个为空的位置
        while(_table[hashi]._state==EXIST) 
        {
              hashi=starti+i;
              ++i;
              //防止找越界
              hashi%=_table.size();
        }
       //找到这里就找到合适的了
       _table[hashi]._kv=kv;
       _table[hashi]._state=EMPTY;
        ++_n; 
       return true;
    }

目前我们的代码还是存在相当严重的问题:

1.如果当前的表是空表,那么就会出现除零错误!
2.插入元素导致哈希表扩容时,原有映射关系失效问题。

因此,我们还需要处理这两个严重的问题!但是哈希表选择在什么时候扩容才会比较好呢?实际上,为了解决哈希表的扩容问题,很多设计者引入了扩容因子的概念! 计算的公式如下:

负载率a=(有效元素n)/(容器的大小size) ,而一旦达到对应的扩容因子的数值,就要进行扩容!

而在Java的官方库里面设计的哈希表的扩容因子为0.75,那么我们就把这里对应的扩容因子设计成0.7

//线性探测插入元素
    bool insert(const pair<K,V>& kv)
    {   
      //计算映射位置
      if(_table.size()==0 || _n*10 / _table.size()>=7){
         //进行扩容处里
         size_t  newSize=_table.size() ? _table.size()*2 : 10;
         //建立新的表,复用insert,把旧表的元素重新计算在新表的哈希位置
         HashTable<K,V> newHT;
         newHT._table.resize(newSize,Data());
         for(auto& e:_table){

           newHT.insert(e._kv);
         }
        
         //交换两个表就可以了
         newHT._table.swap(_table);

      }
       
        size_t starti=kv.first % _table.size();
        size_t hashi=starti;
        size_t i=1;
        while(_table[hashi]._state==EXIST) 
        {

              hashi=starti+i;
              ++i;
              //防止找越界
              hashi%=_table.size();
        }
       //找到这里就找到合适的了
       _table[hashi]._kv=kv;
       _table[hashi]._state=EMPTY;
        ++_n; 
       return true;
    }

现在目前插入的大逻辑没有问题,但是现在仍然存在问题!我们这里pair的第一个参数时一个模板,也就是说,pair的first的类型是任意的!那么可能未来会有无法支持取模的类型!所以我们还需要一个仿函数对pair的first进行转换,让其能够支持取模

template<typename K,typename V,typename HashFunc>
class HashTable
{
private:
    typedef HashData<K,V> Data;
public:
//插入
bool Insert(const pair<K,V>& kv);
//查找
Data* Find(const K& key);
//删除
bool Erase(const K& key);
private:
    vector<Data> _table;
    int _n=0; //负载因子

};

那么对于常见的内置类型,我们肯定需要提供一个默认的缺省参数来支持哈希转换。而我们日常生活中string类也是使用的相对频繁 所以最后我们决定为常见的内置类型和string提供统一的哈希函数。而对于字符串哈希函数就选用哈希冲突率相对较低的BKDR哈希算法

//为了支持寻找,我们还需要定义一个转换key的仿函数
//支持直接转换成size_t的类型用的仿函数
template<typename K>
struct DefaultHash
{
  size_t operator()(const K& key)
  {
     return static_cast<int>(key); 
  }
};
template<>
struct DefaultHash<std::string>
{
  size_t operator()(const std::string& key)
  { 
     size_t hash=0;
     for(auto ch:key)
     {
       hash= hash*131+ch;
     }
     return hash;
  }
};

所以最后我们就可以改造对应的哈希表的插入算法和哈希表的大骨架

template<typename K,typename V,typename HashFunc=DefaultHash<K>>
class HashTable
{
  
  
    typedef HashData<K,V> Data;
  public:
    //线性探测插入元素
    bool insert(const pair<K,V>& kv)
    {   
      //计算映射位置
      //需要防止除0错误和控制负载因子 
      if(_table.size()==0 || _n*10 / _table.size()>=7){
         //进行扩容处理,现代写法 
          
         size_t  newSize=_table.size() ? _table.size()*2 : 10;
         //建立新的表,然后复用insert重新哈希
         HashTable<K,V> newHT;
         newHT._table.resize(newSize,Data());
         for(auto& e:_table){

           newHT.insert(e._kv);
         }
        
         //交换就可以了
         newHT._table.swap(_table);

      }
      //转换kv.first成可以支持取模的类型
       HashFunc hf;
        size_t starti=hf(kv.first) % _table.size();
        size_t hashi=starti;
        size_t i=1;
        while(_table[hashi]._state==EXIST) 
        {

              hashi=starti+i;
              ++i;
              //防止找越界
              hashi%=_table.size();
        }
       //找到这里就找到合适的了
       _table[hashi]._kv=kv;
       _table[hashi]._state=EMPTY;
        ++_n; 
       return true;
    }

接下来就是实现查找接口。查找接口相对而言比较简单,但是也有不少的细节。
我们直接结合代码来进行说明:

//查找
    Data* find(const K& key)
    {
         //如果表是空,会发生除0错误,要单独处理
         if(!_table.size())
           return nullptr;
         //计算位置
         HashFunc hf;
        size_t hashi=hf(key);
        size_t starti=hashi;
        size_t i=1;
        //非空就继续找
        while(_table[hashi]._state!=EMPTY)
        { 
          //注意,如果当前元素是被删除状态的话也是不可以选取的!
           if(_table[hashi]._state!=DELETE && _table[hashi]._kv.first==key)
             return &_table[hashi];
           //没找到就线性探测
           hashi=starti+i;
           ++i;
           hashi%=_table.size();
        }
        return nullptr;
    }

最后我们来看一看删除接口,我们知道,哈希表的底层结构是个vector,而vector的删除就是挪动数据!但是由于哈希结构的特殊性,不能够通过挪动数据的方式删除!而哈希数据的删除只要把对应的位置的状态改成DELETE就可以达到删除的目的了

//删除,只要莫状态就好了
    bool erase(const K& key)
    {   
       Data* ret=find(key);
       //
       if(ret)
       {
           ret->_state=DELETE;
           --_n; 
           return true;
       }
       return false;
    }

接下来我们来写一段测试代码来测试前面写的几个接口

void TestHT1()
	{
		int a[] = { 20, 5, 8, 99999, 10, 30, 50 };
		//hash_table<int, int, DefaultHash<int>> ht;
		hash_table<int, int> ht;

		if (ht.find(10))
		{
			cout << "找到了10" << endl;
		}

		for (auto e : a)
		{
			ht.insert(make_pair(e, e));
		}

		// 测试扩容
		ht.insert(make_pair(15, 15));
		ht.insert(make_pair(5, 5));
		ht.insert(make_pair(15, 15));

		if (ht.find(50))
		{
			cout << "找到了50" << endl;
		}

		if (ht.find(10))
		{
			cout << "找到了10" << endl;
		}

		ht.erase(10);
		ht.erase(10);

		if (ht.find(50))
		{
			cout << "找到了50" << endl;
		}

		if (ht.find(10))
		{
			cout << "找到了10" << endl;
		}
	}

在这里插入图片描述
关于闭散列的哈希的算法我们就暂时介绍到这里。这种算法实现简单,但是缺点也很致命,一旦哈希冲突频繁,就会 导致原来的本来应该能够正确被映射的元素需要线性探测,会进一步使哈希冲突的频率变高 所以标准库里面哈希冲突策略使用不是这种方法,而是哈希桶法。又叫做开散列。

开散列

开散列设计的非常好。我们知道在闭散列的处理方式里面,一个位置只能放一个Data,但是在开散列的情况里面。一个数组下标处存放的是指针!换句话说一旦出现了哈希冲突,只需要把对应的节点头插入到对应的数组下标处就可以了!所以这个时候处理哈希冲突就变成了链表的插入! 大致的结构图如下:
在这里插入图片描述
在这个结构里面,我们需要的是链表,而且我们仅仅只要头插。所以我们可以只是用单链表来完成即可。

//HashData 
template<typename K,typename V>
struct HashNode 
{  //constructor
   explicit HashNode(const std::pair<K,V>& kv)
     :_kv(kv)
     ,_next(nullptr) 
     {}
   std::pair<K,V> _kv;
   HashNode* _next;
};
//对应的哈希表的结构也要改造
//要实现插入,查找,删除
template<typename K,typename V,typename HashFunc=chy::DefaultHash<K>>
class HashTable
{
private:
  typedef chy::HashNode<K,V> HashNode;
public:
   //要实现的接口
   HashTable();//constructor
   bool Insert(const pair<K,V>& kv);
   bool Erase(const K& key);
   HashNode* Find(const K& key);
private:
   std::vector<HashNode*> _tables; //哈希表
   std::size_t _n=0;//有效载荷
};

首先我们来看最简单的接口:Find的实现

 HashNode* Find(const K& key)
  {  
    HashFunc hf;
    //空表
     if(!_tables.size())
     {
        return nullptr;
     }
     //开始遍历寻找 
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* cur=_tables[hashi];
     while(cur)
     {
         if(cur->_kv.first== key) 
         {
            return cur;
         }
         cur=cur->_next;
     }
     return nullptr;
  }

接下来就是插入算法了。同样我们结合代码来说明:

//插入,复用查找 
  bool Insert(const std::pair<K,V>& kv)
  {   
     HashFunc hf;
    //找到了对应的节点
     if(Find(kv.first))
     {
        return false;
     }
     //扩容 
     if(_tables.size()==_n)
     {
        std::size_t newSize=_tables.size() ? 2*_tables.size() : init_size;
        std::vector<HashNode*> newTables;
        newTables.resize(newSize,nullptr);
        //重新哈希 
        for(std::size_t i=0;i < _tables.size();++i)
        {
           HashNode* cur=_tables[i];
           while(cur)
           {
               std::size_t hashi=hf(cur->_kv.first);
               HashNode* next=cur->_next;
               hashi%=newSize;
               cur->_next=newTables[hashi];
               newTables[hashi]=cur;
               cur=next;
           }
           _tables[i]=nullptr;
        }
        //交换即可 
        _tables.swap(newTables);
     }
     //插入逻辑
     std::size_t hashi=hf(kv.first);
     hashi%=_tables.size();
     HashNode* newnode=new HashNode(kv);
     newnode->_next=_tables[hashi];
     _tables[hashi]=newnode;
     ++_n;
     return true;
  }

和前面类似,在扩容的时候,我们选择创建一个新的表进行重新哈希。不同于前面的闭散列,开散列我们需要依次把原先的桶上挂着的链表也一并拿过来!
删除的逻辑也类似,下面直接展示删除的代码:

//删除,复用Find 
 bool Erase(const K& key)
 { 
     //节点不存在
     if(!Find(key))
     {
       return false;
     }
     HashFunc hf;
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* prev=nullptr;
     HashNode* cur=_tables[hashi];
     //单链表删除逻辑,分为头删和中间删除
     while(cur)
     { 
       //查找到节点
       if(cur->_kv.first==key)
       {
           //头删的情况 
           if(!prev)
           {
              _tables[hashi]=cur->_next;
           }
           else 
           {
              prev->_next=cur->_next;
           }
           delete cur;

           return true;
       }
        prev=cur;
        cur=cur->_next;
     }
     return false;
 }

那么既然使用了链表。我们就不得不面对对应的一个问题。到底哈希表的析构函数是否要实现 ? 观察对应哈希表的成员,我们不难可以看出,虽然vector可以自己进行析构,但是问题是vector内部挂着的链表是内置类型成员,一旦vector析构,对应的链表的节点就再也找不到了!所以对于哈希表,我们需要显式提供一个析构函数:

//析构函数 
 ~HashTable()
 {
   for(std::size_t i=0;i<_tables.size();++i)
   {
        HashNode* cur=_tables[i];
        //单链表删除逻辑
        while(cur)
        {
           HashNode* next=cur->_next;
           delete cur;
           cur=next;
        }
        _tables[i]=nullptr;
   }
   _n=0;
 }

另外补充一点:当哈希桶链表的长度非常长的时候,哈希表的查询速度可能会变慢。所以在Java的官方实现的哈希容器的底层,当链表的长度达到一定的范围以后,就改成挂红黑树了!

STL相关容器的模拟实现

用一个哈希表改造两个容器

首先,类比于我们先前模拟实现map和set,忘记的可以点击这个链接 ->map和set的模拟实现
接下来我们结合代码来进行讲解:

/*
使用一个哈希表封装出unordered_set和unordered_map两个容器
首先对于哈希数据结构来说,数据类型就不能写成固定的pair,而应该依旧是一个泛型!所以我们需要对原来的HashNode进行改造
*/
//这里的数据类型就是T
template<typename T>
struct HashNode 
{  //constructor
   explicit HashNode(const T& val)
     :_val(val)
     ,_next(nullptr) 
     {}
   T _val;
   HashNode* _next;
};

接下来我们就需要处理一个新的问题:数据类型成为了泛型!但是我们先前实现的哈希表是针对pair进行处理的!所以我们要对于我们先前的代码进行改变! 参考先前map和set那里,所以我们也要提供一个仿函数类来进行获取对应的key!
所以最后我们更改哈希表的代码如下:

/*要实现插入,查找,删除,哈希表有了哈希函数,必须还要有一个支持可以获取key的仿函数!*/ 
template<typename K,typename T,typename KeyOfT,typename HashFunc>
class HashTable
{
private:
  typedef chy::HashNode<T> HashNode;
public:
  //先实现查找,以便于其他功能复用 
  HashNode* Find(const K& key)
  { 
    //把特殊对象转换成可以取模的类型
    KeyOfT kot;
    HashFunc hf;
    //空表
     if(!_tables.size())
     {
        return nullptr;
     }
     //开始遍历寻找 
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* cur=_tables[hashi];
     while(cur)
     {
         if(kot(cur->_val)== key) 
         {
            return cur;
         }
         cur=cur->_next;
     }
     return nullptr;
  }
  //插入,复用查找 
  bool Insert(const T& val)
  {   
     KeyOfT kot;
     HashFunc hf;
    //找到了对应的节点
     if(Find(kot(val)))
     {
        return false;
     }
     //扩容 
     if(_tables.size()==_n)
     {
        std::size_t newSize=_tables.size() ? 2*_tables.size() : init_size;
        std::vector<HashNode*> newTables;
        newTables.resize(newSize,nullptr);
        //重新哈希 
        for(std::size_t i=0;i < _tables.size();++i)
        {
           HashNode* cur=_tables[i];
           while(cur)
           {   
               HashNode* next=cur->_next;
               std::size_t hashi=hf(kot(cur->_val));
               hashi%=newSize;
               cur->_next=newTables[hashi];
               newTables[hashi]=cur;
               cur=next;
           }
           _tables[i]=nullptr;
        }
        //交换即可 
        _tables.swap(newTables);
     }
     //插入逻辑,hf需要的参数是key
     std::size_t hashi=hf(kot(val));
     hashi%=_tables.size();
     HashNode* newnode=new HashNode(val);
     newnode->_next=_tables[hashi];
     _tables[hashi]=newnode;
     ++_n;
     return true;
  }
 //删除,复用Find 
 bool Erase(const K& key)
 { 
     //节点不存在
     if(!Find(key))
     {
       return false;
     }
     KeyOfT kot;
     HashFunc hf;
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* prev=nullptr;
     HashNode* cur=_tables[hashi];
     //单链表删除逻辑,分为头删和中间删除
     while(cur)
     { 
       //查找到节点
       if(kot(cur->_val)==key)
       {
           //头删的情况 
           if(!prev)
           {
              _tables[hashi]=cur->_next;
           }
           else 
           {
              prev->_next=cur->_next;
           }
           delete cur;

           return true;
       }
        prev=cur;
        cur=cur->_next;
     }
     return false;

 }

//析构函数 
 ~HashTable()
 {
   for(std::size_t i=0;i<_tables.size();++i)
   {
        HashNode* cur=_tables[i];
        while(cur)
        {
           HashNode* next=cur->_next;
           delete cur;
           cur=next;
        }
        _tables[i]=nullptr;
   }
   _n=0;
 }
private:
   std::vector<HashNode*> _tables; //哈希表
   std::size_t _n=0;//有效载荷
};

那么对应到上层两个容器里面,我们就只要通过相应的模板参数进行控制就可以了

/*正式开始封装unordered_set*/
template<typename T>
struct SetKeyofT
{
  //把set的val转换成key 
  const T operator()(const T& val)
  {
      return val;
  }

};
template<typename K,typename HashFunc=chy::DefaultHash<K>>
class unordered_set
{
public:
  //先前封装的方法可以复用了 
  bool insert(const K& key)
  {
      return _ht.Insert(key);
  }
  //删除 
  bool erase(const K& key)
  {
     return _ht.Erase(key);
  }
private:
  //对于unordered_set来说,数据类型就是K
  Bucket::HashTable<K,K,SetKeyofT<K>,HashFunc> _ht;
};
/*对于unordered_map来说,对应的数据类型就是pair,那么对应的key就是第一个参数*/
template<typename K,typename V>
struct MapKeyofT 
{
  const K operator()(const std::pair<K,V>& kv)
  {
     
     return kv.first;
  }

};
template<typename K,typename V,typename HashFunc=chy::DefaultHash<K>>
class unordered_map
{
public:
   bool insert(const std::pair<K,V>& key)
  {
      return _ht.Insert(key);
  }
  bool erase(const K& key)
  {
      return _ht.Erase(key);
  }
private:
//对于unordered_map的第二个参数就是一个pair
  Bucket::HashTable<K,std::pair<K,V>,MapKeyofT<K,V>,HashFunc> _ht;
};

哈希表的迭代器

最后我们开始实现迭代器,根据我们的使用经验,一个迭代器要实现大致如下的几个函数:

T& operator*();
T* operator->();
Self& operator++();
bool operator==(const Self& s) const ;
bool operator!=(const Self& s) const;

所以我们可以快速搭建起来对应的哈希表的迭代器:

template<typename K,typename T,typename KeyOfT,typename HashFunc>
struct __HTIterator 
{
  typedef chy::HashNode<T> HashNode;
  typedef __HTIterator<K,T,KeyOfT,HashFunc> Self;
  __HTIterator(HashNode* node,HashTable<K,T,KeyOfT,HashFunc>* pht)
    :_node(node)
    ,_pht(pht)
    {}
  //以下都是要实现的接口  
  Self& operator++();
  bool operator==(const Self& s) const ;
  bool operator!=(const Self& s) const ;
  T& operator*();
  T* operator->();
  //数据的指针
  HashNode* _node;
  //我们需要一个哈希表,这里使用指针是一个伏笔
  HashTable<K,T,KeyOfT,HashFunc>* _pht;
};

我们这里重点讲解的就是这个++
在这里插入图片描述
所以最后++的实现代码如下:

template<typename K,typename T,typename KeyOfT,typename HashFunc>
struct __HTIterator 
{
  typedef chy::HashNode<T> HashNode;
  //类型太长了
  typedef __HTIterator<K,T,KeyOfT,HashFunc> Self;
  __HTIterator(HashNode* node,HashTable<K,T,KeyOfT,HashFunc>* pht)
    :_node(node)
    ,_pht(pht)
    {}
  Self& operator++()
  {
     if(_node->_next)
     {
        _node=_node->_next;
     }
     else 
     {
        HashFunc hf;
        KeyOfT kot;
        std::size_t hashi=hf(kot(_node->_val));
        hashi%=_pht->_tables.size();
        ++hashi;
        for(;hashi<_pht->_tables.size();++hashi)
        {
            if(_pht->_tables[hashi])
            {
               _node=_pht->_tables[hashi];
               break;
            }
        }
        //如果找到尾都没有找到,那么就把_node设为nullptr
        if(hashi==_pht->_tables.size())
        {
           _node=nullptr;
        }
     }
     return *this;
  }
  HashNode* _node;
  HashTable<K,T,KeyOfT,HashFunc>* _pht;
};

剩下的接口相对比较简单,这里直接展示代码不作过多的解释

bool operator==(const Self& s) const 
  {
       return _node==s._node;
  }
  bool operator!=(const Self& s) const 
  {
       return _node!=s._node;
  }
  T& operator*()
  {
     return _node->_val;
  }
  T* operator->()
  {
    return &_node->_val;
  }

下面,我们就来把迭代器添加到哈希表里面:

template<typename K,typename T,typename KeyOfT,typename HashFunc>
class HashTable
{
  
private:
//声明成友元  
template<typename _K,typename _T,typename _KeyOfT,typename _HashFunc>
friend struct __HTIterator;
private:
  typedef chy::HashNode<T> HashNode;
public:  
  typedef __HTIterator<K,T,KeyOfT,HashFunc> iterator;
public:
   //默认构造 
   HashTable()=default;
  //先实现查找,以便于其他功能复用 
  iterator Find(const K& key)
  { 
    //把特殊对象转换成可以取模的类型
    KeyOfT kot;
    HashFunc hf;
    //空表
     if(!_tables.size())
     {
        return End();
     }
     //开始遍历寻找 
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* cur=_tables[hashi];
     while(cur)
     {
         if(kot(cur->_val)== key) 
         {
            return iterator(cur,this);
         }
         cur=cur->_next;
     }
     return End();
  }
  //插入,复用查找 
  bool Insert(const T& val)
  {   
     KeyOfT kot;
     HashFunc hf;
     iterator pos=Find(kot(val));
    //找到了对应的节点
     if(pos!=End())
     {
        return  false;
     }
     //扩容 
     if(_tables.size()==_n)
     {
        std::size_t newSize=_tables.size() ? 2*_tables.size() : init_size;
        std::vector<HashNode*> newTables;
        newTables.resize(newSize,nullptr);
        //重新哈希 
        for(std::size_t i=0;i < _tables.size();++i)
        {
           HashNode* cur=_tables[i];
           while(cur)
           {   
              HashNode* next=cur->_next;
               std::size_t hashi=hf(kot(cur->_val));
               hashi%=newSize;
               cur->_next=newTables[hashi];
               newTables[hashi]=cur;
               cur=next;
           }
           _tables[i]=nullptr;
        }
        //交换即可 
        _tables.swap(newTables);
     }
     //插入逻辑,hf需要的参数是key
     std::size_t hashi=hf(kot(val));
     hashi%=_tables.size();
     HashNode* newnode=new HashNode(val);
     newnode->_next=_tables[hashi];
     _tables[hashi]=newnode;
     ++_n;
     return true;
  }
 //删除,复用Find 
 bool Erase(const K& key)
 { 
     //节点不存在
     if(Find(key)==End())
     {
       return false;
     }
     KeyOfT kot;
     HashFunc hf;
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* prev=nullptr;
     HashNode* cur=_tables[hashi];
     //单链表删除逻辑,分为头删和中间删除
     while(cur)
     { 
       //查找到节点
       if(kot(cur->_val)==key)
       {
           //头删的情况 
           if(!prev)
           {
              _tables[hashi]=cur->_next;
           }
           else 
           {
              prev->_next=cur->_next;
           }
           delete cur;

           return true;
       }
        prev=cur;
        cur=cur->_next;
     }
     return false;

 }
 iterator Begin()
 {
    for(std::size_t i=0;i<_tables.size();++i)
    {
        HashNode* cur=_tables[i];
        if(cur)
        {
          //this刚好是hashtable*
          return iterator(cur,this);
        }
    }
    return End();
 }
iterator End()
{
    return iterator(nullptr,this);
}
//析构函数 
 ~HashTable()
 {
   for(std::size_t i=0;i<_tables.size();++i)
   {
        HashNode* cur=_tables[i];
        while(cur)
        {
           HashNode* next=cur->_next;
           delete cur;
           cur=next;
        }
        _tables[i]=nullptr;
   }
   _n=0;
 }
private:
   std::vector<HashNode*> _tables; //哈希表
   std::size_t _n=0;//有效载荷
};

但是当前我们的哈希表还是有2个问题:

1.第一个问题:编译器在编译的时候,只会向上查找,但是哈希表的定义在后面。所以编译的时候会出错
2.第二个问题:迭代器访问了哈希表的私有成员

所以接下来我们要处理这两个问题,所以正确的代码如下所示:

//前置声明,告诉编译器这是一个类模板
template<typename K,typename T,typename KeyOfT,typename HashFunc>
class HashTable;
template<typename K,typename T,typename KeyOfT,typename HashFunc>
struct __HTIterator 
{
  typedef chy::HashNode<T> HashNode;
  typedef __HTIterator<K,T,KeyOfT,HashFunc> Self;
  __HTIterator(HashNode* node,HashTable<K,T,KeyOfT,HashFunc>* pht)
    :_node(node)
    ,_pht(pht)
    {}
  Self& operator++()
  {
     if(_node->_next)
     {
        _node=_node->_next;
     }
     else 
     {
        HashFunc hf;
        KeyOfT kot;
        std::size_t hashi=hf(kot(_node->_val));
        hashi%=_pht->_tables.size();
        ++hashi;
        for(;hashi<_pht->_tables.size();++hashi)
        {
            if(_pht->_tables[hashi])
            {
               _node=_pht->_tables[hashi];
               break;
            }
        }
        if(hashi==_pht->_tables.size())
        {
           _node=nullptr;
        }
     }
     return *this;
  }
  bool operator==(const Self& s) const 
  {
       return _node==s._node;
  }
  bool operator!=(const Self& s) const 
  {
       return _node!=s._node;
  }
  T& operator*()
  {
     return _node->_val;
  }
  T* operator->()
  {
    return &_node->_val;
  }
  HashNode* _node;
  HashTable<K,T,KeyOfT,HashFunc>* _pht;
};
template<typename K,typename T,typename KeyOfT,typename HashFunc>
class HashTable
{
  
private:
//声明成友元就可以访问对应的私有成员。
//这里需要注意一下,vs2022编译器是可以支持使用友元声明使用和外面一样的参数名称,但是g++不行  
template<typename _K,typename _T,typename _KeyOfT,typename _HashFunc>
friend struct __HTIterator;
private:
  typedef chy::HashNode<T> HashNode;
public:  
  typedef __HTIterator<K,T,KeyOfT,HashFunc> iterator;
public:
   //默认构造 
   HashTable()=default;
  //先实现查找,以便于其他功能复用 
  iterator Find(const K& key)
  { 
    //把特殊对象转换成可以取模的类型
    KeyOfT kot;
    HashFunc hf;
    //空表
     if(!_tables.size())
     {
        return End();
     }
     //开始遍历寻找 
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* cur=_tables[hashi];
     while(cur)
     {
         if(kot(cur->_val)== key) 
         {
            return iterator(cur,this);
         }
         cur=cur->_next;
     }
     return End();
  }
  //插入,复用查找 
   std::pair<iterator,bool> Insert(const T& val)
  {   
     KeyOfT kot;
     HashFunc hf;
     iterator pos=Find(kot(val));
    //找到了对应的节点
     if(pos!=End())
     {
        return  std::make_pair(pos,false);
     }
     //扩容 
     if(_tables.size()==_n)
     {
        std::size_t newSize=_tables.size() ? 2*_tables.size() : init_size;
        std::vector<HashNode*> newTables;
        newTables.resize(newSize,nullptr);
        //重新哈希 
        for(std::size_t i=0;i < _tables.size();++i)
        {
           HashNode* cur=_tables[i];
           while(cur)
           {   
              HashNode* next=cur->_next;
               std::size_t hashi=hf(kot(cur->_val));
               hashi%=newSize;
               cur->_next=newTables[hashi];
               newTables[hashi]=cur;
               cur=next;
           }
           _tables[i]=nullptr;
        }
        //交换即可 
        _tables.swap(newTables);
     }
     //插入逻辑,hf需要的参数是key
     std::size_t hashi=hf(kot(val));
     hashi%=_tables.size();
     HashNode* newnode=new HashNode(val);
     newnode->_next=_tables[hashi];
     _tables[hashi]=newnode;
     ++_n;
     return std::make_pair(iterator(newnode,this),true);
  }
 //删除,复用Find 
 bool Erase(const K& key)
 { 
     //节点不存在
     if(!Find(key))
     {
       return false;
     }
     KeyOfT kot;
     HashFunc hf;
     std::size_t hashi=hf(key);
     hashi%=_tables.size();
     HashNode* prev=nullptr;
     HashNode* cur=_tables[hashi];
     //单链表删除逻辑,分为头删和中间删除
     while(cur)
     { 
       //查找到节点
       if(kot(cur->_val)==key)
       {
           //头删的情况 
           if(!prev)
           {
              _tables[hashi]=cur->_next;
           }
           else 
           {
              prev->_next=cur->_next;
           }
           delete cur;

           return true;
       }
        prev=cur;
        cur=cur->_next;
     }
     return false;

 }
 iterator Begin()
 {
    for(std::size_t i=0;i<_tables.size();++i)
    {
        HashNode* cur=_tables[i];
        if(cur)
        {
          return iterator(cur,this);
        }
    }
    return End();
 }
iterator End()
{
    return iterator(nullptr,this);
}
//析构函数 
 ~HashTable()
 {
   for(std::size_t i=0;i<_tables.size();++i)
   {
        HashNode* cur=_tables[i];
        while(cur)
        {
           HashNode* next=cur->_next;
           delete cur;
           cur=next;
        }
        _tables[i]=nullptr;
   }
   _n=0;
 }
private:
   std::vector<HashNode*> _tables; //哈希表
   std::size_t _n=0;//有效载荷
};

处理opreator之前要先改造insert

std::pair<iterator,bool> Insert(const T& val)
  {   
     KeyOfT kot;
     HashFunc hf;
     iterator pos=Find(kot(val));
    //找到了对应的节点
     if(pos!=End())
     {
        return  std::make_pair(pos,false);
     }
     //扩容 
     if(_tables.size()==_n)
     {
        std::size_t newSize=_tables.size() ? 2*_tables.size() : init_size;
        std::vector<HashNode*> newTables;
        newTables.resize(newSize,nullptr);
        //重新哈希 
        for(std::size_t i=0;i < _tables.size();++i)
        {
           HashNode* cur=_tables[i];
           while(cur)
           {   
              HashNode* next=cur->_next;
               std::size_t hashi=hf(kot(cur->_val));
               hashi%=newSize;
               cur->_next=newTables[hashi];
               newTables[hashi]=cur;
               cur=next;
           }
           _tables[i]=nullptr;
        }
        //交换即可 
        _tables.swap(newTables);
     }
     //插入逻辑,hf需要的参数是key
     std::size_t hashi=hf(kot(val));
     hashi%=_tables.size();
     HashNode* newnode=new HashNode(val);
     newnode->_next=_tables[hashi];
     _tables[hashi]=newnode;
     ++_n;
     return std::make_pair(iterator(newnode,this),true);
  }
  

接下来,我们就可以开始封装unordered_set和unordered_map

/*正式开始封装unordered_set*/
template<typename T>
struct SetKeyofT
{
  //把set的val转换成key 
  const T operator()(const T& val)
  {
      return val;
  }

};
template<typename K,typename HashFunc=chy::DefaultHash<K>>
class unordered_set
{
public:
  typedef typename chy::Bucket::HashTable<K,K,SetKeyofT<K>,HashFunc>::iterator iterator;
  //先前封装的方法可以复用了 
  bool insert(const K& key)
  {
      return _ht.Insert(key);
  }
  //删除 
  bool erase(const K& key)
  {
     return _ht.Erase(key);
  }
  iterator begin()
  {
     return _ht.Begin();
  }
  iterator end()
  {
     return _ht.End();
  }
  
private:
  //哈希桶
  Bucket::HashTable<K,K,SetKeyofT<K>,HashFunc> _ht;
};

template<typename K,typename V>
struct MapKeyofT 
{
  const K operator()(const std::pair<K,V>& kv)
  {
     
     return kv.first;
  }

};
template<typename K,typename V>
struct MapKeyofT 
{
  const K operator()(const std::pair<K,V>& kv)
  {
     
     return kv.first;
  }
};
template<typename K,typename V,typename HashFunc=chy::DefaultHash<K>>
class unordered_map
{
public:
  //内嵌虚拟类型需要使用typename
  typedef typename Bucket::HashTable<K,std::pair<K,V>,MapKeyofT<K,V>,HashFunc>::iterator iterator;
public:
   std::pair<iterator,bool> insert(const std::pair<K,V>& key)
  {
      return _ht.Insert(key);
  }
  bool erase(const K& key)
  {
      return _ht.Erase(key);
  }
 iterator begin()
 {
     return _ht.Begin();
 }
 iterator end()
 {
     return _ht.End();
 }
 V& operator[](const K& key)
 {
    std::pair<iterator,bool> ret=insert(std::make_pair(key,V()));
    return ret->_val.second;
 }

总结

以上就是本文的主要内容,祝大家新年快乐!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值