C语言数据结构之Hash散列表

 更新时间:2023年08月29日 09:02:01   作者:Abstracted  
这篇文章主要介绍了C语言数据结构之Hash散列表,散列表(哈希表)其思想主要是基于数组支持按照下标随机访问数据,时间复杂度为O(1)的特性,可以说是数组的一种拓展,需要的朋友可以参考下

一、散列表

1.1 散列表

散列表(哈希表),其思想主要是基于数组支持按照下标随机访问数据,时间复杂度为O(1)的特性。

可以说是数组的一种拓展。

假设,我们为了方便记录某高校数学专业的某个学生的信息,要求可以按照学号(入学时间+年级+专业+专业内自增序号,如2019 1001 0002)能够快速找到某个学生的信息。

这个时候我们可以取学号的自增序号部分,即后四位作为数组的索引下表,把学生相应的信息存储到对应的内存空间内即可。

在这里插入图片描述

如上图所示,我们把学号作为Key,通过截取学号后四位的函数计算后得到索引下标,将数据存储到数组中。

当我们按照键值(学号)查找时,只需要再次计算出索引下标,然后取出相应数据即可。

以上便是散列思想。

计算下标的函数我们叫它为 散列函数 或 哈希函数

1.2 散列函数

上面的例子中,截取学号后四位的函数就是一个简单的散列函数。

//散列函数 伪代码 
int Hash(string key) {
  // 获取后四位字符
  string hashValue =int.parse(key.Substring(key.Length-4, 4));
  // 将后两位字符转换为整数
  return hashValue;
}

在这里散列函数的作用就是将Key值映射成数组的索引下标。

关于散列函数的设计方法有很多,如:直接寻址法、数字分析法、随机数法、取余法等等。

但即使再优秀的设计方法也不能够避免散列冲突。

在散列表中散列函数不应设计的太复杂,不然太复杂的散列函数会造成更多的性能消耗,结果就适得其反了。

1.3 散列冲突

在这里插入图片描述

就像图上所表达的一样,不同的key通过散列函数计算出来的hash值相同,造成了这两个数据都需要存储在这个同一个下标(内存空间)上。

散列函数具有确定性和不确定性:

  • 确定性:哈希的散列值不同,那么哈希的原始输入绝对不相同。即hash(key1) ≠ hash(key2),key1≠key2
  • 不确定性:同一个散列值很有可能对应多个不同的原始输入。即hash(key1) =hash(key2),key1≠key2

散列冲突:即key1≠key2,hash(key1)=hash(key2)的情况,散列冲突是不可避免的,如果我们key的个数为100,而数组的索引数量只有50,那么再优秀的算法也无法避免散列冲突。

关于散列冲突也有很多解决办法,这里简单说明两种: 开放寻址法 和 链表法 。

1.3.1 开放寻址法

开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。

比如我们可以使用线性探测法。当我们散列表中插入数据时,如果某个数据的key经过散列函数散列之后,散列值对应的存储位置(下标)已经被占用,那么就从这个被占用的位置开始,依次往后进行寻找空闲的位置,如果遍历到尾部都没有找到空闲的位置,那么我们就在从表头开始找,知道找到为止。

在这里插入图片描述

散列表中查找元素的时候,我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素中的key和要查找的元素的key,如果相等则说明我就是我们要找的元素,否则就按顺序往后依次查找。如果遍历到数组中的空闲位置还是没有找到想要的元素,就说明我们的找的元素不在该散列表中。

好,虽然使用开放寻址法完成了键值对的存储和查找,但是对删除操作稍微有些特别,不能单纯的把要删除的元素设置为空。因为在查找的时候,我们通过线性探测法就会按顺序向后面的内存空间(下标)一个一个的找,直到找到对应的value或空为止,但是如果这时候碰到一个内存空间(下标)中的值是空,而这个空位置是我们删除了其他元素后置为空的,就导致这个查找算法失效。 我们可以将删除的元素,特殊的标记为deleted。当线性探测查找的时候,遇到标记为deleted的空间并不是停下来,而是继续向下探测。

线性探测法存在很大的问题。当散列表中插入的数据越来越多时,其散列冲突的可能性就会越来越大,在极端情况下甚至要探测整个散列表,因此最坏的时间复杂度为O(N)。在开放寻址法中,除了线性探测法,我们可以使用二次探测和双重散列等方式。

1.3.2 链表法

链表法是一种比较常见的散列解决办法,Java的HashMap和Redis的Hash结构就是使用的链表法来解决散列冲突。 链表法的原理:如果遇到冲突,就会在原地址上新建一个空间,让已经存在的元素中的next属性指向当前空间的地址。以链表节点的形式插入到该空间。 当插入的时候我们只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可。

在这里插入图片描述

1.3.3 负载因子loadFactory与rehash

我们可以使用负载因子来衡量散列表的“健康状况”。

散列表的负载因子 = 填入表中的元素个数 ÷ 散列表的长度(数组的长度)

散列表的负载因子越大,代表空闲位置越少,冲突也就越多,散列表的性能会下降。对于散列表来说,负载因子过大或过小都不好,因为负载因子是决定散列表的数据密度。

负载因子过大,散列表的数据密度就会高,链表的长度就会长,导致散列表的性能下降。负载因子过小,散列表很容易就会触发扩容,则会造成内存不能合理使用,从而形成内存浪费。

因此我们为了保证负载因子维持在一个合理的范围内,要对散列表的大小进行收缩或拓展,即rehash。

散列表的rehash过程类似于数组的扩容,但是相比要多出来数据的从新排列,更换空间位置等复杂操作。

1.3.4 开放寻址法与链表比较

对于开放寻址解决冲突的散列表,由于数据都存储在数组中,因此可以有效的利用CPU缓存加快查询速度(数组占用内存中连续的空间)。但是删除数据时比较麻烦,需要标记已删除的元素为deleted。而且开放寻址法中,所有数据都存储在一个数组中,比起链表来说,冲突的代价更高,*(每次都要遍历数组,遍历到数组尾部时再转向数组头部开始遍历,仍没有找到位置便需要扩容。)*所以使用开放寻址法解决冲突的散列表,负载因子的上限不能太大,也就是数据的密度不能太高。将导致比链表法更浪费内存空间。

对于链表法解决冲突的散列表,堆内存的利用率要比开放寻址法要高。因为链表结点可以在需要的时候再创建,并不需要像开放寻址法那样提前准备好内存空间。链表法比起开放寻址法对装载因子的容忍度更高。开放寻址法只能适用装载因子小于1的情况。接近1时,就可能会有大量的散列冲突,性能会下降很多。但是对于链表法,只要散列函数的值随机均匀,即便装载因子变成1,也就是链表变长了而已,虽然查找效率要有所下降,但是比起开放寻址的线性探测顺序查找还是要快的很多。但是链表要存储指针,所以对于比较小的存储对象,是比较消耗内存的,性价比并不高。而且链表中的节点是零散分布在内存中,不是连续的,所以对CPU缓存不是很友好,这对于执行效率有一定的影响,但是链表中的每个节点有相互连接,所以影响不大。

到此这篇关于C语言数据结构之Hash散列表的文章就介绍到这了,更多相关Hash散列表内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C及C++ 基础循环示例详解

    C及C++ 基础循环示例详解

    这篇文章主要介绍了C及C++ 中的循环示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • c语言的形参和实参传递的区别详解

    c语言的形参和实参传递的区别详解

    这篇文章主要介绍了c语言的形参和实参传递的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • C语言编程gcc如何生成静态库.a和动态库.so示例详解

    C语言编程gcc如何生成静态库.a和动态库.so示例详解

    本文主要叙述了gcc如何生成静态库(.a)和动态库(.so),帮助我们更好的进行嵌入式编程。因为有些时候,涉及安全,所以可能会提供静态库或动态库供我们使用
    2021-10-10
  • 关于C语言一维数组算法问题详解

    关于C语言一维数组算法问题详解

    数组是以顺序格式排列的均匀数据的集合,在C语言中学习数组的概念非常重要,因为它是基本的数据结构,这篇文章主要给大家介绍了关于C语言一维数组算法问题的相关资料,需要的朋友可以参考下
    2021-11-11
  • 去掉vs2010中ipch文件和.sdf文件的解决方法

    去掉vs2010中ipch文件和.sdf文件的解决方法

    本篇文章介绍了,在vs2010中产生的ipch文件和.sdf文件的解决方法。需要的朋友参考下
    2013-05-05
  • linux之awk命令的用法

    linux之awk命令的用法

    awk是一个非常棒的数字处理工具。相比于sed常常作用于一整行的处理,awk则比较倾向于将一行分为数个“字段”来处理。运行效率高,而且代码简单,对格式化的文本处理能力超强
    2013-10-10
  • C++存储方案和动态分配

    C++存储方案和动态分配

    这篇文章主要介绍了C++存储方案和动态分配,
    2021-12-12
  • 顺序线性表的代码实现方法

    顺序线性表的代码实现方法

    下面小编就为大家带来一篇顺序线性表的代码实现方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-04-04
  • C++设计类不能被继承的方法实例讲解

    C++设计类不能被继承的方法实例讲解

    在Java 中定义了关键字final,被final修饰的类不能被继承,C++中如何实现,下面我们来看一个例子
    2013-12-12
  • C语言实现单链表逆序与逆序输出实例

    C语言实现单链表逆序与逆序输出实例

    这篇文章主要介绍了C语言实现单链表逆序与逆序输出,是数据结构与算法中比较基础的重要内容,有必要加以牢固掌握,需要的朋友可以参考下
    2014-08-08

最新评论