哈希表的基本操作(一):线性探测法解决哈希冲突

哈希概念

在之前学习过的顺序搜索和二叉树搜索中,元素存储位置和元素各关键码之间没有对应关系,因此在查找一个元素时,必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数。
我们希望可以不经过任何比较,一次直接从表中得到想要的元素,这样一来,搜索效率就有了质的提高。如果构造一种存储结构,通过某种函数是元素的存储位置与他的关键码之间能够建立一一映射的关系,那么在查找的时候通过该函数就可以很快的找到该元素。
当向该结构中:
插入元素时:根据待插入元素关键码,以此计算出该元素的存储位置进行存放
搜索元素时:对元素的关键码进行同样的计算,把求的函数值当做元素的位置,在结构中按此位置取元素比较,若相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(散列表)。

哈希冲突

我们使用同一个哈希函数来计算不止一个的待存放的数据在表中的存放位置,总是会有一些数据通过这个转换函数计算出来的存放位置是相同的,这就是哈希冲突也就是说,不同的关键字通过同一哈希转换函数计算出相同的哈希地址。

处理哈希冲突
针对哈希冲突,这里有两种解决办法:闭散列和开散列

闭散列:(开放地址法或者也叫线性探测法)
当我们要往哈希表中插入一个数据时,通过哈希函数计算该值的哈希地址,当我们找到哈希地址时却发现该位置已经被别的数据插入了,那么此时我们就找紧跟着这一位置的下一个位置,看是否能够插入,如果能则插入,不能则继续探测紧跟着当前位置的下一个位置。
这里写图片描述

下面我们使用C语言来实现哈希表的基本操作:

线性探测法实现:

//hash.h文件内容
#pragma once
#define max_size 1000
typedef int KeyType;
typedef int ValType;
typedef char DataType;
typedef int (*HashFunc)(KeyType key);

typedef enum Stat
{
    Deleted,//删除状态
    Valid,//有效状态
    Empty,//空状态(即无效状态)
}Stat;

typedef struct HashElem
{
    KeyType key;
    ValType value;
    Stat stat;
}HashElem;

typedef struct HashTable
{
    HashElem data[max_size];
    int size;//有效元素个数
    HashFunc func;//哈希函数
}HashTable;

//初始化
void HashInit(HashTable *ht,HashFunc Hash_func);
//销毁
void HashDestroy(HashTable *ht);
//插入数据
void HashInsert(HashTable *ht,KeyType key,ValType value);
//查找数据
int HashFind(HashTable *ht,KeyType key,ValType *value);
//删除数据
void HashRemove(HashTable *ht,KeyType key);

以下是对应函数的实现以测试结果:

//初始化
void HashInit(HashTable *ht,HashFunc Hash_func)
{
    if(ht == NULL)
    {
        //非法输入
        return;
    }
    ht->size = 0;
    ht->func = Hash_func;
    int i = 0;
    for(;i < max_size;i++)
    {
        //将哈希表的每一个位置都初始化为空状态
        //代表相应的位置是未被使用过的
        ht->data[i].stat = Empty;
    }
}
//销毁
void HashDestroy(HashTable *ht)
{
    if(ht == NULL)
    {
        //非法输入
        return;
    }
    //先将表中的每一个位置都置为无效状态
    int i = 0;
    for(;i < max_size;i++)
    {
        ht->data[i].stat = Empty;
    }
    //再将有效元素个数清0
    ht->size = 0;
    //哈希函数指向空
    ht->func = NULL;
}
//测试一下
void TestInit()
{
    Test_Header;
    HashTable ht;
    HashInit(&ht,Hash_func);
    printf("expect size = 0,actual size = %d\n",ht.size);
    printf("expect func = %p,actual func = %p\n",Hash_func,ht.func);
}

测试结果:
这里写图片描述

//插入数据
void HashInsert(HashTable *ht,KeyType key,ValType value)
{
    if(ht == NULL)
    {
        //非法输入
        return;
    }
    //判定当前的hash表能否继续插入
    //假设负载因子为0.8
    if(ht->size >= 0.8*max_size)
    {
        //当前hash表已经达到负载因子的上限,不能再继续插入
        return;
    }
    //由key计算offset(由hash函数计算出的存放位置的下标)
    int offset = ht->func(key);
    //但是该位置可能之前已经被别的数据占据了
    //所以我们需要先判断当前计算出的位置是否能放入当前数据
    //如果不能就从offset位置往后查找
    while(1)
    {
        //先判断当前计算出的位置是否能放入当前数据
        if(ht->data[offset].stat != Valid)
        {
            //一旦找到一个位置不是有效位置
            //就可以将该数据插入
            //这就是处理哈希冲突的线性探测法
            ht->data[offset].key = key;
            ht->data[offset].value = value;
            //插入完成以后将该位置置成有效状态
            ht->data[offset].stat = Valid;
            //哈希表有效元素个数+1
            ++ht->size;
            return;
        }
        //走到这里说明当前计算出的位置
        //不能放置当前待插入的数据
        //判断当前位置的元素是否和待插入的元素一样
        else if(ht->data[offset].stat == Valid \
                && ht->data[offset].key == key)
        {
            //说明存在相同元素
            //我们这里约定该哈希表中不存在重复元素
            //则直接插入失败返回
            return;
        }
        //则更新offset值继续下一次循环往后查找
        else
        {
            ++offset;
            if(offset >= max_size)
            {
                //如果查找时offset走到了哈希表的末尾
                //还没有找到一个可插入的位置
                //则将其置为0,从头开始往后继续查找
                offset = 0;
            }
        }//else结束
    }//while结束
}
//打印哈希表中的元素的函数
void HashPrint(HashTable *ht,const char *msg)
{
    printf("[%s]\n",msg);
    int i = 0;
    for(;i < max_size;i++)
    {
        if(ht->data[i].stat == Valid)
        {
            printf("(%d:%d,%d) ",i,ht->data[i].key,\
            ht->data[i].value);
        }
    }
    printf("\n");
}
//测试一下
void TestInsert()
{
    Test_Header;
    HashTable ht;
    HashInit(&ht,Hash_func);
    HashInsert(&ht,1,1);
    HashInsert(&ht,1,10);
    HashInsert(&ht,2,20);
    HashInsert(&ht,1000,100);
    HashInsert(&ht,2000,200);
    HashPrint(&ht,"插入5个元素");
}

测试结果:
这里写图片描述

//查找数据
int HashFind(HashTable *ht,KeyType key,ValType *value)
{
    if(ht == NULL)
    {
        //非法输入
        return 0;
    }
    //判断当前hash表中是否有有效元素
    if(ht->size == 0)
    {
        //空哈希表
        return 0;
    }
    //由key值计算出offset
    int offset = ht->func(key);
    //从offset开始往后查找
    while(1)
    {
        //在当前位置存放的是有效数据的前提下
        if(ht->data[offset].stat == Valid)
        {
            if(ht->data[offset].key == key)
            {
                //找到了
                *value = ht->data[offset].value;
                return 1;
            }
            //当前位置不是待查找的元素
            //则更新offset的值继续查找
            else
            {
                ++offset;
                if(offset >= max_size)
                {
                    offset = 0;
                }
            }
        }
        else if(ht->data[offset].stat == Empty)
        {
            //说明带查找的元素不存在与hash表中
            //查找失败返回
            return 0;
        }
    }//while循环结束
    return 0;
}
//测试一下
void TestFind()
{
    Test_Header;
    HashTable ht;
    HashInit(&ht,Hash_func);
    HashInsert(&ht,1,1);
    HashInsert(&ht,1,10);
    HashInsert(&ht,2,20);
    HashInsert(&ht,1000,100);
    HashInsert(&ht,2000,200);
    ValType value;
    int ret = HashFind(&ht,1000,&value);
    printf("查找数据为1000的元素结果为:");
    printf("expect ret = 1,actual ret = %d;",ret);
    printf("expect value = 100,actual value = %d\n",value);
    ret = HashFind(&ht,3000,&value);
    printf("查找数据为3000的元素结果为:");
    printf("expect ret = 0,actual ret = %d\n",ret);
}

测试结果:
这里写图片描述

//删除数据
void HashRemove(HashTable *ht,KeyType key)
{
    if(ht == NULL)
    {
        //非法输入
        return;
    }
    if(ht->size == 0)
    {
        //空哈希表
        return;
    }
    //由key值计算出offset
    int offset = ht->func(key);
    //从offset开始往后找
    while(1)
    {
        if(ht->data[offset].stat == Valid \
           && ht->data[offset].key == key)
        {
            //找到了待删除的元素
            //直接将该位置的状态置为被删除状态即可
            ht->data[offset].stat = Deleted;
            //将hash表中有效元素个数-1
            --ht->size;
            return;
        }
        else if(ht->data[offset].stat == Empty)
        {
            //走到这里说明该元素不存在
            return;
        }
        else
        {
            //走到这里说明当前offset位置的值不是我们想要删除的
            //则更新offset值继续查找
            ++offset;
            if(offset >= max_size)
            {
                offset = 0;
            }
        }
    }//while循环结束
    return;
}
//测试一下
void TestRemove()
{
    Test_Header;
    HashTable ht;
    HashInit(&ht,Hash_func);
    HashInsert(&ht,1,1);
    HashInsert(&ht,1,10);
    HashInsert(&ht,2,20);
    HashInsert(&ht,1000,100);
    HashInsert(&ht,2000,200);
    HashRemove(&ht,2);
    HashPrint(&ht,"删除数据为2的元素后的结果:");
    HashRemove(&ht,20);
    HashPrint(&ht,"删除数据为20的元素后的结果:");
}

测试结果:
这里写图片描述
另外一种处理哈希冲突的方法:开散列(哈希桶)处理哈希冲突。请移步下一篇文章:哈希桶处理哈希冲突

  • 19
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
哈希表是一种基于哈希函数实现的数据结构,它可以在常数时间内完成查找、插入和删除等操作。但是在实际应用中,由于哈希函数的不完美性或者数据量的增加,可能会出现哈希冲突的情况,即两个或多个键被映射到了同一个哈希桶中。为了解决哈希冲突,常见的方法之一是线性探测法线性探测法是指当发生哈希冲突时,顺序地查看相邻的哈希桶,直到找到一个空闲的哈希桶为止。具体地,假设哈希函数将键 $k$ 映射到哈希桶 $i$ 中,但是哈希桶 $i$ 已经被占用了,那么我们就从哈希桶 $i+1$ 开始依次查找,直到找到一个空闲的哈希桶 $j$ 为止,把键值对 $(k, v)$ 存储在哈希桶 $j$ 中。如果哈希桶 $i+1$ 到哈希桶 $m$ 都被占用了,我们就从哈希桶 $0$ 开始继续查找,直到找到一个空闲的哈希桶为止。 当使用线性探测法解决哈希冲突时,需要注意以下几点: 1. 哈希表的装载因子 $\alpha=\frac{n}{m}$ 应该尽量小,通常不超过 $0.75$。 2. 在查找、插入和删除键值对时,都需要按照线性探测的方式依次查看相邻的哈希桶,直到找到目标键值对或者遇到空闲的哈希桶为止。 3. 当删除一个键值对时,需要将其占用的哈希桶标记为已删除状态,而不是真正地删除该键值对。这是因为如果真正地删除该键值对,可能会导致后续查找时出现错误的结果。 总体来说,线性探测法是一种简单而有效的解决哈希冲突的方法,但是它的性能受到装载因子的影响,当装载因子较大时,线性探测法可能会退化为链表,导致查找、插入和删除等操作的时间复杂度从常数级别变成线性级别。因此,在使用线性探测法时,需要合理地选择哈希函数和哈希表的大小,以及采用其他解决哈希冲突的方法,如链表法、双散列法等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值