散列查找的C实现

概念

散列查找,类似与查英文字典的过程。如果我们要查找“zoo”(key)对应的释义(value),我们不会从第一页开始逐页查找(顺序查找),而是直接根据大致的推算(Hash函数),找到最后几页。

所以散列查找的基本思想是根据key通过hash函数计算出value的位置,然后得到value。
按理说每一个key都对应不同的地址是最理想的,然而这种hash函数很难找到(或者说没有实用意义,比如线性函数)就会发生多个key对应一个地址的现象,这是就需要去处理冲突。

数字关键字的散列函数的构造方法一般有:

1)直接定址法

h(key) = a * key + b

计算简单,且不会产生冲突,但要求地址集合与关键词集合大小相同,所以并不实用。

数组使用的就是直接定址法。

2)除留余数法

h(key) = key mod p

一般选取key为素数,这样均匀分布的可能性比较大。

3)数字分析法

就是根据具体情况来处理。比如使用身份证号码作为关键字,那么应选取其中比较随机的数值,比如前两位代表省编号的数字就不适合,会导致聚集效应。

字符关键字的散列函数构造

1)ASCII码加和法

h(key) = (∑key[i]) mod TableSize

即将每一位字符对应的ASCII码加起来求和作为特征值。但这种方法太过简单,假设最大长度为8的字符串,(∑key[i])的取值区间为0至1016,而关键词集合包含有63^8种关键词,如此显然会造成严重的冲突。

2)简单改进——前三个字符移位法

h(key) = (key[0] + key[1]*27 + key[2]*27^2) mod TableSize

3)好的散列函数——移位法

只考虑26个字母,那么一个字母至少需要5位去存储,假设字符串最大长度为12,那么取一个64位长度的无符号整数,依次移位将字符串映射到整数上去,便得到了特征值,不会出现重复。

处理冲突的方法

1)开放地址法

1.线性探测法

比如在插入时发生冲突(已被占据),那么逐次向后查找空位。

会产生一次聚集(Primary Clustering)现象

2.平方探测法
image

2)分离链表法
image

实现

//头文件
//HashTable.h
#ifndef HASHTABLE_H_
#define HASHTABLE_H_
#include <stdbool.h>

#define KEYLENGTH 20
#define MAXTABLESIZE 100000

//哈希表类型
typedef struct TblNode* HashTable;
//关键字类型为字符串,最大长度由KEYLENGTH宏定义,只能包含大小写字母,数字,及下划线和空格
typedef char KeyType[KEYLENGTH + 1];
//值类型,为void指针
typedef void* ValueType;

//自定义的释放数据函数指针类型
typedef void (*FreeIt)(void* data);

//创建一个容量为N的散列表(N < MAXTABLESIZE)
//表越大性能越好,同时也越占据内存
HashTable CreateTable(int TableSize);

//向表中 插入/修改 键值关系,如果出错则返回false(比如key中有非法字符)
bool InsertEntry(HashTable H, const KeyType key, const ValueType data);

//通过key-value的关系获取值,如果不存在或key中有非法字符则返回NULL
ValueType getValue(const HashTable H, const KeyType key);

//通过key来删除关系,失败返回false
bool DelEntry(HashTable H, const KeyType key);

//释放散列表占据的内存
//如果不需要释放数据,则第二个为NULL
void DestroyTable(HashTable H, FreeIt func);

#endif
//HashTable.c
#include "HashTable.h"
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <malloc.h>
#include <math.h>
#include <stdio.h>

#define ERROR -1

//一个存储单元包括键,值,及状态标记
typedef struct HashEntry Cell;

struct HashEntry {
	KeyType Key;	//Key是一个数组
	ValueType Data;
	Cell* Next;	//单向链表
};

//哈希表,存放存储单元数组
//主干使用数组,分支使用链表
typedef struct TblNode* HashTable;
struct TblNode {
	int TableSize;
	Cell* Cells;
};

//将单个字符(数字、字母、下划线或空格)映射到0-63上
//返回ERROR表示出错
static int CharMap_(char ch)
{
	//数字映射到0-9
	if (isdigit(ch)) {
		return ch - '0';
	} else
	//大写字母映射到10-35
	if (isupper(ch)) {
		return ch - 'A' + 10;
	} else
	//小写字母映射到36-61
	if(islower(ch)) {
		return ch - 'a' + 36;
	} else
	//空格映射到62
	if (ch == ' ') {
		return 62;
	} else
	//下划线映射到63
	if (ch == '_') {
		return 63;
	} else {
	//超出映射范围
		fprintf(stderr, "Error key format
");
		return ERROR;
	}
}

//利用移位法计算字符串关键字映射的地址
//每一个字符有26*2+2+10=2^6,64/6=10,
//字符串最多10位,如果超过10位,便只取奇数位数字
//于是有效长度为20位
//如果出错,返回ERROR
static int Hash(const char* key, int TableSize)
{
	//关键字长度
	int len = strlen(key);
	//步长
	int step = (len > 10) ? 2 : 1;
	//字符串尾指针
	const char* const end = key + len;

	//散列函数值,初始化为0
	uint64_t H = 0;

	//每一次左移6位,把字符的6位映射刻上去
	while (key < end) {
		int val = CharMap_(*key);
		if (val == ERROR) {
			//错误传递
			fprintf(stderr, "Error in Hash()
");
			return ERROR;
		}
		
		H = (H << 6) + val;
		key += step;
	}

	//将计算出来的字符串特征值对表长取模即为地址
	return (H % TableSize);
}

//返回大于N且小于MAXTABLESIZE的最小素数
static int NextPrime(int N)
{
	//从大于N的下一个奇数开始
	int i, p = (N % 2) ? (N + 2) : (N + 1);

	while (p < MAXTABLESIZE) {
		for (i = (int)sqrt(p); i > 2; --i) {
			if (p % i == 0) break;
		}
		//说明for没有中途退出,p是素数
		if (i == 2) break;
		//否则试探下一个素数
		else p += 2;
	}
	return p;
}

//找到key对应的cell的前一个cell的指针,如果返回NULL说明出了问题
//正常情况下不会返回NULL
static Cell* FindEntryPre(const HashTable H, const KeyType key)
{
	//计算得到数组中的位置
	int p = Hash(key, H->TableSize);
	if (p == ERROR) {
		return NULL;
	}
	Cell* pre = &(H->Cells[p]);
	for (; (pre->Next) && strcmp(key, (pre->Next)->Key); pre = pre->Next);
	return pre;
}

//创建一个容量为N的散列表(N < MAXTABLESIZE)
HashTable CreateTable(int TableSize)
{
	HashTable H = (HashTable)malloc(sizeof(struct TblNode));
	//散列表中的数组的实际大小比输入的TableSize大一点
	H->TableSize = NextPrime(TableSize);

	//为数组分配内存
	H->Cells = (Cell*)malloc(sizeof(Cell) * H->TableSize);

	//初始化数组
	for (int i = 0; i < H->TableSize; ++i) {
		H->Cells[i].Data = NULL;
		H->Cells[i].Key[0] = '';
		H->Cells[i].Next = NULL;
	}

	return H;
}

//插入键值关系,如果出错则返回false(比如key中有非法字符)
bool InsertEntry(HashTable H, const KeyType key, const ValueType data)
{
	Cell* pre = FindEntryPre(H, key);	
	if (pre == NULL) {
		fprintf(stderr, "Error in InsertEntry()
");
		return false;
	}
	//如果是空的,那就新建一个
	if (pre->Next == NULL) {
		pre->Next = (Cell*)malloc(sizeof(Cell));
		strcpy((pre->Next)->Key, key);
		pre->Next->Next = NULL;
	}
	//修改对应的value
	pre->Next->Data = data;
	return true;
}

//key-value获取值,如果不存在或key中有非法字符则返回NULL
ValueType getValue(const HashTable H, const KeyType key)
{
	Cell* pre = FindEntryPre(H, key);
	if (pre == NULL) {
		fprintf(stderr, "Error in InsertEntry()
");
		return NULL;
	}
	//不存在,则返回NULL
	if ((pre->Next) == NULL) {
		return NULL;
	} else {
		return pre->Next->Data;
	}
}

//通过key来删除关系
bool DelEntry(HashTable H, const KeyType key)
{
	Cell* pre = FindEntryPre(H, key);
	if (pre == NULL) {
		fprintf(stderr, "Error in DelEntry()
");
		return false;
	} else
	if (pre->Next == NULL) {
		printf("Can not find %s!
", key);
	} else {
		//删除此链表节点
		Cell* mid = pre->Next;
		pre->Next = mid->Next;
		free(mid);
	}
	return true;
}

//从给定位置开始释放单项链表(递归)
static void freeList_(Cell* begin, FreeIt func)
{
	if (begin) {
		freeList_(begin->Next, func);
		if (func) func(begin->Data);
		free(begin);
	}
}

//释放散列表占据的内存
//如果不需要释放数据,则第二个为NULL
void DestroyTable(HashTable H, FreeIt func)
{
	//首先释放分支上的链表
	for (int i = 0; i < H->TableSize; ++i) {
		//跳过链表头,递归释放
		freeList_(H->Cells[i].Next, func);
	}
	//然后释放主干的数组
	free(H->Cells);
	//最后释放表
	free(H);
}
原文地址:https://www.cnblogs.com/ark2000/p/11377484.html