【C++进阶】哈希表详解

文章目录

  • 哈希表是什么?
  • 哈希表的底层
  • 哈希表的插入
  • 哈希冲突
    • 闭散列
      • 线性探测
      • 二线探测
    • 开散列(哈希桶)
  • 哈希表的删除
    • 闭散列删除
    • 开散列删除
  • 哈希表的查找
  • 扩容问题
    • 闭散列扩容问题
    • 开散列扩容问题

哈希表是什么?

哈希表是一种常见的数据结构,英文名是hashtable。它和红黑树一样是用来存储数据的。我们知道红黑树查找数据的时间复杂度是O( l o g 2 N log_2 N log2N) ,也就是它的高度次。但是这样效率还是太低了,所以C++的专家们就创造了哈希表,哈希表的查找效率很高时间复杂度是O(1);

哈希表的底层

为了支持O(1)的查找效率,所以哈希表的底层由数组实现,因为数组可以支持下标访问,我们可以一次就锁定数据所在的空间地址。了解哈希表的底层之后,我们就该思考一个问题那么我们该如何插入数据呢

哈希表的插入

哈希表为了节省空间不造成空间浪费一般只开一个固定空间,等空间满了再进行扩容
我们首先假设哈希表开了10个空间。
【C++进阶】哈希表详解_第1张图片
这时我们要插入 4和6怎么插入呢?这时我们直接按下标插入就好。
【C++进阶】哈希表详解_第2张图片
这时就有个问题了,我们要插入21该怎么办呢?我们总不能扩容吧,那样太浪费空间了,还有好多空间都还没填上数据。这时就运用到了除留余数法
除留余数法的计算规则:
在这里插入图片描述
index为数组的下标
【C++进阶】哈希表详解_第3张图片

哈希冲突

在这里插入图片描述

这时我们又遇到一个问题了,我们要插入44该怎么办?因为4下标处的数据已经被占领了,那么我们44要放哪里?这就哈希冲突问题
解决哈希冲突问题的方法有三种:
线性探测
二次探测
哈希桶
前面两种也称作闭散列,后面一直称作开散列

闭散列

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

线性探测

线性探测的解决方法是我们一直往后找,直到找到空位置为止。
在这里插入图片描述
接下来我们多插入几个冲突的值来看一下。
比如14,24,34,54。
【C++进阶】哈希表详解_第4张图片
这是我们遇到了一个问题54我们要插入在哪里呢?数组的后面已经没有空间了,但是数组前面还有空间啊。所以我们往后找也要进行除留余数法
【C++进阶】哈希表详解_第5张图片
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

二线探测

【C++进阶】哈希表详解_第6张图片
经过线性探测处理后我们明显发现一个问题,是产生冲突的数据堆积在一块。这之后插入数据冲突的概率就太大了,那么我们如何避免呢?
线性探测是依次往后找空位置,而二次探测的按冲突次数的平方往后找位置
在这里插入图片描述
从这里我们可以看到冲突的位置不是那么集中了。

开散列(哈希桶)

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
【C++进阶】哈希表详解_第7张图片
这就是哈希桶,哈希桶的数组存的是节点的指针,所以从这可以看出没有哈希桶的实现后根本不怕哈希冲突。

哈希表的删除

闭散列删除

哈希表的删除其实很简单,我们用哈希表数组存放结构体时,结构体里面有两个数据,一个是要存的数据,一个是空间的状态值,空间就有三种状态(EMPTY(表示没有数据),DELETE(表示已经删除),EXIST(表示已经存在))
我们插入成功就把状态值改成EXIST,删除就直接找到对应的数据映射位置把数据该成DELETE

开散列删除

因为开散列的哈希表数组存放的是指针,所以我们要找到数据映射的位置,然后寻找数据,最后释放这个值的空间。释放前要注意这个节点后面是否还有数据,如果有先改变链接关系再释放。

哈希表的查找

哈希表的查找很简单,用数据除留余数法再用下标寻找映射位置就能找到数据。时间复杂度为O(1)。

扩容问题

闭散列扩容问题

学了之前我们知道,数组是一定有被填满的,但是我们在什么时候扩容合适呢?
这时我们就定义了一个负载因子,负载因子在[0,1]之间,当大于某个值时就扩容。
【C++进阶】哈希表详解_第8张图片
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
在这里插入图片描述
当扩容时我们就会遇到一个新的问题,我们旧哈希表的数据该怎么办,因为扩容了,空间就变大了,数据映射的位置就不一样了。那么我们就要重新把数据插入到新哈希表中。
【C++进阶】哈希表详解_第9张图片

开散列扩容问题

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
【C++进阶】哈希表详解_第10张图片

这里还有一个问题就是,我们存的的是整形数据可以直接进行取模运算,但是如果要存的数据是字符串的类型呢?我们要怎么办?
这里就要我们自己配套实现一个仿函数,如果是int就自己实现int类型的仿函数,如果是string就自己实现string类型的仿函数。然后用仿函数去计算。

你可能感兴趣的:(C++初阶和高阶,c++,散列表,数据结构)