散列表

摘要

散列表的实现常常叫做散列(hashing).散列是一种用于以常数平均时间执行插入、删除和查找的技术。但是,那些需要元素间任何排序信息的操作将不会得到有效的支持。


直接寻址表

当关键字的全域U比较小时,直接寻址是一种简单而有效的技术。一般可以采用数组实现直接寻址表,数组下标对应的就是关键字的值,即具有关键字k的元素被放在直接寻址表的槽k中。直接寻址表的字典操作实现比较简单,直接操作数组即可,只需O(1)的时间

散列表

直接寻址表的不足之处在于当关键字的范围U很大时,在计算机内存容量的限制下,构造一个存储|U|大小的数组不太实际。当存储在字典中的关键字集合K比所有可能的关键字域U要小的多时,散列表需要的存储空间要比直接寻址表少的很多。散列表通过散列函数h计算出关键字k在槽的位置。散列函数h将关键字域U映射到散列表T[0...m-1]的槽位上:



采用散列函数的目的在于缩小需要处理的小标范围,从而降低空间的开销


散列函数

一个好的散列函数应(近似地)满足简单一致散列的假设:每个关键字都等可能地散列到m个槽位的任何一个之中去,并与其他的关键字已被散列到哪一个槽位中无关。多数散列函数都假定关键字域为自然数集 N = {0, 1, 2,...}.如果所给关键字不是自然数,则必须有一种方法来将它们解释为自然数

除法散列法

通过取k除以m的余数,来将关键字k映射到m个槽的某一个中去,散列函数为:

h(k) = k mod m;

注意:m不应是2的幂,通常m的值是与2的整数幂不太接近的质数

乘法散列法

用关键字k先乘上A,然后取出k * A 的小数部分,然后用m乘以这个值,再取结果的底(floor),散列函数为:

h(k) = floor(m * (k * A % 1));

根据研究,knuth认为A取(sqrt(5) - 1) / 2是一个比较理想的值(ps:我是没搞懂这个方法)

全域散列

全域散列用的方式是:随机地选择散列函数,使之独立于要存储的关键字,这样就很难出现最坏情况,平均性能很好,最后设计的散列函数为:

h(a, b) = ((ak + b) % p) % m;

这几个散列函数可以参考算法导论,我就是看了点皮毛,不多说了


碰撞处理

散列表的缺点就是容易出现冲突(也叫碰撞),两个关键字可能映射到同一个槽中,然后就产生了冲突,解决冲突的方法有很多种,这里只讨论其中最简单的两种:

链接法

就是把散列到同一个槽中的所有元素都放在一个链表中,如果,槽j中有一个指针,它指向所有散列到j的元素构成的链表的头;如果不存在这样的元素,则j为null,如图所示:

散列表_第1张图片

参考代码(c语言)

首先,这里是参考《深入理解php内核》中哈希表实现,原文链接: http://www.php-internals.com/book/?p=chapt03/03-01-01-hashtable

hash.h

哈希表数据结构&&接口定义头文件

#ifndef HASH_H
#define HASH_H

#define HASH_TABLE_INIT_SIZE 7

#define SUCCESS 1
#define FAILED 0


/**
 * 哈希表槽的数据结构
 */
typedef struct Bucket {
	char *key;
	void *value;
	struct Bucket *next;
} Bucket;

/**
 * 哈希表数据结构
 */
typedef struct HashTable {
	int size;	// 哈希表大小
	int elem_num;	// 哈希表已经保存的数据元素个数
	Bucket **buckets;
} HashTable;

int hashIndex(HashTable *ht, char *key);
int hashInit(HashTable *ht);
int hashLookup(HashTable *ht, char *key, void **result);
int hashInsert(HashTable *ht, char *key, void *value);
int hashRemove(HashTable *ht, char *key);
int hashDestory(HashTable *ht);
#endif

hash.c

哈希表操作函数具体实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hash.h"

/**
 * 初始化哈希表
 *
 * T = O(1)
 *
 */
int hashInit(HashTable *ht)
{
	ht->size = HASH_TABLE_INIT_SIZE;
	ht->elem_num = 0;
	ht->buckets = (Bucket **)calloc(ht->size, sizeof(Bucket *));

	if (ht->buckets == NULL)
		return FAILED;
	else
		return SUCCESS;
}

/**
 * 散列函数
 *
 * T = O(n)
 *
 */
int hashIndex(HashTable *ht, char *key)
{
	int hash = 0;

	while (*key != '\0') {
		hash += (int)*key;
		key ++;
	}

	return hash % ht->size;
}

/**
 * 哈希查找函数
 *
 * T = O(n)
 *
 */
int hashLookup(HashTable *ht, char *key, void **result)
{
	int index = hashIndex(ht, key);

	Bucket *bucket = ht->buckets[index];

	while (bucket) {
		if (strcmp(bucket->key, key) == 0) {
			*result = bucket->value;
			return SUCCESS;
		}
		bucket = bucket->next;
	}

	return FAILED;
}


/**
 * 哈希表插入操作
 *
 * T = O(1)
 *
 */
int hashInsert(HashTable *ht, char *key, void *value)
{
	int index = hashIndex(ht, key);

	Bucket *org_bucket, *tmp_bucket;
	org_bucket = tmp_bucket = ht->buckets[index];

	// 检查key是否已经存在于hash表中
	while (tmp_bucket) {
		if (strcmp(tmp_bucket->key, key) == 0) {
			tmp_bucket->value = value;
			return SUCCESS;
		}
		tmp_bucket = tmp_bucket->next;
	}

	Bucket *new = (Bucket *)malloc(sizeof(Bucket));
	
	if (new == NULL)	return FAILED;

	new->key = key;
	new->value = value;
	new->next = NULL;

	ht->elem_num += 1;

	// 头插法
	if (org_bucket) {
		new->next = org_bucket;
	}

	ht->buckets[index] = new;

	return SUCCESS; 
}

/**
 * 哈希删除函数
 *
 * T = O(n)
 *
 */
int hashRemove(HashTable *ht, char *key)
{
	int index = hashIndex(ht, key);

	Bucket *pre, *cur, *post;

	pre = NULL;
	cur = ht->buckets[index];

	while (cur) {
		if (strcmp(cur->key, key) == 0) {
			post = cur->next;
			
			if (pre == NULL) {
				ht->buckets[index] = post;
			} else {
				pre->next = post;
			}

			free(cur);

			return SUCCESS;
		}

		pre = cur;
		cur = cur->next;
	}

	return FAILED;
}

/**
 * 哈希表销毁函数
 *
 * T = O(n)
 */
int hashDestory(HashTable *ht)
{
	int i;
	Bucket *cur, *tmp;

	cur = tmp = NULL;

	for (i = 0; i < ht->size; i ++) {
		cur = ht->buckets[i];

		while (cur) {
			tmp = cur->next;
			free(cur);
			cur = tmp;
		}
	}

	free(ht->buckets);

	return SUCCESS;
}

test.c

单元测试文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include "hash.h"

int main(int argc, char **argv)
{
	HashTable *ht = (HashTable *)malloc(sizeof(HashTable));
	int result = hashInit(ht);

	assert(result == SUCCESS);

	/* Data */
	int int1 = 10;
	int int2 = 20;
	char str1[] = "Hello World!";
	char str2[] = "Value";
	char str3[] = "Hello New World!";

	/* to find data container */
	int *j = NULL;
	char *find_str = NULL;

	/* Test Key Insert */
	printf("Key Insert:\n");
	hashInsert(ht, "FirInt", &int1);
	hashInsert(ht, "FirStr", str1);
	hashInsert(ht, "SecStr", str2);
	printf("Pass Insert\n");

	/* Test Key Lookup*/
	printf("Key Lookup:\n");
	result = hashLookup(ht, "FirStr", &find_str);
	assert(result == SUCCESS);
	printf("pass lookup, the value is %s\n", find_str);

	/* Test Update */
	printf("Key Update:\n");
	hashInsert(ht, "FirStr", str3);
	result = hashLookup(ht, "FirStr", &find_str);
	assert(result == SUCCESS);
	printf("pass update, the value is %s\n", find_str);

	return 0;
}

编译方法

gcc -Wall -g -o main test.c hash.c

运行结果

散列表_第2张图片


开放寻址法

在开放寻址法(open addressing)中,所有的元素都存放在散列表里。亦即,每个表项或包含动态集合的一个元素,或包含NIL。当查找一个元素时,要检查所有的表项,直到找到所需的元素,或者最终发现该元素不在表中。不像在链接法中,这没有链表,也没有元素存放在散列表外。在这种方法中,散列表可能会被填满,以致于不能插入任何新的元素,但装载因子a是绝对不会超过1的

线性探测法

第一次冲突移动1个单位,再次冲突时,移动2个,再次冲突,移动3个单位,依此类推

它的散列函数是:H(x) = (Hash(x) + F(i)) mod TableSize, 且F(0) = 0

举例(腾讯面试题目)

已知一个线性表(38, 25, 74, 63, 52, 48),假定采用散列函数 h(key) = key % 7 计算散列地址,并散列存储在散列表 A[0..6]中,若采用线性探测方法解决冲突,则在该散列表上进行等概率成功查找的平均长度为 ?

下边模拟线性探测:

  • 38 % 7 == 3,  无冲突, ok
  • 25 % 7 == 4, 无冲突, ok
  • 74 % 7 == 4, 冲突, (4 + 1)% 7 == 5, 无冲突,ok
  • 63 % 7 == 0, 无冲突, ok
  • 52 % 7 == 3, 冲突, (3 + 1) % 7 == 4. 冲突, (4 + 1) % 7 == 5, 冲突, (5 + 1)%7 == 6,无冲突,ok
  • 48 % 7 == 6, 冲突, (6 + 1) % 7 == 0, 冲突,  (0 + 1) % 7 == 1,无冲突,ok

画图如下:

散列表_第3张图片


平均查找长度 = (1 + 3 + 1 + 1 + 2 + 3) % 6 = 2

线性探测方法比较容易实现,但它却存在一个问题,称为一次群集(primary clustering).随着时间的推移,连续被占用的槽不断增加,平均查找时间也随着不断增加。集群现象很容易出现,这是因为当一个空槽前有i个满的槽时,该空槽为下一个将被占用的槽的概率是 (i + 1 ) / n.连续占用的槽的序列会变得越来越长,因而平均查找时间也会随之增加

平方探测

为了避免上面提到的一个群集的问题:第一次冲突时移动1(1的平方)个单位,再次冲突时,移动4(2的平方)个单位,还冲突,移动9个单位,依此类推。F(i) = i * i





你可能感兴趣的:(散列表)