(P22)-(P23)miniftpd项目实战22:散列表的特点,冲突处理的方法:链地址法

1.散列表的特点

  • 散列表是在建立的时候,将数据项的关键码与数据项在表中的存储位置进行映射。
    从而实现在查找的时候,可不必进行关键码的比较即可直接访问相应的数据项的一种数据结构。

  • 数据项所存储的表要用数组来实现
    因为只有数组是支持直接访问的,链表是不支持的。

  • 给定关键码,经过散列函数计算得到的是关键码对应的数据项在数组中存储的下标。

  • hash table与hash map
    用数组的形式来实现就称之为hash table,用动态数据结构来实现的,就称之为hash map

1)区别:区别在于表是否是动态的,如果是数组是不能动态扩展的,因为数组是静态的,如果是链表的话,就可以是动态扩展的
(2)
hash table:缺点:表空间不易扩展,优点:表查找比较快
hash map:缺点:查找比较慢,优点:表空间易扩展,因为存储的数据结构是动态的
  • 散列函数(哈希函数)的选取原则
    (1)快速:散列函数的计算比较快速
    通过散列函数,计算表数据项在表中的存储地址
    (2)均匀:尽可能的减少冲突
    所选取的关键码,这些数据项能均匀的分布在表的地址空间当中

  • 关键码的集合大小远远大于表的大小,这是一个压缩的过程,在压缩过程就有可能出现,所以两个或者多个不同的关键码,被映射到了同一个地址中,将这种现象称为冲突,将这些关键码称为同义词。

  • 拟定冲突处理的方案
    散列表最重要的2个地方:一个散列函数的选取,一个冲突处理的方案

2.冲突处理的方法:链地址法

  • 这种基本思想:将所有哈希地址为i的元素构成一个称为同义词链的链表,并将链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。

  • 开散列方法首先对关键码集合用某一个散列函数计算它们的存放位置。

  • 若设散列表地址空间的所有位置是从0到m-1,则关键码集合中的所有关键码被划分为m个子集,具有相同地址的关键码归于同一子集。我们称同一子集中的关键码互为同义词。每一个子集称为一个桶。

  • 通常各个桶中的表项通过一个链表链接起来,称之为同义词子表。所有桶号相同的表项都链接在同一个同义词子表中,各链表的表头结点组成一个向量。

  • 假设给出一组表项,它们的关键码为 Burke, Ekers, Broad, Blum, Attlee, Alton, Hecht, Ederly。采用的散列函数是:取其第一个字母在字母表中的位置序号-A的序号

hash (x) = ord (x) - ord (‘A’) 

这样,可得
		hash (Burke) = 1	hash (Ekers) = 4
		hash (Broad) = 1	hash (Blum) = 1
		hash (Attlee) = 0	hash (Hecht) = 7
		hash (Alton) = 0	hash (Ederly) = 4
  • 图片如下
    在这里插入图片描述

  • eg:hash02.tar中的hash.c中的void hash_add_entry(hash_t *hash, void *key, unsigned int key_size,
    void *value, unsigned int value_size)

		// 将新结点插入到链表头部
		node->next = *bucket;//node节点的后继,对应(1)
		(*bucket)->prev = node;//*bucket节点的前驱,对应(2)
		*bucket = node;//对应(3)

在这里插入图片描述

  • eg:hash02.tar中hash.c中的void hash_free_entry(hash_t *hash, void *key, unsigned int key_size)
	//添加一个新的节点
	hash_node_t *node = malloc(sizeof(hash_node_t));
	node->prev = NULL;
	node->next = NULL;

	node->key = malloc(key_size);
	memcpy(node->key, key, key_size);

	node->value = malloc(value_size);
	memcpy(node->value, value, value_size);

	hash_node_t **bucket = hash_get_bucket(hash, key);
	if (*bucket == NULL)//*bucket说明链表不存在
	{
		*bucket = node;
	}
	else
	{
		// 将新结点插入到链表头部(头插法)
		node->next = *bucket;//node节点的后继,对应(1)
		(*bucket)->prev = node;//*bucket节点的前驱,对应(2)
		*bucket = node;//对应(3)
	}

释放一个节点,核心代码和思想如下:
中间节点是node,node的前驱指向下一个节点,即(1)
node后继指向node的上一个节点就是(2)
在这里插入图片描述
在这里插入图片描述

3.测试

  • eg:hash03\main.c,带有注释的

  • 测试:以字符串作为关键码
    在这里插入图片描述

  • eg:hash03\main.c,不带有注释的

  • 测试:整数作为关键码
    在这里插入图片描述

  • 总结:通常,每个桶中的同义词子表都很短,设有n个关键码通过某一个散列函数,存放到散列表中的 m 个桶中。那么每一个桶中的同义词子表的平均长度为 n / m。这样,以搜索平均长度为 n / m 的同义词子表代替了搜索长度为 n 的顺序表,搜索速度快得多。
    (1)节点数有n个(n仅代表当前哈希表中的关键码的数目,就是表空间大小),桶的大小是m个,平均子表的个数就是n/m,每个子表的数目就是n/m,
    算法时间复杂度就是n/m(查找次数),因为通过哈希函数得到一个地址,然后在链表中进行遍历,最终需要n/m次查找到相应的数据项
    在这里插入图片描述
    (2)算法的时间复杂度是n/m,是一个常数,就是O(1),顺序查找的时间复杂度是O(n),二分查找的时间复杂度是O( l o g 2 n log_2n log2n)(n说明二分查找的时间复杂度还与表空间大小有关),所以链地址法的速度最快

  • 总结:
    应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上,由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装填因子α<= 0.5,而表项所占空间又比指针大得多,所以使用链地址法反而比开地址法节省存储空间。

  • 打印hash表中所有的非空数据
    hash02\hash.c


typedef struct stu2
{
	int sno;
	char name[32];
	int age;
}stu2_t;
void hash_get_all_useful_node_by_key(hash_t *hash)
{
	printf("hash_get_all_useful_node_by_key start\n");
	unsigned int hash_tab_size = hash->buckets;
	for (int i = 0; i < hash_tab_size; ++i)
	{
		hash_node_t* bucket_head = hash->nodes[i];
		if (bucket_head == NULL)
			continue;
		else
		{
			while (bucket_head)
			{
				printf("%-d: %s\n", *(int*)bucket_head->key, ((stu2_t*)(bucket_head->value))->name);
				bucket_head = bucket_head->next;
			}
		}
	}

	printf("hash_get_all_useful_node_by_key end\n");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜欢打篮球的普通人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值