散列表(哈希表)

哈希表的定义

“散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

用来:可以根据一个key值来直接访问数据,因此查找速度快

在最基本的几个数据结构中,数组肯定是查询效率是最高的。因为它可以直接通过数组下标来访问数据

其实哈希表的本质上就是一个数组,它之所以叫哈希表,只能说它的底层实现是用到了数组,稍微加工,自立门户成了哈希表

散列技术既是一种存储方法, 也是一种查找方法,散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到置上。

这里我们把这种对应关系f称为散列函数,又称为哈希(Hash) 函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表哈希表(Hash table)。那么关键字对应的记录存储位置我们称为散列地址

存储位置=f(关键字)

散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为冲突,这些发生碰撞的不同关键字称为同义词。一方面,设计得好的散列函数应尽量减少这样的冲突;另一方面,由于这样的冲突总是不可避免的,所以还要设计好处理冲突的方法。

理想情况下,对散列表进行查找的时间复杂度为O(1),即与表中元素的个数无关。

散列函数的构造方法

在构造散列函数时,必须注意以下几点:

1.散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于散列表的大小或地址范围。

2.散列函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。

3.散列函数应尽量简单,能够在较短的时间内计算出任一关键字对应的散列地址。

1、直接定址法

直接取关键字的某个线性函数值为散列地址,散列函数为

H(key)=key或H(key)=a∗key+b

式中,a和b是常数。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。

举例:0~100岁的人口数字统计表,可以吧年龄数值直接当做散列地址。

2、数字分析法

例如当手机号码为关键字时,其11位数字是有规则的,此时是无需把11位数值全部当做散列地址,这时我们给关键词抽取, 抽取方法是使用关键字的一部分来计算散列存储位置的方法,这在散列函数中是常常用到的手段。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。

3、平方取中法

这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。再比如关键字是4321,那么它的平方就是18671041,抽取中间的3位就可以是671,也可以是710,用做散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。

4、除留余数法

这是一种最简单、最常用的方法,假定散列表表长为m,取一个不大于m但最接近或等于m的质数p,利用以下公式把关键字转换成散列地址。散列函数为

H(key)=key%p (p<=m)

事实上,这方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。

除留余数法的关键是选好p,使得每个关键字通过该函数转换后等概率地映射到散列空间上的任一地址,从而尽可能减少冲突的可能性。

5、随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址。也就是

H(key)=random(key)

这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

处理散列冲突

1.开放地址法

所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

它的公式是:

Hi(key)=(f(key)+di)%m (di=1,2,3,...,m−1)

式中,H(key)为散列函数;i=0,1,2,...,k (k<=m−1);m表示散列列表表长;di 为增量序列。

取定某一增量序列后,对应的处理方法就是确定的。通常有以下4种取法:

1.线性探测法。当di=0,1,2,...,m−1时,称为线性探测法。这种方法的特点是:冲突发生时,顺序查看表中下一个单元(探测到表尾地址m−1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。

线性探测法可能使第i个散列地址的同义词存入第i+1个散列地址,这样本应存入第i+1个散列地址的元素就争夺第i+2个散列地址的元素的地址,从而造成大量元素在相邻的散列地址上堆积,大大降低了查找效率。

2.平方探测法。当di=0^2,1^2,−1^2,2^2,−2^2,..,k^2,−k^2 时,称为平方探测法,其中k<m/2,散列表长度m必须是一个可以表示成4k+3的素数,又称二次探测法。

平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。

3.再散列法。当di=Hash2(key)时,称为再散列法,又称双散列法。需要使用两个散列函数,当通过第一个散列函数H(key)得到的地址发生冲突时,则利用第二个散列函数Hash2(key)计算该关键字的地址增量。它的具体散列函数形式如下:

Hi=(H(key)+i∗Hash2(key))%m初始探测位置H0=H(key)。i是冲突的次数,初始为0。在再散列法中,最多经过m−1次探测就会遍历表中所有位置,回到H0位置。

4.伪随机序列法。当di=伪随机数序列时,称为伪随机序列法。

注意:在开放定址的情形下,不能随便物理删除表中的已有元素,因为若删除元素,则会截断其他具有相同散列地址的元素的查找地址。因此,要删除一个元素时,可给它做一个删除标记,进行逻辑删除。但这样做的副作用是:执行多次删除后,表面上看起来散列表很满,实际上有许多位置未利用,因此需要定期维护散列表,要把删除标记的元素物理删除。

2、链地址法(拉链法)

不换地方。

转换一下思路,为什么非得有冲突就要换地方呢,如果不换地方该怎么处理?于是我们就有了链地址法。

将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。

例如,关键字序列为{12,67,56,16,25,37,22,29,15,47,48,34},我们用除留余数法构造散列函数H(key)=key%12,用拉链法处理冲突,建立的表如下图所示。

3、公共溢出区法

就是把凡是冲突的家伙额外找个公共场所待着。我们为所有冲突的关键字建立了一个公共的溢出区来存放。

就前面的例子而言,我们共有三个关键字37 , 48 , 34 {37,48,34}37,48,34与之前的关键字位置有冲突,那么就将它们存储到溢出表中,如下图所示。

如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

性能分析

从散列表的查找过程可见:

虽然散列表在关键字与记录的存储位置之间建立了直接映像,但由于“冲突”的产生,使得散列表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需要以平均查找长度作为衡量散列表的查找效率的度量。

散列表的查找效率取决于三个因素:散列函数、处理冲突的方法和装填因子。

若用ci表示每一个关键字查找的次数,则平均查找次数可表示为:

ASL=(i=0∑mci)/m

散列表查找实现

1、算法

首先是需要定义一个散列表的结构以及一些相关的常数。其中HashTable就是散列表结构。结构当中的elem为一个动态数组。


#define SUCCESS 1;
#define UNSUCCESS 0;
#define HASHSIZE 12;    //定义散列表表长为数组的长度
#define NULLKEY -32768;    //代表空地址
typedef struct{
    int *elem;    //数组元素存储基址,动态分配数组
    int count;    //当前数据元素个数
}HashTable;
int m=0;    //散列表表长,全局变量

初始化。


/*初始化散列表*/
bool InitHashTable(HashTable *H){
    int i;
    m=HASHSIZE;
    H->count=m;
    H->elem=(int *)malloc(m*sizeof(int));
    for(i=0; i<m; i++){
        H->elem[i]=NULLKEY;
    }
    return TRUE;
}

为了插入时计算地址,我们需要定义散列函数,散列函数可以根据不同情况更改算法。


/*散列函数*/
int Hash(int key){
    return key % m;    //除留余数法
}

初始化完成后,我们可以对散列表进行插入操作。


/*插入关键字进散列表*/
void InsertHash(HashTable *H, int key){
    int addr = Hash(key);    //通过散列函数求散列地址
    //如果不为空。则冲突
    while (H->elem[addr] != NULLKEY){
        addr = (addr + 1) % m;    //开放定址法的线性探测
    }
    H->elem[addr] = key;    //直到有空位后插入关键字
}

代码中插入关键字时,首先算出散列地址,如果当前地址不为空关键字,则说明有冲突。此时我们应用开放定址法的线性探测进行重新寻址,此处也可更改为链地址法等其他解决冲突的办法。

散列表存在后,我们在需要时就可以通过散列表查找要的记录。


/*
散列表查找关键字
找到后用addr保存地址
*/
bool SerachHash(HashTable H, int key, int *addr){
    *addr = Hash(key);    //通过散列函数求得散列地址
    //如果不为空,则有同义词冲突
    while(H.elem[*addr] != key){
        *addr = (*addr+1) % m;    //开放地址法的线性探测
        if(H.elem[*addr] == NULLKEY || *addr == Hash(key)){
            //如果循环到空址或回到原点
            return FALSE;    //则说明关键字不存在
        }
    }
    return TRUE;
}

查找的代码与插入的代码非常类似,只需做一个不存在关键字的判断而已。

完整代码


#include <iostream>
#include <cstring>
using namespace std;

#define DEFAULT_SIZE 32

//哈希表元素定义
typedef struct _listNode {
    struct _listNode* next;//指像下一个位置的指针
    int key;//索引编号
    void* data;//保存的数据
}listNode;

typedef listNode* list;
typedef listNode* elem;

//哈希表结构定义
typedef struct _HashTable {
    int TableSize;//哈希桶的总个数
    list* Thelists;//保存在链表的位置
}HashTable;

//哈希函数: 根据key 计算索引,定位Hash桶的位置
int Hash(int key, int TableSize) {
    return (key % TableSize);
}

//初始化哈希表
HashTable* initHash(int TableSize) {
    if (TableSize <= 0) {
        TableSize = DEFAULT_SIZE;
    }

    HashTable* htable = NULL;
    htable = new HashTable;
    if (htable == NULL) {
        cout << "htable new error!" << endl;
        return NULL;
    }
    htable->TableSize = TableSize;

    //为hash桶分配内存空间,其为一个指针数组
    htable->Thelists = new list[htable->TableSize];//这里需要分配一个指针数组
    if (htable->Thelists == NULL) {
        cout << "list new error!" << endl;
        delete htable;
        return NULL;
    }

    //为hash桶对应的指针数组初始化链表节点
    for (int i = 0; i < TableSize; i++) {
        htable->Thelists[i] = (list)(new listNode);//需要把分配的节点进行类型转换
        if ((list)htable->Thelists[i]==NULL) {
            cout << "listNode new error!" << endl;
            delete htable->Thelists;
            delete htable;
            return NULL;
        }
        else {
            //初始化hash桶里的所有元素为0
            memset(htable->Thelists[i], 0, sizeof(listNode));
        }
    }

    return htable;
}

//从哈希表中根据键值查找元素
elem Find(HashTable* table, int key) {
    list L = NULL;
    elem e = NULL;
    int i = 0;
    i = Hash(key, table->TableSize);
    L = table->Thelists[i];
    e = L->next;

    while (e != NULL && e->key != key) {
        e = e->next;
    }

    return e;
}

//哈希表链表插入元素,元素为键值对
void insertHash(HashTable* table, int key, void* value) {
    elem e = NULL;
    elem tmp = NULL;
    list L = NULL;

    e = Find(table, key);

    if (e == NULL) {
        tmp = new listNode;
        if (tmp == NULL) {
            cout << "tmp new error!" << endl;
            return;
        }

        //使用前插法
        L = table->Thelists[Hash(key,table->TableSize)];
        tmp->data = value;
        tmp->key = key;
        tmp->next = L->next;
        L->next = tmp;
    }
    else {
        cout << "key already exist!" << endl;
    }
}

//哈希表链表删除元素
void deleteHash(HashTable* table, int key) {
    elem e = NULL;
    elem last = NULL;
    list L = NULL;

    int i = Hash(key, table->TableSize);
    L = table->Thelists[i];

    last = L;
    e = L->next;
    while (e != NULL && e->key != key) {
        e = e->next;
        last = e;
    }

    if (e != NULL) {
        //如果键值对存在
        last->next = e->next;
        delete e;
    }
}

//提取哈希表中的数据
void *retrieveHash(elem e) {
    return e ? e->data : NULL;
}

//销毁哈希表
void destoryHash(HashTable* hash) {
    list L = NULL;
    elem cur = NULL;
    elem next = NULL;

    for (int i = 0; i < hash->TableSize; i++) {
        L = hash->Thelists[i];
        cur = L->next;

        while (cur != NULL) {
            next = cur->next;
            delete cur;
            cur = next;
        }

        delete L;
    }

    delete hash->Thelists;
    delete hash;
}

//测试代码
int main(void) {

    const char* elems[] = { "小王","老王","老宋"};

    HashTable* hash;
    hash = initHash(16);
    
    insertHash(hash, 1, (char*)elems[0]);
    insertHash(hash, 2, (char*)elems[1]);
    insertHash(hash, 3, (char*)elems[2]);

    deleteHash(hash, 1);

    for (int i = 1; i < 4; i++) {
        elem e = Find(hash, i);

        if (e != NULL) {
            cout << "NO."<<i<<" is:"<<(const char*)retrieveHash(e) << endl;
        }
        else {
            cout << "not seek out:"<<elems[i-1] << endl;
        }
    }

    system("pause");
    return 0;
}

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值