浅析散列函数的构造方法与解决散列冲突的方法

简介: 浅析散列函数的构造方法与解决散列冲突的方法散列函数的构造方法数字分析法平方取中法平方取中法测试测例1测例2折叠法折叠法测试测例1测例2随机数法随机数法测试测例1除留余数法解决散列冲突的方法开放定址法公共溢出区法以开放定地法为基础的全部代码全部代码测试效果以公共溢出区法为基础的全部代码全部代码测试效果尾言


散列函数的构造方法



数字分析法

数字分析法常用于手机号码、身份证号码、订单编号等位数较多的关键字,对于身份证号码,前六位为地址码,中间八位为出生日期码,后三位为顺序码,最后一位为校验码;世间不缺乏生活地方、出生年月日相同的人,那么可能会增大关键字冲突的几率,我们选取后四位作为关键字的地址,若还是会出现冲突,可以采用将后四位的前两位与后两位相加、颠倒后四位关键字的顺序等方法降低关键字冲突。

数字分析法实现代码如下:

/**
 数字分析法:以手机号码,身份证为例,取后四位*/
int NumAnalysis(long key)
{
  return key % 10000;
}

数字分析法加强版如下:

/**
 若数字分析法产生冲突,则将后四位的前两位与后两位相加做为关键字地址*/
int SumNumbers(int key)
{
  //此处传入的key为产生冲突的关键字的后四位
  int FirstTwo = key / 100;
  int LastTwo = key % 100;
  return FirstTwo + LastTwo;
}


平方取中法

平方取中法:顾名思义,对关键字进行平方处理,然后取中间n位数,抽取的位数根据实际而定。

平方取中法实现代码如下:

此方法所取得中间位由key的长度决定,如果key长度为4,则取平方的中间三位;如果key的长度为3,则取平方的中间的二位;以此类推。

int SquareMedian(int key)
{
  int square = key * key;
  int length = 0;
  //获取关键字的长度
  length = KeyLength(key);
  //Times(n);方法为求10的倍数,比如Times(2);则返回100
  square = square / Times(length - 2);
  square = square % Times(length - 1);
  return square;
}


平方取中法测试


测例1

以key=1234为例,其平方为1522756,key的长度为四,则取中间三位做为关键字存储地址,即227。

int testNum = 1234;
  printf("测试数字为:%d\n",testNum);
  printf("测试数字的平方为:%d\n", testNum * testNum);
  printf("中间位为:%d", SquareMedian(testNum));

image.png


测例2

以key=4321为例,其平方为18671041,key的长度为四,则取中间三位做为关键字存储地址,即671与710都可以。

int testNum = 4321;
  printf("测试数字为:%d\n",testNum);
  printf("测试数字的平方为:%d\n", testNum * testNum);
  printf("中间位为:%d", SquareMedian(testNum));

image.png


折叠法

折叠法是将关键字从左到右按n位数为一组,切割成m组,若最后一组不足n位,也为一组,最后将切割的m组数据相加之和做为关键字的地址。

折叠法实现代码如下:

/**
 折叠法,按照散列表表长(n)将关键字分为m组*/
long FoldMethod(long key)
{
  //设散列表表长为3位,则将9876543210分为4组
  long num = key;
  int tableLength = 3;
  long sum = 0;
  //此处以key为9876543210为例
  //sum = 987 + 456 + 321 + 0 = 1962
  //result = 962
  //获取关键字长度,即10位
  int length = KeyLength(num);
  //10.0 / 3 = 3.3 > 3,即还有最后一位未分组,但又不足三位,也纳入组数
  /*if ((float)length / tableLength > tableLength)
  {
    group = tableLength + 1;
  }*/
  while (length > 0)
  {
    //sum = key % 1000;
    //9876543210 / (10*(10-3))即取前三位
    //即对最后一位0,进行取值时
    if (length - tableLength < 0)
    {
      sum += key % 10;
      length = length - tableLength;
      printf("sum=%d\n", sum);
    }
    else
    {
      sum += num / Times(length - tableLength);
      //printf("次数测试=%ld\n", Times(7));
      num = num % Times(length - tableLength);
      length = length - tableLength;
      printf("sum=%d\n", sum);
    }
  }
  //只取结果的后三位,故判定分组之和是否为三位数
  if (sum / 1000 > 0)
  {
    //大于0,则判定结果不为三位数,则对其进行取模操作,取其后三位
    sum %= 1000;
  }
  return sum;
}


折叠法测试


测例1

以key=987654321为例,将其按3位一组分成三部分:987+654+321=1962;但我们的目的是取三位做为关键字的地址,故对1962进行模运算,得到后三位:962。

 long testNum = 987654321;
  printf("测试数字为=%ld\n", testNum);
  printf("关键字长度为:%d\n", KeyLength(testNum));
  long result = FoldMethod(testNum);
  printf("折叠法后三位散列地址为:%ld", result);

image.png


测例2

以key=123456为例,将其按3位一组分成三部分:123+456=579;对所获得的结果进行位数判定,结果为符合要求,长度为3,则关键字地址为:579

 long testNum = 123456;
  printf("测试数字为=%ld\n", testNum);
  printf("关键字长度为:%d\n", KeyLength(testNum));
  long result = FoldMethod(testNum);
  printf("折叠法后三位散列地址为:%ld", result);

image.png


随机数

顾名思义,取随机数做为关键字的散列地址,公式为:

f(key)=random(key)

随机数法实现代码如下:

/**
 随机数法:f(key) = Random(key)*/
int RandomAddr(int key)
{
  srand(key);
  int addr = rand();
  return addr;
}


随机数法测试


测例1

以数组 { 12,67,56,16,25,37,22,29,15,47,48,34 }为例子

   int HashArray[12] = { 12,67,56,16,25,37,22,29,15,47,48,34 };
  for (int i = 0; i < 12; i++)
  {
      int result = RandomAddr(HashArray[i]);
      printf("result = %d\n", result);
  }

image.png


除留余数法

此方法最为常用,其关键为寻找合适的除数P。对于散列表长为m的散列函数公式为:

f(key)=key mod p (p <= m)

除留余数法实现代码如下:

/**
 散列函数:除留余数法*/
int ModHash(int key)
{
  return key % size;
}


解决散列冲突的方法



开放定址法

开放定址法定义为:如果不存在冲突,则直接使用除留余数法获取散列地址并保存到相应的散列表下标中;反之,则寻找空的地址,直到记录存入为止。

开放定址法插入关键字代码如下:

/**
 往哈希表内插入关键字*/
void InsertHash(HashTable *H,int key)
{
  int addr = ModHash(key);
  //如果不为NULLKEY,则说明存在冲突项,采用开放定址法的线性探索,不断自增地址取余,直到得到空余的下标,然后进行插入
  while (H->elem[addr] != NULLKEY)
  {
    addr = (addr + 1) % size;
  }
  H->elem[addr] = key;
}

公共溢出区法

将冲突的关键字保存到溢出散列中表,查找关键字时,首先查找基本表,若基本表不存在,则前往溢出表进行查找。

公共溢出区法插入关键字代码如下:

/**
 往哈希表内插入关键字*/
void InsertHash(HashTable* baseTable, HashTable* overFlow,int key)
{
  int addr = ModHash(key);
  //如果不为NULLKEY,则说明存在冲突项,采用公共溢出区法,将冲突项保存到溢出表中
  if (baseTable->elem[addr] != NULLKEY)
  {
    overFlow->elem[overIndex++] = key;
  }
  else
  {
    baseTable->elem[addr] = key;
  }
}


以开放定地法为基础的全部代码



全部代码

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
//宏定义
#define Status int
#define SUCCESS 1
#define FAIL 0
#define TABLESIZE 12
#define NULLKEY -32768
int size = 0;//散列表表长
//结构体
typedef struct
{
  int count; //数据个数
  int *elem;  //数据存储的数组
}HashTable;
/**
 初始化哈希表*/
Status InitHash(HashTable *H)
{
  size = TABLESIZE;
  H->count = size;
  H->elem = (int *)malloc(size * sizeof(int));
  //如果内存申请失败,则初始化失败
  if (!H->elem)
  {
    return FAIL;
  }
  //将哈希表所有地址初始化为NULLKEY
  for (int i = 0; i < size; i++)
  {
    H->elem[i] = NULLKEY;
  }
  return SUCCESS;
 }
/**
 散列函数:除留余数法*/
int ModHash(int key)
{
  return key % size;
}
/**
 散列函数:平方取中法
 截取长度为关键字长度减1*/
int SquareMedian(int key)
{
  int square = key * key;
  int length = 0;
  //获取关键字的长度
  length = KeyLength(key);
  square = square / Times(length - 2);
  square = square % Times(length - 1);
  return square;
}
/**
 数字分析法:以手机号码,身份证为例,取后四位*/
int NumAnalysis(long key)
{
  return key % 10000;
}
/**
 若数字分析法产生冲突,则将后四位的前两位与后两位相加做为关键字地址*/
int SumNumbers(int key)
{
  //此处传入的key为产生冲突的关键字的后四位
  int FirstTwo = key / 100;
  int LastTwo = key % 100;
  return FirstTwo + LastTwo;
}
/**
 10的倍数*/
long Times(int num)
{
  long numSum = 1;
  while (num-- > 0)
  {
    numSum *= 10;
  }
  return numSum;
}
/**
 折叠法,按照散列表表长(n)将关键字分为m组*/
long FoldMethod(long key)
{
  //设散列表表长为3位,则将9876543210分为4组
  long num = key;
  int tableLength = 3;
  long sum = 0;
  //此处以key为9876543210为例
  //sum = 987 + 456 + 321 + 0 = 1962
  //result = 962
  //获取关键字长度,即10位
  int length = KeyLength(num);
  //10.0 / 3 = 3.3 > 3,即还有最后一位未分组,但又不足三位,也纳入组数
  /*if ((float)length / tableLength > tableLength)
  {
    group = tableLength + 1;
  }*/
  while (length > 0)
  {
    //sum = key % 1000;
    //9876543210 / (10*(10-3))即取前三位
    //即对最后一位0,进行取值时
    if (length - tableLength < 0)
    {
      sum += key % 10;
      length = length - tableLength;
      printf("sum=%d\n", sum);
    }
    else
    {
      sum += num / Times(length - tableLength);
      //printf("次数测试=%ld\n", Times(7));
      num = num % Times(length - tableLength);
      length = length - tableLength;
      printf("sum=%d\n", sum);
    }
  }
  //只取结果的后三位,故判定分组之和是否为三位数
  if (sum / 1000 > 0)
  {
    //大于0,则判定结果不为三位数,则对其进行取模操作,取其后三位
    sum %= 1000;
  }
  return sum;
}
/**
 随机数法:f(key) = Random(key)*/
int RandomAddr(int key)
{
  srand(key);
  int addr = rand();
  return addr;
}
int KeyLength(long key)
{
  int i = 0;
  while (key > 0)
  {
    key = key / 10;
    i++;
  }
  return i;
}
/**
 往哈希表内插入关键字*/
void InsertHash(HashTable *H,int key)
{
  int addr = ModHash(key);
  //如果不为NULLKEY,则说明存在冲突项,采用开放定址法的线性探索,不断自增地址取余,直到得到空余的下标,然后进行插入
  while (H->elem[addr] != NULLKEY)
  {
    addr = (addr + 1) % size;
  }
  H->elem[addr] = key;
}
/**
 在哈希表内进行关键字查找*/
int SearchHash(HashTable* H, int key)
{
  int addr = ModHash(key);
  //地址冲突
  while (H->elem[addr] != key)
  {
    addr = (addr + 1) % size;
    //等于NULLKEY则说明关键字不存在
    //如果addr循环了一圈还没有找到,并且最终与开始的addr相等,则说明没有在冲突的情况下找到该关键字
    if (H->elem[addr] == NULLKEY || addr == ModHash(key))
    {
      return FAIL;
    }
  }
  return addr;
}
/**
 主函数*/
void main(void)
{
  int HashArray[12] = { 12,67,56,16,25,37,22,29,15,47,48,34 };
  int index;
  int result;
  HashTable H;
  InitHash(&H);
  for (int i = 0; i < size; i++)
  {
    InsertHash(&H,HashArray[i]);
  }
  printf("哈希表为:");
  for (int i = 0; i < size; i++)
  {
    printf("%d\t", H.elem[i]);
  }
  printf("\n");
  printf("请输入你要查找的关键字:");
  scanf_s("%d", &index);
  result = SearchHash(&H, index);
  if (result == 0)
  {
    printf("没有在哈希表内查到相关关键字\n");
    return;
  }
  printf("你所查找的关键字在哈希表内的下标为:%d", result);
}


测试效果

以数组 { 12,67,56,16,25,37,22,29,15,47,48,34 };

查找关键字25,返回下标1

image.png

查找关键字16,返回下标4

image.png

查找关键字33,在哈希表内没有查到该关键字

image.png


以公共溢出区法为基础的全部代码



全部代码

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
//宏定义
#define Status int
#define SUCCESS 1
#define FAIL 0
#define TABLESIZE 12
#define NULLKEY -32768
int size = 0;//散列表表长
int overIndex = 0;//公共溢出区表下标
//结构体
typedef struct
{
  int count; //数据个数
  int* elem;  //数据存储的数组
}HashTable;
/**
 初始化哈希表*/
Status InitHash(HashTable* H)
{
  size = TABLESIZE;
  H->count = size;
  H->elem = (int*)malloc(size * sizeof(int));
  //如果内存申请失败,则初始化失败
  if (!H->elem)
  {
    return FAIL;
  }
  //将哈希表所有地址初始化为NULLKEY
  for (int i = 0; i < size; i++)
  {
    H->elem[i] = NULLKEY;
  }
  return SUCCESS;
}
/**
 散列函数:除留余数法*/
int ModHash(int key)
{
  return key % size;
}
int KeyLength(long key)
{
  int i = 0;
  while (key > 0)
  {
    key = key / 10;
    i++;
  }
  return i;
}
/**
 往哈希表内插入关键字*/
void InsertHash(HashTable* baseTable, HashTable* overFlow,int key)
{
  int addr = ModHash(key);
  //如果不为NULLKEY,则说明存在冲突项,采用公共溢出区法,将冲突项保存到溢出表中
  if (baseTable->elem[addr] != NULLKEY)
  {
    overFlow->elem[overIndex++] = key;
  }
  else
  {
    baseTable->elem[addr] = key;
  }
}
/**
 在哈希表内进行关键字查找*/
int SearchHash(HashTable* baseTable, HashTable* overFlow, int key)
{
  int addr = ModHash(key);
  //地址冲突
  if (baseTable->elem[addr] != key)
  {
    //在溢出表进行寻找
    for (int i = 0; i < size; i++)
    {
      if (overFlow->elem[i] == key)
      {
        return i;
      }
    }
    //没有匹配项
    return -1;
  }
  return addr;
}
/**
 主函数*/
void main(void)
{
  int HashArray[12] = {12,67,56,16,25,37,22,29,15,47,48,34 };
  int index;
  int result;
  HashTable baseTable,overFlow;
  InitHash(&baseTable);
  InitHash(&overFlow);
  /*printf("初始化数据为:");
  for (int i = 0; i < size; i++)
  {
    printf("%d ", baseTable.elem[i]);
  }
  printf("\n");*/
  for (int i = 0; i < size; i++)
  {
    InsertHash(&baseTable,&overFlow,HashArray[i]);
  }
  printf("基本哈希表数据为:");
  for (int i = 0; i < size; i++)
  {
    printf("%d\t", baseTable.elem[i]);
  }
  printf("\n");
  printf("溢出哈希表数据为:");
  for (int i = 0; i < size; i++)
  {
    printf("%d\t", overFlow.elem[i]);
  }
  printf("\n");
  printf("请输入你要查找的关键字:");
  scanf_s("%d", &index);
  result = SearchHash(&baseTable,&overFlow,index);
  if (result == -1)
  {
    printf("没有在哈希表内查到相关关键字\n");
    return;
  }
  printf("你所查找的关键字在哈希表内的下标为:%d", result);
}


测试效果

以数组 { 12,67,56,16,25,37,22,29,15,47,48,34 };

查找关键字25,在基本表中,返回基本表下标1

image.png


查找关键字34,在溢出表中,返回溢出表下标2


image.png


查找关键字17,在哈希表内没有查到该关键字

image.png


尾言


一个设计的几乎完美的哈希表,在应用于不同场合,都会出现散列冲突,只有根据不同场合,不同环境设计不同散列函数和处理散列冲突的方法可以有效避免散列冲突。

相关文章
|
前端开发 JavaScript Java
图解HTTP请求Tomcat服务器实现前后端交互-1
图解HTTP请求Tomcat服务器实现前后端交互
422 0
|
5月前
|
人工智能 缓存 前端开发
通义灵码2.5+qwen3——节假日抢票不用愁,基于12306-MCP实现个人火车票智能查询小助手!
本项目作为通义灵码2.5的深度实践案例,充分展现了通义灵码2.5编程智能体调用MCP实现大模型智能化工具的强大优势。
424 10
|
8月前
|
机器学习/深度学习 人工智能 物联网
微软Phi-4系列开源:多模态与文本处理的创新突破
微软近期推出 Phi-4-multimodal 和 Phi-4-mini,这些模型是 Microsoft Phi 系列小型语言模型 (SLM) 中的最新模型。Phi-4-multimodal 能够同时处理语音、视觉和文本,为创建创新且具有上下文感知能力的应用程序开辟了新的可能性。另一方面,Phi-4-mini 在基于文本的任务方面表现出色,以紧凑的形式提供高精度和可扩展性。
484 4
|
10月前
|
敏捷开发 存储 API
《小型开发者在鸿蒙Next上的成本与收益平衡之道》
鸿蒙Next系统的开发对小型开发者存在一定挑战。学习成本方面,需掌握新架构和API;开发成本受功能复杂度影响,经验不足会增加支出;设备成本因多设备测试需求较高;市场推广成本受限于资金资源。然而,鸿蒙系统也带来机遇:用户群体庞大、创新空间广阔、华为激励政策支持。通过利用开源资源、敏捷开发、聚焦垂直领域及合作,小型开发者可在鸿蒙生态中实现成功并获得收益。
376 4
|
架构师 关系型数据库 MySQL
MySQL最左前缀优化原则:深入解析与实战应用
【10月更文挑战第12天】在数据库架构设计与优化中,索引的使用是提升查询性能的关键手段之一。其中,MySQL的最左前缀优化原则(Leftmost Prefix Principle)是复合索引(Composite Index)应用中的核心策略。作为资深架构师,深入理解并掌握这一原则,对于平衡数据库性能与维护成本至关重要。本文将详细解读最左前缀优化原则的功能特点、业务场景、优缺点、底层原理,并通过Java示例展示其实现方式。
444 1
|
安全 算法 网络安全
量子计算与网络安全:保护数据的新方法
量子计算的崛起为网络安全带来了新的挑战和机遇。本文介绍了量子计算的基本原理,重点探讨了量子加密技术,如量子密钥分发(QKD)和量子签名,这些技术利用量子物理的特性,提供更高的安全性和可扩展性。未来,量子加密将在金融、政府通信等领域发挥重要作用,但仍需克服量子硬件不稳定性和算法优化等挑战。
|
存储 算法 数据处理
模拟数据读取函数有哪些
模拟数据读取函数主要用于测试和开发阶段,常见的有:numpy的random系列函数、pandas的DataFrame.sample()、Python内置的random模块等。这些函数可以生成随机或样本数据,方便快捷地进行数据处理和算法测试。
你知道Seata AT模式中前后镜像是如何生成的嘛?
你知道Seata AT模式中前后镜像是如何生成的嘛?
484 0
你知道Seata AT模式中前后镜像是如何生成的嘛?
|
机器学习/深度学习 数据采集 自然语言处理
深度学习之文本检索
文本检索(Text Retrieval)是指在大量文本数据中,根据用户的查询文本找到相关文档。基于深度学习的方法通过提取文本的高层次语义特征,实现了高效和准确的文本检索。
270 3