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

简介: 散列函数的构造方法数字分析法平方取中法平方取中法测试测例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


尾言


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

相关文章
|
11月前
|
存储 弹性计算 固态存储
阿里云X86架构云服务器解析:实例规格、性能特点、适用场景与收费价格参考
阿里云服务器架构分为X86计算、Arm计算、高性能计算等多种架构,其中X86计算是用户选择最多的一种架构,阿里云x86架构ECS实例覆盖从通用计算到高性能、高内存、高网络吞吐的多样化业务需求,结合弹性伸缩与多种计费模式,可有效支撑企业核心业务创新与成本优化。本文为大家介绍阿里云X86计算架构的云服务器,包括包含的实例规格、其技术特性、适用场景、性能优势以及最新价格情况,以供参考。
1552 1
|
11月前
|
存储 编解码
观海微电子---LCD的gamma校正原理
人眼对亮度感知呈非线性,暗环境下更敏感。Gamma曲线描述此特性,Gamma 2.2为标准校正值,使灰阶分布符合人眼感知,提升图像质量。通过digital Gamma可调节色温,实现护眼模式,减少有害蓝光,保护视力。
|
存储 运维 监控
为什么网络日志如此重要?
日志审计是网络安全的重要组成部分,通过分析网络日志,可快速定位故障、解决危机并提升系统安全性。网络日志记录了文件访问、用户登录等详细信息,甚至受某些法规约束需包含额外数据。日志审计无法被绕过,其在检测安全漏洞、法律取证和员工行为监控中作用显著。推荐一款工具EventLogAnalyzer,具备日志采集、分类存储、事件报警等功能,满足企业需求。购买时需注意品牌、功能及试用期,以选择最适合的产品。
683 11
|
安全 vr&ar 计算机视觉
AR智能眼镜在警务安防的人脸识别场景,有哪些应用和优势?
AR智能眼镜结合人脸识别技术,为安防领域带来高效精准的解决方案。通过实时采集与分析人脸信息,快速识别重点人员并即时报警,广泛应用于机场、车站、大型活动等场景,显著提升安全防控能力。
AR智能眼镜在警务安防的人脸识别场景,有哪些应用和优势?
|
数据采集 运维 监控
Serverless爬虫架构揭秘:动态IP、冷启动与成本优化
随着互联网数据采集需求的增长,传统爬虫架构因固定IP易封禁、资源浪费及扩展性差等问题逐渐显现。本文提出基于Serverless与代理IP技术的新一代爬虫方案,通过动态轮换IP、弹性调度任务等特性,显著提升启动效率、降低成本并增强并发能力。架构图与代码示例详细展示了其工作原理,性能对比数据显示采集成功率从71%提升至92%。行业案例表明,该方案在电商情报与价格对比平台中效果显著,未来有望成为主流趋势。
748 0
Serverless爬虫架构揭秘:动态IP、冷启动与成本优化
|
机器学习/深度学习 人工智能 Java
Java机器学习实战:基于DJL框架的手写数字识别全解析
在人工智能蓬勃发展的今天,Python凭借丰富的生态库(如TensorFlow、PyTorch)成为AI开发的首选语言。但Java作为企业级应用的基石,其在生产环境部署、性能优化和工程化方面的优势不容忽视。DJL(Deep Java Library)的出现完美填补了Java在深度学习领域的空白,它提供了一套统一的API,允许开发者无缝对接主流深度学习框架,将AI模型高效部署到Java生态中。本文将通过手写数字识别的完整流程,深入解析DJL框架的核心机制与应用实践。
1128 3
|
缓存 Unix 调度
[笔记]深入解析Windows操作系统《二》系统架构(一)
[笔记]深入解析Windows操作系统《二》系统架构
1476 0
|
搜索推荐 SEO
HTML中各种标题标签的正确使用方法及其特点
在HTML中,标题标签(`&lt;h1&gt;`至`&lt;h6&gt;`)用于定义文档的标题结构。`&lt;h1&gt;`表示主标题,每个页面应只有一个;`&lt;h2&gt;`至`&lt;h6&gt;`分别表示不同层级的子标题,可用于细分内容。正确使用这些标签不仅有助于文档的层次分明和可读性提升,还能优化SEO。使用时需注意保持层级结构连续、内容描述清晰。
|
机器学习/深度学习 人工智能 数据可视化
大数据时代的数据可视化技术:趋势、挑战与未来展望
【7月更文挑战第22天】随着技术的不断进步和应用场景的不断拓展,数据可视化技术将在更多领域发挥更大的作用。未来,我们可以期待更加智能化、实时化、沉浸式和民主化的数据可视化解决方案的出现。同时,随着数据量的不断增加和数据类型的不断丰富,数据可视化技术也将面临更多的挑战和机遇。只有不断创新和优化技术才能满足日益增长的需求并推动数据可视化技术的持续发展。
2223 3
|
Android开发 iOS开发
WebView加载https的空白及图片加载不了的问题
WebView加载https的空白及图片加载不了的问题
1142 0