C++:哈希:闭散列哈希表

简介: 讲解了哈希的概念以及哈希函数,简单实现了闭散列哈希。闭散列哈希的重点之一是取模操作。

 哈希的概念

哈希表就是通过哈希映射,让key值与存储位置建立关联。比如,一堆整型{3,5,7,8,2,4}在哈希表的存储位置如图所示:

LI_@OG)7U8IQ}@Z02MZLPE3.png

插入数据的操作:

在插入数据的时候,计算数据相应的位置并进行插入。

查找数据的操作:

计算key值所在的位置,并判断该位置的值是否等于key,如果等于查找成功。

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

哈希冲突

所谓哈希冲突,就是前后插入的key值通过计算,得到的存储位置的地址是相同的,这种现象就是哈希冲突,也称为哈希碰撞。可以把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。比如在上面的图中,可以看到2和4都为哈希冲突现象。

哈希函数

引起哈希冲突的原因之一可能是哈希函数的设计不合理,即计算存储地址的算法出现了不合理。

哈希函数设计原则:

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

常用的哈希函数:

①直接定址法:取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。其优点是简单切数据分布均匀。其缺点是需要事先知道关键字的分布情况,因此直接定址法适用于数据小且连续的情况。

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

 

闭散列

为了解决哈希冲突,有闭散列和开散列两种常见方法。接下来先介绍闭散列。

闭散列也叫做开放定址法,当哈希冲突的时候,如果哈希表没有被装满,说明哈希表中有其它位置,那么就把key值存放到冲突位置的下一个空位置上。(这里的下一个位置,并不是说真正的下一个位置,而是往后找,找到一个空位置)。

线性探测

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

插入步骤:①通过哈希函数获取待插入元素在哈希表中的位置。②如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

D7P{PV))84J8QS}I1X{8N3E.png

删除操作:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素。

闭散列哈希表的简单代码实现:

定义哈希表存储的节点,使用状态来表示闭散列中元素的删除或空位置。

//定义状态。用于插入删除操作
  enum State
  {
    EMPTY,
    EXIST,
    DELETE,
  };
  //每一个数据的节点
  template<class K,class V>
  struct HashData
  {
    pair<K, V> _kv;
    State _state = EMPTY;
  };

image.gif

插入操作:

插入操作的思路是拿着需要插入的数据进行取模,取模得到初步确认的下标。然后从这个下标开始寻找存储状态为EMPTY空的位置,然后插入数据。

负载因子:闭散列哈希表最好不能满,即留出一些空位置。因此我们通过负载因子来判断是否需要扩容。当负责因子大于等于0.7,即哈希表的位置已经使用了百分之七十的时候,就扩容。负责因子的计算方法是哈希表中有效数据个数/哈希表的大小。

扩容的方法:创建一个新的哈希对象,然后遍历旧的哈希表,根据旧的哈希表的数据来重新计算数据的位置。在新表插入数据的操作就是使用这个新的哈希对象调用insert函数即可。

bool Insert(const pair<K, V>& kv)
    {
      //如果存在了就直接返回false;
      if (Find(kv.first))
        return false;
      //负载因子如果大于0.7,则扩容
      if (_n * 10 / _tables.size() >= 7)
      {
        HashTable<K, V, Hash> newHt;
        //扩容原来的两倍
        newHt._tables.resize(_tables.size() * 2);
        //这一步是按照旧表中的数据插入到新表中
        for (auto& e : _tables)
        {
          //如果旧表中的数据存在,状态为EXIST,
          //那么让新表调用Insert函数,这不是递归哦!
          if (e._state == EXIST)
          {
            newHt.Insert(e._kv);
          }
        }
        //最后,让原本在vector中的旧表,与新表交换。
        _tables.swap(newHt._tables);
      }
      //不需要扩容
      Hash hf;
      //因为是泛型,不知道使用的类型是int还是char还是string
      //因此,需要获取该类型变量的值的整型,再去模size;
      size_t hashi = hf(kv.first) % _tables.size();
      while (_tables[hashi]._state == EXIST)
      {
        //线性探测
        ++hashi;
        hashi %= _tables.size();
      }
      _tables[hashi]._kv = kv;
      _tables[hashi]._state = EXIST;
      ++_n;
      return true;
    }

image.gif

删除操作:

由于直接将哈希表中的数据删除,会影响后续的其它操作,因此对于闭散列哈希表使用伪善处。把要删除的数据的状态置为DELETE即可。

bool Erase(const K& key)
    {
      Data* ret = Find(key);
      if (ret)
      {
        ret->_state = DELETE;
        --_n;
        return true;
      }
      else
      {
        return false;
      }
    }

image.gif

查找操作:

若要查找key值的话,先计算出下标,然后从这个位置开始遍历查找,当这个位置上的数据与key值相同并且其状态为EXIT,那么就返回地址。如果找不到返空指针。

Data* Find(const K& key)
    {
      Hash hf;
      size_t hashi = hf(key) % _tables.size();
      while (_tables[hashi]._state != EMPTY)
      {
        if ((_tables[hashi]._state == EXIST) && (_tables[hashi]._kv.first == key))
        {
          return &_tables[hashi];
        }
        ++hashi;
        hashi %= _tables.size();
      }
      return nullptr;
    }

image.gif

由于哈希表的数据类型是泛型,我们不知道要传入的数据类型是int还是string还是什么类型的,因此闭散列的难点之一是取模。因此我们要将key转化成整型,然后去取模。

如果原本就是整型,那么就直接返回这个值。如果是string类,那么就逐个将单个字符取出并累加起来,转为size_t类型做返回值。每获取一个字符,将其*31。因为对于字符串来说,冲突的可能很大,乘31减少冲突性。

代码如下:

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 ch : key)
    {
      hash *= 31;
      hash += ch;
    }
    return hash;
  }
};

image.gif

整体代码如下:

#pragma once
#include <iostream>
#include <vector>
#include <string>
using namespace std;
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 ch : key)
    {
      hash *= 31;
      hash += ch;
    }
    return hash;
  }
};
namespace closehash
{
  //定义状态。用于插入删除操作
  enum State
  {
    EMPTY,
    EXIST,
    DELETE,
  };
  //每一个数据的节点
  template<class K,class V>
  struct HashData
  {
    pair<K, V> _kv;
    State _state = EMPTY;
  };
  template<class K,class V,class Hash = HashFunc<K>>
  class HashTable
  {
    typedef HashData<K, V> Data;
  public:
    //初始化
    HashTable()
      :_n(0)
    {
      _tables.resize(10);
    }
    bool Insert(const pair<K, V>& kv)
    {
      //如果存在了就直接返回false;
      if (Find(kv.first))
        return false;
      //负载因子如果大于0.7,则扩容
      if (_n * 10 / _tables.size() >= 7)
      {
        HashTable<K, V, Hash> newHt;
        //扩容原来的两倍
        newHt._tables.resize(_tables.size() * 2);
        //这一步是按照旧表中的数据插入到新表中
        for (auto& e : _tables)
        {
          //如果旧表中的数据存在,状态为EXIST,
          //那么让新表调用Insert函数,这不是递归哦!
          if (e._state == EXIST)
          {
            newHt.Insert(e._kv);
          }
        }
        //最后,让原本在vector中的旧表,与新表交换。
        _tables.swap(newHt._tables);
      }
      //不需要扩容
      Hash hf;
      //因为是泛型,不知道使用的类型是int还是char还是string
      //因此,需要获取该类型变量的值的整型,再去模size;
      size_t hashi = hf(kv.first) % _tables.size();
      while (_tables[hashi]._state == EXIST)
      {
        //线性探测
        ++hashi;
        hashi %= _tables.size();
      }
      _tables[hashi]._kv = kv;
      _tables[hashi]._state = EXIST;
      ++_n;
      return true;
    }
    Data* Find(const K& key)
    {
      Hash hf;
      size_t hashi = hf(key) % _tables.size();
      while (_tables[hashi]._state != EMPTY)
      {
        if ((_tables[hashi]._state == EXIST) && (_tables[hashi]._kv.first == key))
        {
          return &_tables[hashi];
        }
        ++hashi;
        hashi %= _tables.size();
      }
      return nullptr;
    }
    bool Erase(const K& key)
    {
      Data* ret = Find(key);
      if (ret)
      {
        ret->_state = DELETE;
        --_n;
        return true;
      }
      else
      {
        return false;
      }
    }
  private:
    vector<Data> _tables;//将每个数据放到vector中
    size_t _n = 0;//哈希表中存储的有效数据的个数
  };
}

image.gif

相关文章
|
4月前
|
存储 监控 算法
基于 C++ 哈希表算法实现局域网监控电脑屏幕的数据加速机制研究
企业网络安全与办公管理需求日益复杂的学术语境下,局域网监控电脑屏幕作为保障信息安全、规范员工操作的重要手段,已然成为网络安全领域的关键研究对象。其作用类似网络空间中的 “电子眼”,实时捕获每台电脑屏幕上的操作动态。然而,面对海量监控数据,实现高效数据存储与快速检索,已成为提升监控系统性能的核心挑战。本文聚焦于 C++ 语言中的哈希表算法,深入探究其如何成为局域网监控电脑屏幕数据处理的 “加速引擎”,并通过详尽的代码示例,展现其强大功能与应用价值。
100 2
|
6月前
|
存储 算法 数据处理
公司局域网管理中的哈希表查找优化 C++ 算法探究
在数字化办公环境中,公司局域网管理至关重要。哈希表作为一种高效的数据结构,通过哈希函数将关键值(如IP地址、账号)映射到数组索引,实现快速的插入、删除与查找操作。例如,在员工登录验证和设备信息管理中,哈希表能显著提升效率,避免传统线性查找的低效问题。本文以C++为例,展示了哈希表在局域网管理中的具体应用,包括设备MAC地址与IP分配的存储与查询,并探讨了优化哈希函数和扩容策略,确保网络管理高效准确。
|
3月前
|
存储 缓存 Java
c++的哈希表、哈希桶的介绍与实现
这一篇文章大致实现详细介绍什么是哈希,然后再介绍什么是哈希表,怎么代码实现哈希表,然后再介绍哈希桶,怎么代码实现哈希桶,最后再介绍他俩有什么细节上的差别,与代码的一些细节优化。
78 0
|
5月前
|
存储 监控 算法
基于 C++ 哈希表算法的局域网如何监控电脑技术解析
当代数字化办公与生活环境中,局域网的广泛应用极大地提升了信息交互的效率与便捷性。然而,出于网络安全管理、资源合理分配以及合规性要求等多方面的考量,对局域网内计算机进行有效监控成为一项至关重要的任务。实现局域网内计算机监控,涉及多种数据结构与算法的运用。本文聚焦于 C++ 编程语言中的哈希表算法,深入探讨其在局域网计算机监控场景中的应用,并通过详尽的代码示例进行阐释。
113 4
|
8月前
|
存储 算法 安全
基于哈希表的文件共享平台 C++ 算法实现与分析
在数字化时代,文件共享平台不可或缺。本文探讨哈希表在文件共享中的应用,包括原理、优势及C++实现。哈希表通过键值对快速访问文件元数据(如文件名、大小、位置等),查找时间复杂度为O(1),显著提升查找速度和用户体验。代码示例展示了文件上传和搜索功能,实际应用中需解决哈希冲突、动态扩容和线程安全等问题,以优化性能。
|
11月前
|
存储 算法 C++
【算法】哈希映射(C/C++)
【算法】哈希映射(C/C++)
|
存储 搜索推荐 Serverless
【C++航海王:追寻罗杰的编程之路】哈希的应用——位图 | 布隆过滤器
【C++航海王:追寻罗杰的编程之路】哈希的应用——位图 | 布隆过滤器
83 1
|
存储 Serverless C++
【C++】手撕哈希表的闭散列和开散列
【C++】手撕哈希表的闭散列和开散列
149 1
|
存储 缓存 NoSQL
【C++】哈希容器
【C++】哈希容器
|
存储 Serverless C++
【C++航海王:追寻罗杰的编程之路】一篇文章带你认识哈希
【C++航海王:追寻罗杰的编程之路】一篇文章带你认识哈希
116 0