线性探测/闭散列法实现哈希表

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

        哈希表是一种不用遍历,可以随机访问元素,实现时间复杂度为O(1)的一种数据结构,大大节省了时间。

一. 哈希表定义

        我们一般定义一个数组用来表示一个哈希表的存储元素的空间,并且通过哈希函数来实现确定某个元素的存入位置、查找某个元素、删除某个元素。

哈希函数

        我们定义哈希函数,一般采用除整取余法:hash(key) = key % m(m为内存单元个数,可以自己定义)。这里我们定义为10。我们就可以根据该哈希函数计算元素的存储位置:

        hash(1) = 1;hash(2) = 2;hash(8) = 8;hash(5) = 5;hash(10) = 0

        所以,这组数据存储如下:


这样,我们就可以根据数组下标随机访问我们想要查找的元素,实现时间复杂度O(1)。

        这里的每一个下标对应一个元素,就类似于一个中文单词对应有一个英文单词,这个我们叫做键值对

二. 哈希冲突

        利用以上所说的哈希函数来存储元素时,我们可能会遇到取到的余数相同,即存储位置相同。比如说hash(1)=1,hash(11)=1,但是下标为1的位置我们只能存入一个元素,这样的情况就叫做哈希冲突。

        为了解决哈希冲突,当然相应的也会有很多办法。其中,我们最常用的有两种:开散列法、闭散列法。

开散列法:每个下标对应的元素不是单纯的只放一个元素,而是存放一个链表,将相同余数的元素当做一个结点都放在链表上。(该方法见另一篇博客开散列实现哈希表

闭散列法:发生冲突时,若要插入位置已有元素,但是哈希表还有存储位置,就线性向后探测,遇到的第一个可插入元素位置就可以进行操作。(本文即使用该方法实现哈希表

负载因子:因为使用哈希表的目的是实现时间上的好处,避免了遍历,可以随机访问,使得操作变得简单并且高效。但是若哈希表中存储了很多元素,这样产生哈希冲突的可能性就很大,使用哈希表目的也得不到实现。这个时候就有了负载因子的出现,负载因子就是控制哈希表中存入元素的个数,若是存入元素的个数占哈希表可存元素的比例,达到或超过了负载因子,这个时候我们就应该扩充哈希表,或者是停止使用当前的哈希表。

三. 哈希表的实现——闭散列法

1. 哈希表的结构定义

//除整取余法:线性向后探测存放
#pragma once

#include <stdio.h>

#define SHOW_NAME printf("\n====================%s====================\n", __FUNCTION__);
#define HASHMAXSIZE 1000

typedef int KeyType;
typedef int ValType;
typedef size_t (*HashFunc)(KeyType key);//函数指针,用来调用哈希函数,控制元素存入位置                      

typedef enum
{
    Empty,//表示当前元素为空状态
    Valid,//表示当前元素为有效状态
    Deleted,//表示当前元素为被删除状态
}Stat;

typedef struct HashElem//表示哈希表中的一个元素,该元素包括键值对
{
    KeyType key;
    ValType val;
    Stat stat;//表示当前元素的状态
}HashElem;
typedef struct HashTable
{
    HashElem data[HASHMAXSIZE];
    HashFunc func;
    size_t size;//表示当前哈希表中有效元素的个数,哈希表不是线性结构,所以不能用[0,size)表示有效元素区间
}HashTable;      

        如上代码,我们定义一个枚举用来表示每个元素的状态,除了有效之外,其他两种状态我们都可以对它进行插入操作。我们在哈希表中存的每一个元素包括key与它对应的值,以及该元素的状态。整个哈希表我们用一个数组表示,调用一个函数用作实现该哈希表的哈希函数,哈希表中还包括一个size用以计算当前哈希表中存入了多少了元素。下面就是哈希表的基本操作:

2. 初始化

size_t HashFuncDefault(KeyType key)//哈希函数                                                              
{
    return key%HASHMAXSIZE;
}
void HashInit(HashTable* ht, HashFunc func)//初始化
{
    ht->size = 0;
    ht->func = func;
    size_t i = 0;
    for(i=0; i<HASHMAXSIZE; ++i)
    {   
        ht->data[i].stat = Empty;
    }   
    return;
}

3. 销毁

//2.销毁
void HashDestroy(HashTable* ht)//销毁
{
    ht->size = 0;
    ht->func = NULL;
    size_t i = 0;
    for(i=0; i<HASHMAXSIZE; ++i)
    {
        ht->data[i].stat = Empty;
    }
    return;
}       

3. 打印函数,用以测试

//3.打印函数,用于测试
void HashPrint(HashTable* ht, const char* msg)//打印函数
{
    printf("[%s]\n", msg);
    if(ht == NULL)
        return;
    size_t i = 0;
    for(; i<HASHMAXSIZE; ++i)
    {   
        if(ht->data[i].stat == Valid)
        {   
            printf("%d: [%d:%d]\n", i, ht->data[i].key, ht->data[i].val);
        }   
    }   
    return;
}

4. 插入操作

(1)根据负载因子判断当前哈希表是否能继续插入

(2)根据key调用哈希函数计算插入位置offset

(3)若当前offset状态不为有效,直接进行插入操作

(4)若当前offset状态有效,就线性向后探测直至遇到一个不为有效状态的位置,进行插入

(5)插入操作结束之后,++size

//4.插入操作
void HashInsert(HashTable* ht, KeyType key, ValType val)//插入
{
    if(ht == NULL)//非法输入
        return;
    //1.根据负载因子判断哈希表是否可以继续插入
    //我们约定负载因子为0.8
    if(ht->size >= 0.8*HASHMAXSIZE)
    {
        //当前哈希表已达到负载因子上限,插入失败
        return;
    }
    //2.根据key计算插入位置offset
    size_t offset = ht->func(key);
    while(1)
    {
        //3.若offset位置状态不为Valid,直接插入
        if(ht->data[offset].stat != Valid)
        {
            ht->data[offset].stat = Valid;
            ht->data[offset].key = key;
            ht->data[offset].val = val;
            //6.插入结束后++size
            ++ht->size;
            return;
        }
        //5.若发现哈希表中已有key相同的元素,我们约定为插入失败
        //也可以约定为继续向后查找插入或者替换已有的key键值对,这个由程序员自行决定
        else if(ht->data[offset].key == key && ht->data[offset].stat == Valid)
        {
            return;
        }
        //4.当前offset位置状态是Valid,线性向后查找,找到第一个不为Valid状态的位置插入
        else
        {                                                                                                                                                               
            ++offset;
            //若已探测到数组最后,就从头开始
            if(offset >= HASHMAXSIZE)
                offset = 0;
        }

5. 给定一个key值,查找对应的value值

(1)根据key调用哈希函数计算key对应的位置offset

(2)若当前offset状态有效且key与查找的key相同,查找成功

(3)若当前offset状态有效,但key不相同,就线性向后探测直至遇到一个不为有效状态的位置,查找失败

//5.给定一个key,查找对应的val
int HashFind(HashTable* ht, KeyType key, ValType* val)//查找
{
    if(ht == NULL || val == NULL)//非法操作
        return 0;
    if(ht->size == 0)//哈希表为空
        return 0;
    //1.根据key计算offset
    size_t offset = ht->func(key);
    while(1)
    {
        //2.从offset开始线性向后查找
        //3.找到了相同的key,返回val,操作成功
        if(ht->data[offset].key == key && ht->data[offset].stat == Valid)
        {
            *val = ht->data[offset].val;
            return 1;
        }
        //5.查找过程直至遇到一个不为Valid状态的元素,说明查找失败
        else if(ht->data[offset].stat != Valid)
            return 0;
        //4.找不到相同的key,继续向后查找
        else
        {
            offset++;
            if(offset >= HASHMAXSIZE)
                offset = 0;
        }                                                                                                                                                               
    }
}

6. 给定一个key,删除值为key的元素

    这里查找过程与上面相同,删除过程即将该元素的状态设置为Deleted即可,要注意的是删除之后要--size。

//6.给定一个key,删除对应元素
void HashRemove(HashTable* ht, KeyType key)//删除操作
{
    if(ht == NULL)//非法操作
        return;
    if(ht->size == 0)//空哈希表
        return;
    //1.根据key计算offset
    size_t offset = ht->func(key);
    //2.从offset线性向后查找要删除元素
    while(1)
    {
        //3.若当前key与要删除的key相同,删除元素即将其状态设置为Deleted
        if(ht->data[offset].key == key && ht->data[offset].stat == Valid)
        {
            ht->data[offset].stat = Deleted;
            --ht->size;
            return;
        }
        //4.若查找过程中遇到状态为Empty的元素,则查找失败
        else if(ht->data[offset].stat == Empty)
            return;
        //5.除3、4情况以外,++offset,线性探测下一个元素
        else
        {
            ++offset;
            offset = offset >= HASHMAXSIZE ? 0 : offset;
        }
    }
}

7. 以下为以上函数的测试代码

void TestInit()
{
    SHOW_NAME;
    HashTable ht;
    HashInit(&ht, HashFuncDefault);
    printf("expected is 0, actual is %d\n", ht.size);
    printf("expected is %p, actual is %p\n", HashFuncDefault, ht.func);
}

void TestDestroy()
{
    SHOW_NAME;
    HashTable ht;
    HashInit(&ht, HashFuncDefault);
    HashDestroy(&ht);
    printf("expected is 0, actual is %d\n", ht.size);
    printf("expected is nil, actual is %p\n", ht.func);
}

void TestInsert()
{
    SHOW_NAME;
    HashTable ht;
    HashInit(&ht, HashFuncDefault);
    HashInsert(&ht, 1, 1);
    HashInsert(&ht, 2, 2);
    HashInsert(&ht, 1001, 3);
    HashInsert(&ht, 1002, 4);
    HashPrint(&ht, "插入4个元素");
}

void TestFind()
{
    SHOW_NAME;
    HashTable ht;                                                                                                                                                       
    HashInit(&ht, HashFuncDefault);
    HashInsert(&ht, 1, 1);
    HashInsert(&ht, 2, 2);
    HashInsert(&ht, 1001, 3);
    HashInsert(&ht, 1002, 4);
    HashPrint(&ht, "插入4个元素");
    int value;
    int ret = HashFind(&ht, 1001, &value);
    printf("expected is 3, actual is %d\n", value);
    ret = HashFind(&ht, 2001, &value);
    if(ret == 0)
        printf("查找失败\n");
}

void TestRemove()
{
    SHOW_NAME;
    HashTable ht;
    HashInit(&ht, HashFuncDefault);
    HashInsert(&ht, 1, 1);
    HashInsert(&ht, 2, 2);
    HashInsert(&ht, 1001, 3);
    HashInsert(&ht, 1002, 4);
    HashPrint(&ht, "插入4个元素");
    HashRemove(&ht, 1001);
    HashPrint(&ht, "删除key=1001的元素");
    HashRemove(&ht, 2001);
    HashPrint(&ht, "删除key=2001的元素");
}


猜你喜欢

转载自blog.csdn.net/lycorisradiata__/article/details/80409109