浅谈哈希表

Posted Li-Yongjun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈哈希表相关的知识,希望对你有一定的参考价值。

前言

最近,在启动一块嵌入式 Linux 开发板时,发现,同一份 uboot,从 SD 卡启动,和从 EMMC 启动,产生的效果不一样。最后发现是这两种存储介质中 uboot 的环境变量不一样。
遂去研究 uboot 环境变量存储机制

  • 存储在 flash 中
  • 存储在内存中

在 flash 中是以字符串的形式存放的。而在内存中,为了命令检索高效,是存储在哈希表(HashTable)中的。
哈希!了解,但只了解一点点。今天再次碰上了,就进一步了解下。

哈希表的作用

假设有一个长度为 100 的整型数组,数组中的每个元素的值也都是 100 以内,给出一个小于 100 的整数,你能以最快的速度告诉我,这个整数在数组中出现几次吗?
这里使用哈希表就是一个比较好的办法

创建一个长度为 100 的整型数组 h 作为哈希表,遍历待测数组 a,以待测数组中元素的值作为哈希表的下标,如
a[0] = 3,则 h[3]++
a[1] = 8,则 h[8]++

最后,哈希表中就记录了每个数字出现的次数,这时候有人来问,整数 x 在数组 a 中出现了几次,你就直接返回 h[x] 的值就行了。比你挨个遍历数组 a,比对是否等于 x,最后记录相等的次数,要快的多吧。
这只是一个简单的哈希表例子,实际应用中要比这复杂不少,数据类型也不是简单的整数类型,不过核心思想就是:

通过把关键码值映射到表中的一个位置来访问记录,以加快查找的速度。

uthash

研究哈希表可以使用 uthash 这个开源项目,据说这份代码已经被集成到最新的 GCC 了,可见其代码之优秀。
uthash 所有的代码都是用宏实现的,总共 6 个 .h 文件,所以你想使用 uthash 时,只要包含这些头文件就可以了。

感受

先直观感受下其查找的速度
(查找 0-99999 这 10 万个整数是否在目标数组中)
test10_2.c(传统双重循环遍历查找法)

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main()

	int i;
	int j;
	clock_t start, stop;
	double duration;
	int *a = malloc(sizeof(int) * 100000);

	for (i = 0; i < 100000; i++) 
		a[i] = i;
	

	start = clock();
	/*--------------------------------*/
	for (i = 0; i < 100000; i++) 
		for (j = 0; j < 100000; j++) 
			if (i == a[j]) 
				break;
			
		
	
	/*--------------------------------*/

	stop = clock();
	duration = (double)(stop - start) / 1000000;
	printf("%lf s\\n", duration);

	return 0;


test10_1.c(哈希表查找法)

#include <stdio.h>	/* printf */
#include <stdlib.h> /* malloc */
#include <time.h>
#include "uthash.h"

typedef struct example_user_t 
	int id;
	UT_hash_handle hh;
 example_user_t;

int main()

	int i;
	example_user_t *user, *tmp, *users = NULL;
	clock_t start, stop;
	double duration;

	/* create elements */
	for (i = 0; i < 100000; i++) 
		user = (example_user_t *)malloc(sizeof(example_user_t));
		if (user == NULL)
			exit(-1);

		user->id = i;
		HASH_ADD_INT(users, id, user);
	

	start = clock();
	/*--------------------------------*/
	for (i = 0; i < 100000; i++) 
		HASH_FIND_INT(users, &i, tmp);
	
	/*--------------------------------*/

	stop = clock();
	duration = (double)(stop - start) / 1000000;
	printf("%lf s\\n", duration);

	HASH_CLEAR(hh, users);

	return 0;


运行

$ ./test10_2
9.571326 s
$
./test10_1
0.022954 s

传统查找法耗时 9.5s,哈希查找法耗时 0.02s,可见哈希在查找方面的优越性。

分析源码

test10.c

#include <stdio.h>	/* printf */
#include <stdlib.h> /* malloc */
#include "uthash.h"

typedef struct example_user_t 
	int id;
	UT_hash_handle hh;
 example_user_t;

int main()

	int i;
	example_user_t *user, *tmp, *users = NULL;

	/* create elements */
	for (i = 0; i < 30; i++) 
		user = (example_user_t *)malloc(sizeof(example_user_t));
		if (user == NULL) 
			exit(-1);
		
		user->id = i;
		HASH_ADD_INT(users, id, user);
	

	i = 3;
	HASH_FIND_INT(users, &i, tmp);
	printf("%d %s in hh\\n\\n", i, (tmp != NULL) ? "found" : "not found");

	i = 5;
	HASH_FIND_INT(users, &i, tmp);
	printf("%d %s in hh\\n\\n", i, (tmp != NULL) ? "found" : "not found");

	i = 7;
	HASH_FIND_INT(users, &i, tmp);
	printf("%d %s in hh\\n\\n", i, (tmp != NULL) ? "found" : "not found");

	i = 1007;
	HASH_FIND_INT(users, &i, tmp);
	printf("%d %s in hh\\n", i, (tmp != NULL) ? "found" : "not found");

	HASH_CLEAR(hh, users);

	return 0;


将 1 - 30 这 30 个整数添加到哈希表,然后查询 3、5、7、1007 是否在哈希表中
运行

$ ./test10
3 found in hh
5 found in hh
7 found in hh
1007 not found in hh

加点打印,窥探其实现机理。

_ha_hashv = 0x2a333225
_ha_hashv = 0x6217f7a5
_ha_hashv = 0x6392f77c
_ha_hashv = 0xb68ef0e4
_ha_hashv = 0xdb3b8e5
_ha_hashv = 0x46fd8a31
_ha_hashv = 0x45f2400e
_ha_hashv = 0xb33656fa
_ha_hashv = 0x9d13c292
_ha_hashv = 0x74401341
_ha_hashv = 0x72f8f9ee
_ha_hashv = 0x36b478b8
_ha_hashv = 0xcc635388
_ha_hashv = 0x3fe7d7fc
_ha_hashv = 0x4042a413
_ha_hashv = 0x5615360e
_ha_hashv = 0x1006120
_ha_hashv = 0x48b34b17
_ha_hashv = 0x49fbfa2e
_ha_hashv = 0xd5e8b63d
_ha_hashv = 0xb0aec8fc
_ha_hashv = 0xcf292f65
_ha_hashv = 0xc8cee813
_ha_hashv = 0x8436b233
_ha_hashv = 0xd6af3c42
_ha_hashv = 0x6a16ee6b
_ha_hashv = 0x7288f523
_ha_hashv = 0x848fa6f1
_ha_hashv = 0x29f27f7f
_ha_hashv = 0x8168fce7

在将 1-30 这 30 个数字添加到哈希表时,分别对这 30 个数做了哈希 HASH_VALUE(),得到上面这 30 个哈希值。
将这 30 个值放入 32 个容器中,注意不是随便放置,是按照哈希值放置的,比如 3 对应的哈希值为 _ha_hashv = 0xb68ef0e4,将其和 0x1F 按位与,得到 4,就把 3 存入第 4 个容器中。
在查找 3 是,再次对 3 这个值做哈希,同样会得到 0xb68ef0e4 这个哈希值,和 1F 按位与,得到 4,就去第四个容器中寻找,由于在这 30 个值中,只有 3 会存放在第 4 个容器中,所以一下子就找到了。
如果有两个值存放于同一个容器中怎么办呢?比方说 5 和 27,两者的哈希值分别为 0x46fd8a31 和 0x848fa6f1,和 1F 按位与,都等于 0x11,则这两个值都要存入 0x11 也就是第 17 个容器(这就是哈希表冲突问题)。没关系,在 17 这个容器中再放置两个小容器,分别存放 5 和 27。这样,在查找时,先通过哈希值确定大容器的位置 17,再去 17 这个容器中遍历 2 个小容器,这样查找 2 次也就得到结果了,总比查找 30 次快得多吧。uthash 实现小容器的方式是链表
上面是存放 30 个值的情况,出现了两个值存入一个容器的冲突情况,出现冲突就会降低查找效率。可想而知,如果存放 300 个值,容器就 32 个,会有更多冲突,查找效率就会下降,那要怎么解决这个问题呢?uthash 采用的办法就是扩充容器数量,当样本数量达到某个阈值时,就会触发容器数量扩充,比方说样本数量为 300 时,容器数量就由 32 扩充到了 64,这样就可以一直保证哈希表的查找效率不降低。

源码地址

https://github.com/troydhanson/uthash.git

以上是关于浅谈哈希表的主要内容,如果未能解决你的问题,请参考以下文章

图书管理(Loj0034)+浅谈哈希表

为啥 Haskell Maps 实现为平衡二叉树而不是传统的哈希表?

浅谈GIT之底层对象理解

浅谈Perl的类包模块与面对对象编程

哈希表(散列表)

浅谈哈希算法