15--散列查找

287 阅读8分钟

关于数据结构与算法的相关知识,请查看文章《数据结构与算法基础知识文章汇总》

一、散列技术

散列技术是记录的位置和它的关键字key之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据关键字key与存储位置之间的对应关系f(key),求得关键字key在内存中的存储位置,如果找到的位置的数据刚好就是关键字key,则查找成功。

假如有如下数据:

10、22、34、43、9、81、47、56

现在需要我们设计一个公式,求得上面的每个在数组中的存储位置,而且存储的位置在数组中是相对均匀的。并且这个公式不能太复杂,要相对简单

  • 1.所求的公式即为哈希公式
  • 2.存储时,把数据代入哈希公式,即求得存储的位置,即哈希地址
  • 3.查找时,把数据代入哈希公式,求得存储的位置。获取存储位置的数据与要查找的数据进行比较,如果相等,则查找成功
  • 4.这样一种通过公式求解存储和查找位置的技术即为散列技术,散列也称为哈希

二、哈希公式的设计

哈希公式的设计要遵循简单的原则,并且数据存储的位置要相对均匀,下面介绍几种哈希公式设计的方法。

1.直接定址法

image.png

  • 1.假如要存储的数据如上图所示,左边表示存储的是数据是年龄和人数之间的关系;
  • 2.右边表示存储的数据是出生年份与人数之间的关系;
  • 3.分析数据发现,年龄是依次递增的,出生年份也是依次递增的,它们和存储位置之间存在着某种线性关系;
  • 4.于是,就可以以年龄和出生年份为key,设计一个哈希算法;
  • 5.比如,存储位置与年龄之间的哈希算法为:地址 = 年龄;则存储位置与出生年份之间的哈希算法为:地址 = 出生年份 - 1980。
  • 6.在存储数据时,就可以用哈希公式计算出存储位置,查找时也能使用哈希公式快速查找到数据。

直接定址法的要求存储位置关键字key之间存在线性关系

image.png

2.数字分析法

image.png

  • 1.假如存储的数据是手机号码,前面7位相同的可能性比较大,后面四位相同的可能性比较小,所以可以根据手机号码后四位来设计一个哈希公式;
  • 2.再比如人的出生年月日,同一年出生的人很多,同一天月日出生的人较少,所以可以根据月日来设计哈希式;
  • 3.再比如人的身份证,身份证后4位相同的人很少,也可以根据身份证后4位来设计哈希公式;
  • 4.数据分析法就是找出数据的规律,尽可能找到差异较大的数据来设计哈希公式

3.平方取中法

假如要为下面的数据设计哈希公式:

1234、1322,4322,3421,6693,9993,7852,8721

我们可以对上面的数据求平方,再取平方后的数据的中间部分来构造哈希公式。

image.png

比如,1234的平方为1522756,227或者27作为要存储的位置。

4.折叠法

折叠法就是将关键字从左到右分割成位数相等的几部分,再对分割后的数据进行操作,最终求得哈希地址。 image.png

5.除留余数法

image.png

  • 1.假设要存储的数据如上,可以分别对上在面的关键字进行数组长度的取模处理,从而得到数据要存储的位置,即哈希地址
  • 2.上面的数组长度为12,所以关键字模以12,则关键字12的哈希地址为12%12 = 0,25%12 = 1,38%12 = 2。所以12、25、38的哈希地址为0、1、2.

公式:

image.png

6.随机数法

选择一随机函数,取关键字的随机值作为散列地址,即f(key)=random(key)其中random为随机函数,通常用于关键字长度不等的场合。

哈希公式的设计要具体分析数据后进行设计,可以选择上面介绍的直接定址法数字分析法平方取中法折叠法除留余数法随机数法来进行设计。哈希公式的设计要具体问题,具体分析

三、解决哈希冲突的常见方法

假设要存储的数据如下图,图中的数据使用除留余数据法得到的哈希地址,现在要将24这个数据存进入,但24的通过除留余数法得到的哈希地址为:24%12 = 0,但0的位置已经存了12了,不能再存24了,这样的情况称为哈希冲突

image.png

即发生了哈希冲突,就要解决哈希冲空。即如果求得的存储位置已经存了其他数据了,就要再次查找没有存储数据的位置。下面介绍几种解决哈希冲突的几种方法。

1.开放定址法

image.png

如果使用除留余数法后求得的哈希地址发生了哈希冲突,则将关键字key + 某个常数,然后再执行哈希公式,直到找到没有发生哈希冲突的地址为止;

2.再散列函数法

image.png

可以多设计几个哈式公式(散列函数),如果发生哈希冲突,再使用其他的哈希公式求哈希地址。

3.链地址法

将所有的关键字为同义词的记录存储在一个单向链表中,称之为同义词字表。如下图:

image.png

  • 1.12和48进行除留余数后的哈希地址都是0,所以12和48就是单向链表的形式存储在下标为0的数组的位置所对应的链表中
  • 2.查找的时候,求得哈希地址后,要再查找单向链表中是否有对应数据,如果有,则查找成功。

4.公共溢出法

image.png

如果除留余数法求得的哈希地址发生了哈希冲突,则再申请一个数组来保存数。这种方法会造成大量的空间浪费,不建议使用。

四、散列查找的示例代码

对以下数据设计哈希公式:

{12,67,56,16,25,37,22,29,15,47,48,34}

1.状态值定义

typedef int Status;

#define OK 1

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define MAXSIZE 100 //存储空间初始分配量

#define SUCCESS 1

#define UNSUCCESS 0

//定义散列表长为数组的长度

#define HASHSIZE 12

#define NULLKEY -32768

2.数据结构设计

typedef struct
{
    //数据元素存储基址,动态分配数组
    int *elem;

    //当前数据元素个数
    int count;
}HashTable;

int m=0; /* 散列表表长,全局变量 */

3.初始化散列表

Status InitHashTable(HashTable *H)
{
    int i;

    //① 设置H.count初始值; 并且开辟m个空间
    m=HASHSIZE;
    H->count=m;
    H->elem=(int *)malloc(m*sizeof(int));

    //② 为H.elem[i] 动态数组中的数据置空(-32768)
    for(i=0;i<m;i++)
        H->elem[i]=NULLKEY;
    return OK;
}

4.哈希公式设计

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

5.插入数据


//3. 插入关键字进散列表
void InsertHash(HashTable *H,int key)

{
    //① 求散列地址
    int addr = Hash(key);
    
    //② 如果不为空,则冲突
    while (H->elem[addr] != NULLKEY)
    {
        //开放定址法的线性探测
        addr = (addr+1) % m;
    }

    //③ 直到有空位后插入关键字
    H->elem[addr] = key;
}

6.查找数据

Status SearchHash(HashTable H,int key,int *addr)
{
    //① 求散列地址
    *addr = Hash(key);

    //② 如果不为空,则冲突
    while(H.elem[*addr] != key)
    {
        //③ 开放定址法的线性探测
        *addr = (*addr+1) % m;

        //④H.elem[*addr] 等于初始值或者循环有回到了原点.则表示关键字不存在;
        if(H.elem[*addr] == NULLKEY || *addr == Hash(key))
            //则说明关键字不存在
            return UNSUCCESS;
    }
    return SUCCESS;
}

7.调试代码

    int arr[HASHSIZE]={12,67,56,16,25,37,22,29,15,47,48,34};

    int i,p,key,result;

    HashTable H;

    //1.初始化散列表
    InitHashTable(&H);

    //2.向散列表中插入数据
    for(i=0;i<m;i++)
        InsertHash(&H,arr[i]);

    //3.在散列表查找key=39
    key=39;
    result=SearchHash(H,key,&p);

    if(result)
        printf("查找 %d 的地址为:%d \n",key,p);
    else
        printf("查找 %d 失败。\n",key);

    //4.将数组中的key,打印出所有在散列表的存储地址
    for(i=0;i<m;i++)
    {
        key=arr[i];
        SearchHash(H,key,&p);
        printf("查找 %d 的地址为:%d \n",key,p);
    }

五、总结

散列查找就是设计一个哈希公式(散列函数),使得存储数据查找数据时能快速定位到数据的一种查找方法