数据结构之哈希表(包含哈希桶)

本文深入探讨哈希表的基本概念、工作原理及其两种主要实现方法:开放定址法和拉链法。通过实例讲解如何解决哈希冲突,并提供详细的C语言实现代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

哈希表

什么是哈希表

哈希表就是一个元素有一一对应位置的一个表,如下图,哈希表也叫散列表,和函数的一个x对应一个y类似,不存在多个y对应一个x,当然哈希表可能有多个数对应一个下标,我们后面讲,这里暂且理解为和函数一样,是一种映射。
在图中,哈希表存的数据位整形,如果我们存手机号,可以将后四位作为key,或者是后四位经过一个算数处理,当作key也可以。
哈希表
哈希表作为一个查找时间复杂度只有O(1)的数据接结构,可以说效率非常高。
对于二叉搜索树而言,二叉搜索树中的元素存储位置和其值没有直接对应关系,需要多次将关键码进行比较来查询。
相比二叉搜索树,哈希表有一对一对应的映射关系,不需要多次比较来查找。
我认为这是典型的空间换时间的数据结构,通过一对一映射的方式,必将有空间为空,说明其空间利用率其实不是100%,像java中,哈希表的存储利用率为0.75,超过0.75这个预设值,哈希表将被扩容,我在这篇文章中所用的开放定址法的代码将这个值控制在0.7,而拉链法的比例控制为1。

哈希冲突

如上图一样,11和22在哈希表容量为11的时候,他们对应的都是0,这就有了冲突,解决冲突有两种办法,开散列和闭散列。

开散列

开散列又叫开放定址法,一个位置只存一个元素,如果新来的元素发现该位置有冲突,就向后寻找,直至找到一个空位为止,存入数据,但是很多时候这种方法很不好,容易造成过多的冲突,于是有了二次线性探测,和直接探测不同,二次探测是依次往后探测n的平方个,第一次往后一个,第二次往后4个,第三次往后9个。
哈希表开放定址法

闭散列(用此种方法实现的哈希表称之为哈希桶)

闭散列又称拉链法,当有数冲突的时候,不是往后找,而是直接挂在这个位置的下面,像链表一样挂起来,这个方式一容量和元素个数的比例一般控制为1,多了会影响查找效率。
哈希表拉链法
但是这种方法也有个坏处,假如有人恶意将其所有元素均存在一个位置,然后进行访问,就会出现访问速度过慢的问题,假如这被应用在网站的话,就会造成网站崩溃,无法访问。

负载因子

负载因子我在上面也有带着提到过,这里详细说一下。
负载因子α = 表中元素个数 / 散列表的长度
由于表长是定值,所以α和填入表中的元素个数成正比,α越大,表明表中数据越多,冲突的可能性越大,反之冲突越小。
实际上,散列表的平均查找长度是载荷因子α 的函数,只是不同处理冲突的方法有不同的函数。
对于开放定址法,负载因子是特别中原的因素,应严格控制在0.7~0.8以下,超过0.8,查表时CPU的缓存不命中(cache missing)按照指数曲线上升,因此一些采用开放定址法的库比如java就是限制负载因子为0.75,超过则resize。

实现代码

开放定址法实现哈希表

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
#include <time.h>

typedef int HTDataType;

typedef enum Status
{
    EMPTY,
    EXIST,
    DELETE
}Status;

typedef struct HashTableNode
{
    HTDataType _data;
    Status _status;
}HTNode;

typedef struct HashTable
{
    HTNode* _table;
    size_t _size;
    size_t _capacity;
}Hash;

void HashInit(Hash* ht, size_t capacityy);//初始化

//功能辅助函数
static size_t GetPrime(Hash* ht);//STL里面的素数表,获取素数
static size_t HashFunc(HashTable* ht, HTDataType data); //哈希函数
static void CheckCapacity(HashTable* ht);//空间检查
static void HashPrint(HashTable* ht);//打印

//基本功能函数,增删查
bool HashInsert(Hash* ht, HTDataType data);
bool HashRemove(Hash* ht, HTDataType data);
HTNode* HashFind(Hash* ht, HTDataType data);
void TestHashTable();//测试用例

void HashInit(Hash* ht, size_t capacity)
{
    assert(ht);
    ht->_size = 0;
    ht->_capacity = capacity;
    ht->_capacity = GetPrime(ht);
    ht->_table = (HTNode*)malloc(sizeof(HTNode) * ht->_capacity);
    assert(ht->_table);
    for (size_t i = 0; i < ht->_capacity; i++)
    {
        (*(ht->_table + i))._status = EMPTY;
    }
}

static size_t GetPrime(Hash* ht)
{
    const int _PrimeSize = 28;//仅可在c++下使用
    //该数据都是经过许多人测试的素数,能够满足需求,冲突较小
    static const unsigned long _PrimeList[_PrimeSize] = {
        53ul,97ul,193ul,389ul,769ul,
        1543ul,3079ul,6151ul,12289ul,
        24593ul,49157ul,98317ul,196613ul,
        393241ul,786433ul,1572869ul,3145739ul,
        6291469ul,12582917ul,25165843ul,50331653ul,
        100663319ul,201326611ul,402653189ul,805306457ul,
        1610612741ul, 3221225473ul, 4294967291ul
    };
    int index = 0;
    while (index < _PrimeSize)
    {
        if (ht->_capacity < _PrimeList[index])
        {
            return _PrimeList[index];
        }
        index++;
    }
    return _PrimeList[_PrimeSize - 1];
}

static size_t HashFunc(HashTable* ht, HTDataType data)
{
    //若需要存储其他类型的数据,需要修改此函数的寻址规则
    return data % ht->_capacity;
}

static void CheckCapacity(HashTable* ht)
{
    assert(ht);
    //若空间不够,扩容
    if (ht->_size * 10 / ht->_capacity >= 7)
    {
        HashTable newht;
        HashInit(&newht, ht->_capacity);
        for (int i = 0; i < ht->_capacity; i++)
        {
            if ((*(ht->_table + i))._status == EXIST)
            {
                HashInsert(&newht, (*(ht->_table + i))._data);//赋用Insert进行重新寻址插入
            }
        }
        free(ht->_table);
        ht->_table = newht._table;
        ht->_size = newht._size;
        ht->_capacity = newht._capacity;
    }
}

bool HashInsert(Hash* ht, HTDataType data)
{
    assert(ht);
    CheckCapacity(ht);

    size_t index = HashFunc(ht, data);
    size_t i = 1;
    while ((*(ht->_table + index))._status != EMPTY)
    {
        if ((*(ht->_table + index))._status == EXIST)
        {
            if ((*(ht->_table + index))._data == data)
            {
                return false;
            }
        }
        //index++;
        //if (index == ht->_capacity)
        //{
        //  index = 0;
        //}
        index = index + i * i;//二次探测寻址
        index = index % ht->_capacity;
        i++;
    }
    (*(ht->_table + index))._data = data;
    (*(ht->_table + index))._status = EXIST;
    ht->_size++;
    return true;
}

bool HashRemove(Hash* ht, HTDataType data)
{
    HTNode* node = HashFind(ht, data);
    if (node)
    {
        node->_status = DELETE;
        return true;
    }
    return false;
}

HTNode* HashFind(Hash* ht, HTDataType data)
{
    assert(ht);
    size_t index = HashFunc(ht, data);
    size_t i = 1;
    while ((*(ht->_table + index))._status != EMPTY)
    {
        if ((*(ht->_table + index))._data == data)
        {
            if ((*(ht->_table + index))._status == EXIST)
            {
                return ht->_table + index;
            }
            else
            {
                return NULL;
            }
        }
        //index++;//线性探测寻址法
        index = index + i * i;//二次探测寻址
        index = index % ht->_capacity;
        i++;
    }
    return NULL;
}

static void HashDestroy(HashTable* ht)
{
    assert(ht);
    free(ht->_table);
    ht->_table = NULL;
    ht->_size = 0;
    ht->_capacity = 0;
}

void HashPrint(HashTable* ht)
{
    //为了测试显示用的打印函数
    for (int i = 0; i < ht->_capacity; i++)
    {
        if (i % 10 == 0 && i != 0)
        {
            printf("\n");
        }
        if ((*(ht->_table + i))._status == EXIST)
        {
            printf("%2d ", (*(ht->_table + i))._data);
        }
        else if((*(ht->_table + i))._status == DELETE)
        {
            printf(" D ");
        }
        else
        {
            printf(" N ");
        }
    }
    printf("\n");
}

void TestHashTable()
{
    Hash ht;
    HashInit(&ht, 0);
    //插入测试
    srand((unsigned)time(0));
    for (int i = 0; i < 100; i++)
    {
        HashInsert(&ht, rand() % 100);
    }
    HashPrint(&ht);
    printf("-------------------------------------------------\n");
    //删除测试
    for (int i = 0; i < 100; i++)
    {
        HashRemove(&ht, rand() % 100);
    }

    HashPrint(&ht);
    HashDestroy(&ht);
}

拉链法实现哈希表

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>
#include <assert.h>

typedef int HashDataType;

typedef struct HashBucketNode
{
    HashDataType _data;
    struct HashBucketNode* _next;
}HashNode;

typedef struct HashBucket
{
    HashNode** _tables;
    size_t _size;
    size_t _capacity;
}HashBucket;

void HashInit(HashBucket* hb, size_t capacity);//初始化哈希桶

static size_t HashFunc(HashBucket* hb, HashDataType data);//哈希函数
static HashNode* BuyNewHashNode(HashDataType data);//创建新链节点
static void CheckCapacity(HashBucket* hb);//检查空间,确保效率
static size_t GetPrime(HashBucket* hb);
static void HashPrint(HashBucket* hb);

bool HashInsert(HashBucket* hb, HashDataType data);//插入
HashNode* HashFind(HashBucket* hb, HashDataType data);//查找某个元素是否存在
bool HashRemove(HashBucket* hb, HashDataType data);//删除
size_t HashSize(HashBucket* hb);

void TestHashTable();//测试用例


void HashInit(HashBucket* hb, size_t capacity)
{
    assert(hb);
    hb->_capacity = capacity;
    hb->_capacity = GetPrime(hb);
    hb->_tables = (HashNode**)malloc(sizeof(HashNode*) * hb->_capacity);
    assert(hb->_tables);
    hb->_size = 0;
    for (size_t i = 0; i < hb->_capacity; i++)
    {
        *(hb->_tables + i) = NULL;
    }
}

static size_t HashFunc(HashBucket* hb, HashDataType data)
{
    assert(hb);
    return data % hb->_capacity;
}

static HashNode* BuyNewHashNode(HashDataType data)
{
    HashNode* new_node = (HashNode*)malloc(sizeof(HashNode));
    new_node->_data = data;
    new_node->_next = NULL;
    return new_node;
}

static void CheckCapacity(HashBucket* hb)
{
    assert(hb);
    if (hb->_size == hb->_capacity)
    {
        HashBucket new_hb;
        HashNode* cur = NULL;
        HashNode* next = NULL;
        HashInit(&new_hb, hb->_capacity);
        for (size_t i = 0; i < hb->_capacity; i++)
        {
            //将原数据插入新表中
            cur = *(hb->_tables + i);
            while (cur)
            {
                //之所以可以头插是因为数据没有重复
                next = cur->_next;
                size_t index = HashFunc(&new_hb, cur->_data);
                cur->_next = *(new_hb._tables + index);
                *(new_hb._tables + index) = cur;
                cur = next;
            }
        }
        hb->_capacity = new_hb._capacity;
        free(hb->_tables);//释放旧表
        hb->_tables = new_hb._tables;//链接新表
    }
}

static size_t GetPrime(HashBucket* hb)
{
    assert(hb);
    const int _PrimeSize = 28;
    //该数据都是经过许多人测试的素数,能够满足需求,冲突较小
    static const unsigned long _PrimeList[_PrimeSize] = {
        53ul,97ul,193ul,389ul,769ul,
        1543ul,3079ul,6151ul,12289ul,
        24593ul,49157ul,98317ul,196613ul,
        393241ul,786433ul,1572869ul,3145739ul,
        6291469ul,12582917ul,25165843ul,50331653ul,
        100663319ul,201326611ul,402653189ul,805306457ul,
        1610612741ul, 3221225473ul, 4294967291ul
    };
    int index = 0;
    while (index < _PrimeSize)
    {
        if (hb->_capacity < _PrimeList[index])
        {
            return _PrimeList[index];
        }
        index++;
    }
    return _PrimeList[_PrimeSize - 1];
}

static void HashPrint(HashBucket* hb)
{
    //辅助打印函数
    assert(hb);
    HashNode* cur = NULL;
    for (size_t i = 0; i < hb->_capacity; i++)
    {
        cur = *(hb->_tables + i);
        printf("tables[%d]", i);
        while (cur)
        {
            printf("->%d", cur->_data);
            cur = cur->_next;
        }
        printf("->NULL\n");
    }
}

bool HashInsert(HashBucket* hb, HashDataType data)
{
    assert(hb);
    CheckCapacity(hb);
    size_t index = HashFunc(hb, data);
    HashNode* cur = *(hb->_tables + index);
    HashNode* prev = cur;
    if (cur == NULL)
    {
        //该位置没有任何节点,直接插入
        *(hb->_tables + index) = BuyNewHashNode(data);
    }
    else
    {
        //该位置已有节点,找寻合适位置插入
        while (cur)
        {
            //不允许头插,因为可能有数据重复
            if (cur->_data == data)
            {
                return false;
            }
            prev = cur;
            cur = cur->_next;
        }
        prev->_next = BuyNewHashNode(data);
    }
    hb->_size++;
    return true;
}

HashNode* HashFind(HashBucket* hb, HashDataType data)
{
    assert(hb);
    size_t index = HashFunc(hb, data);
    HashNode* cur = *(hb->_tables + index);
    while (cur)
    {
        if (cur->_data == data)
        {
            return cur;
        }
        cur = cur->_next;
    }
    return NULL;
}

bool HashRemove(HashBucket* hb, HashDataType data)
{
    assert(hb);
    size_t index = HashFunc(hb, data);
    HashNode* cur = *(hb->_tables + index);
    HashNode* prev = cur;
    while (cur)
    {
        if (cur->_data == data)
        {
            break;
        }
        prev = cur;
        cur = cur->_next;
    }
    if (cur == NULL)
    {
        //cur已经查到底或者该位置本就没有节点为空,未找到
        return false;
    }
    else
    {
        //找到
        if (cur == prev)
        {
            //节点为第一个结点,不能直接prev = cur,因为prev是临时变量
            *(hb->_tables + index) = cur->_next;
        }
        else
        {
            //节点不是第一个节点
            prev->_next = cur->_next;
        }
        free(cur);
        return true;
    }
}

size_t HashSize(HashBucket* hb)
{
    assert(hb);
    return hb->_size;
}

void TestHashTable()
{
    HashBucket hb;
    HashInit(&hb, 0);

    srand((unsigned)time(0));
    for (int i = 0; i < 50; i++)
    {
        HashInsert(&hb, rand() % 200);
    }
    HashPrint(&hb);
    printf("\n");

    for (int i = 0; i < 50; i++)
    {
        HashInsert(&hb, rand() % 200);
    }
    HashPrint(&hb);
    printf("\n");

    for (int i = 0; i < 200; i++)
    {
        HashRemove(&hb, rand() % 200);
    }
    HashPrint(&hb);

}
### 哈希表扩容原理 当哈希表中的元素数量接近其容量时,为了保持高效的查找性能,通常会触发扩容机制。扩容意味着创建一个新的更大的哈希表,并将原有哈希表中的所有键值对重新分配到新表中[^2]。 #### 扩容原因 随着哈希表内存储的元素增多,发生碰撞的概率也会增加,这会影响哈希表的操作效率。因此,在负载因子(即已存入元素数目除以哈希表长度)达到一定阈值时,就需要进行扩容来降低平均查找成本并减少冲突频率[^1]。 #### 扩容过程概述 扩容主要包括以下几个方面的工作: - **新建更大尺寸的新数组**:一般情况下会选择原大小的一个质数倍作为新的容量,比如原来的两倍加一。 - **迁移已有数据**:遍历当前哈希表内的每一个桶(bucket),对于非空节点执行再散列(rehashing)操作,将其放置于合适的新位置上;如果采用的是开放地址法,则需逐个处理每个槽位上的项直至完成整个转移流程;而如果是分离链接法则只需简单地调整指针指向即可[^3]。 - **更新内部状态变量**:修改记录实际占用空间以及最大允许填充量的相关参数,确保后续插入等动作能够依据最新的配置正常运作。 以下是简单的Python代码示例展示了如何实现基本的哈希表及其扩容逻辑: ```python class MyHashMap: def __init__(self, capacity=8): # 初始化默认容量为8 self.capacity = capacity # 当前容量 self.size = 0 # 已使用的槽数目 self.threshold = int(self.capacity * 0.75) # 负载因子设为0.75 self.buckets = [[] for _ in range(capacity)] # 使用列表模拟链表形式的bucket def put(self, key, value): index = hash(key) % self.capacity bucket = self.buckets[index] found_key = False for i, (k, v) in enumerate(bucket): if k == key: bucket[i] = (key, value) found_key = True if not found_key: bucket.append((key, value)) self.size += 1 if self.size >= self.threshold: # 达到预设比例则启动扩容 self.resize() def resize(self): old_buckets = self.buckets[:] new_capacity = self.capacity * 2 + 1 # 新容量通常是现有容量的约两倍多一点 self.__init__(new_capacity) for bucket in old_buckets: for pair in bucket: self.put(pair[0], pair[1]) # 将旧表里的每一对kv重新put进新表里去 ``` 此段程序实现了简易版支持动态增长特性的哈希映射结构,其中`resize()`函数负责具体实施上述提到的一系列扩容步骤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值