哈希(hash、散列表)

哈希的概念


可以认为哈希有着数组的思想,它将所有的数据段拼成一个数组(顺序表)进行存储,通过哈希函数,可以基本上以O(1)的时间复杂度来查找和存储数据。

哈希函数

通过数据段中的唯一关键字(key),经过某种算法,得出此数据段在整个哈希数组(顺序表)中的的下标,然后直接取元素即可。

哈希的存储(插入)过程

有数据结构如下:

//数据段(key,value)
typedef struct Data
{
    int key;
    ......(数据);
}Data;
//哈希表
//可以动态的从堆中申请连续的空间,将地址赋给table之后table就可以按照数组的方式来使用了
typedef struct Hash
{
    Data *table;
    int size;
    int capacity;
    int (*HashFunc)(int);
}

有一系列key数据:
{23,34,45,56,67,78,89,12,24}

因为数据段中的key不一定从零开始,并且不一定连续,所以我们需要哈希函数来计算下标

哈希函数方法一 —— 直接取余法:

  • 用关键字直接模哈希表的大小,将数据段存储在哈希表中下标位余数的位置

那么函数就可以定义如下:

int HashFunc(int key, int capacity)
{
    return key/capacity;
}

此时,一个简单的哈希表就完成了。
但是,好像有问题,因为会出现两个不一样的key经过哈希函数最后得出来的下标有可能相同,此时,就需要进行冲突解决了。

哈希的冲突解决之线性探测

当一个key计算出的的下标和之前一个key计算出来的下标相同,那么就从此下标挨个向后找,碰到第一个空的就将数据存进去
所以最后哈希数组的存储情况为:

线性探测

这里的12和24计算的下标都是0,所以按照线性探测规则,将24存在了1下标处

哈希表的操作

哈希表的插入

首先哈希表是空的,所以我们只需要按照哈希函数的方法以及解决冲突的方法插入即可。

哈希表的查找

哈希表的查找可以和插入是相同的,按照哈希函数的方法和约定的冲突方法就可以很快找出想找的数据,但是如果找的时候向碰到了空,那么说明此哈希表中没有此数据。

哈希表的删除

哈希表的删除是不能世界进行删除的,为什么呢?
我用上面的例子:
例如现在想删除12这个key所对应的数据,如果直接删除的话,那么再次寻找24的数据就会先找到0号空间,那么一找到空的空间,查找的函数会判定此哈希函数没有此数据,但其实表中是存在的。

状态假删除

发生如上的情况的话话那就可以通过给每个空间设置一个状态,判断是为没数据但已删除 还是有数据没删除 ,这样在删除数据的时候将空间的状态置成没数据但已删除,在给查找的方法增加判断状态的机制,这样就很容易解决这个问题。

哈希表的扩容

我们这里举的例子是一个数组,所以不方便扩容,但在实际使用的时候,哈希表是很容易需要扩容的。

  • 扩容条件(负载因子)

    • 当哈希表里面的数据占哈希表容量的70%~80%之间的时候,我们是需要扩容的,否则会出现大量的冲突,降低查找效率
  • 扩容容量选择:

    • 一般用素数做容量大小能降低哈希冲突,容量一般选择大于等于旧的容量的两倍

哈希函数方法二 —— 二次探测法(减少冲突)

如果下标值已经被占用,那就先给key值加上冲突次数的平方然后再进行取余。

int HashFunc(int key,int capacity, int times)
{
    return (key+ times*times)/capacity;
}

优点:

  • 打散存储机制,减少冲突

哈希函数方法三 —— 开散列(拉链法)

哈希表的每一个下标不止放一个数据段,即每个下标所对应的类型是一个数据段的集合
此集合可以为数组结构,树形结构,链表结构,或者哈希结构,此集合就叫做哈希桶(HashBuket)。
再次举例一下链表结构的数据结构:

//首先是数据段,和上面定义的数据段是一样的
//然后是一个链表
typedef struct SList
{
    Data data,
    struct SList *pNext;
    enum State state;//用作状态假删除的状态量
}SList;
//哈希表
typedef struct Hash
{
    SList *table;//可以再堆中申请一连续的SList空间,并把他们当作数组使用
    int size;
    int capacity;
    int (*HashFunc)(int);//计算出数组的下标
}

结构图:
开散列
注:队中的元素全都是SList结构体,table是一个有头的链表。

查找的时候先用哈希函数计算出下标,决定进入那个链表,然后再进行遍历链表。
这样的方法会频繁的出现数据拥堵的现象,因为查找的时候是编译,这样会降低效率,所以如果使用树形结构,或者哈希结构,这样查找的效率是非常高的。

哈希的多元化

哈希的key不只可以为整型,其实还可以是其他类型的数据,只要是做出合理的哈希函数即可

哈希的安全问题

如果别人知道了哈希函数的算法,那么数据将会不安全,因为一般用户操作的是key值,并且只能根据所给的选项正常的修改数据,如果一些人知道了哈希算法,那么他们就可以通过此算法找到实际的数据,然后任意的修改,这样是不安全的,所以再写哈希的算法的时候可以哦那个过使用加上一些随机的因子来保证数据的安全

猜你喜欢

转载自blog.csdn.net/qq_38590948/article/details/81462928