• 散列查找的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);
    }
    
  • 相关阅读:
    centos7 hbase 搭建笔记
    【转】python 2.6.6升级到python 2.7.x版本的方法
    centos7 sqoop 1 搭建笔记
    centos7 hive + 远程mysql 搭建笔记
    docker 批量操作容器
    centos7 hdfs yarn spark 搭建笔记
    【转】mysql查看表空间占用情况
    【转】Centos 7 修改主机名hostname
    jQuery选择
    jQuery检查复选框是否被选
  • 原文地址:https://www.cnblogs.com/ark2000/p/11377484.html
Copyright © 2020-2023  润新知