23.0、C语言数据结构——散列表(哈希表)查找、
散列表(哈希表)查找:
散列技术是在记录的存储位置和他的关键字之间建立一个确定的对应关系 f ,使得每个关键字 key 对应一个存储位置 f ( key );
这里我们把这种对应关系 f 称为散列函数,又称为哈希( Hash )函数;采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table)
散列表的查找步骤:
当存储记录时,通过散列函数计算出记录的散列地址;
当查找记录时,我们通过同样的是散列函数计算出的散列地址,并按次散列地址进行访问;
构造散列函数的两个基本原则:
1. 计算出来的散列地址需要分布均匀
2. 计算的函数简单
直接定址法:
例 1 :有一个从 1 到 100 岁的人口数字统计表,其中 年龄作为关键字 key ,哈希函数取关键字自身
即: f ( key ) = key;
地址的选取就是直接取决于年龄的值
数据分析法:
数字分析法通常适合处理关键字尾数比较大的情况,例如我们现在要存储某家公司员工登记表,如果用手机号作为关键字,那么我们发现抽取后面的四位数字作为散列地址是不错的选择;
这里因为前三位是运营商的固定号码,中间四位是识别码(用于标志归属地)可能重复,后面四位才是区分不同用户的号码;所以我们选择用后面四位来作为散列表的关键字;
平方取中法:
平方取中法是将关键字平方之后取中间若干位数字作为散列地址;
下面给大家举个例子:
假设关键字为 -> 1234 那么将 1234 平方之后等于 1522759,那么取中间的值 227 作为散列地址;
折叠法:
折叠法是将关键字从左到右分割成位数相等的几部分,然后将这几个部分叠加求和,并按照散列表表长取后几位作为散列地址;
例如: 9876543210 将这个数字以 三个数字为一组 不够的补 0 ,得到 987 654 321 000 这四组数字,然后将这四组数字相加得到 -> 1962 最后取出最后三位 962 作为散列表的地址;
除留余数法:
此方法为最常用的构造散列函数方法,对于散列表长为 m 的散列函数计算公式为:
f ( key ) = key mod p ( p <= m )
事实上,这个方法不仅可以对关键字直接取模,也可以通过折叠、平方取中后再取模;
例如下表,我们对有 12 个记录的关键字构造散列表时,就可以用 f ( key ) = key mod 12 的方法;
每个关键字在模上 12 之后得到他应该存储的地址位置;
但是我们会发现如果有两个取模得到相同数字的关键字的话就会导致地址冲突;可以用以下解决办法;
当遇到取模后地址冲突时,我们将第一次取模后的值依次 + 1、+2、+3、+4 ...... 后再取模查看是否有存储空间 ,一直加到有空出的地址来存放关键字为止;
【比如说 第一次取模得到 2 ,但是发现 2 的位置上已经存储有值了,此时我们将 2 + 1 = 3 然后用 3 取模,要是3取模的地址也有值了就用 2 + 2 = 4 然后用4取模,4 取模也有了 就用 2 + 3 = 5 取模,依次...... 】
随机数法:
选择一个随机数,取关键字的随机函数值作为他的散列地址;
即:f ( key ) = random ( key );
这里的random是随机函数,当关键字的长度不等时,采用这个方法构造散列函数是比较合适的;
总而言之在现实中,我们应该是不同的情况采用不同的散列函数,这里给大家一些参考方向 ->
- 计算散列地址所需的时间;
- 关键字的长度;
- 散列表的大小;
- 关键字的分布情况;
- 记录查找的频率;
接下用代码去实现一下除留余数法:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#define TABLE_MAX 10
#define DATA_DEFAULT -9999
//定义哈希表结构体
typedef struct HashTabel {
int* data;
int count;
}Table;
//哈希表初始化
void InitHashTable(Table* table) {
table->count = TABLE_MAX;
int* tmp = (int*)malloc(TABLE_MAX * sizeof(int));
assert(tmp != NULL);
table->data = tmp;
int i = 0;
for (i = 0;i < TABLE_MAX;i++) {
(table->data)[i] = DATA_DEFAULT;
}
}
//除留余数法
int DivideTheRemainderMthod(int key) {
return key % TABLE_MAX;
}
//插入关键字
void insertKey(int key,Table* table) {
int address = DivideTheRemainderMthod(key); //第一次取模的结果
int num = 1; //如果取模没有找到key ,就依次 +1 +2 +3
int tmp = address; //用一个tmp临时变量记录第一次取模的结果
while((table->data)[tmp] != DATA_DEFAULT) {
tmp = (address + num) % TABLE_MAX; //模上一个数的值一定小于模上的这个数
num++;
}
(table->data)[tmp] = key;
}
//通过关键字查找他的地址
int* queryKey(Table* table,int key) {
int address = DivideTheRemainderMthod(key);//第一次取模的结果
int num = 1; //如果取模没有找到key ,就依次 +1 +2 +3
int tmp = address; //用一个tmp临时变量记录第一次取模的结果
while(key != (table->data)[tmp]) { //如果 key 不等于第一次取模得到地址上key的值,就让他++
tmp = (address + num) % TABLE_MAX;
num++;
//如果找到的结果等于默认值或者回到第一次取模的结果,却还没有找到 key 的话说明该 key 不存在
if ((table->data)[tmp] == DATA_DEFAULT || tmp == address) {
printf("该关键字 %d 未能找到",key);
return NULL;
}
}
return table->data + tmp;
}
int main() {
return 0;
}