散列

散列的基本概念

散列方法的主要思想是根据结点的关键码值来确定其存储地址:以关键码值K为自变量,通过一定的函数关系h(K)(称为散列函数),计算出对应的函数值来,把这个值解释为结点的存储地址,将结点存入到此存储单元中。检索时,用同样的方法计算地址,然后到相应的单元里去取要找的结点。通过散列方法可以对结点进行快速检索。散列(hash,也称“哈希”)是一种重要的存储方式,也是一种常见的检索方法

按散列存储方式构造的存储结构称为散列表(hash table)。散列表中的一个位置称为槽(slot)。散列技术的核心是散列函数(hash function)。 对任意给定的动态查找表DL,如果选定了某个“理想的”散列函数h及相应的散列表HT,则对DL中的每个数据元素X。函数值h(X.key)就是X在散列表HT中的存储位置。插入(或建表)时数据元素X将被安置在该位置上,并且检索X时也到该位置上去查找。由散列函数决定的存储位置称为散列地址。 因此,散列的核心就是:由散列函数决定关键码值(X.key)与散列地址h(X.key)之间的对应关系,通过这种关系来实现组织存储并进行检索。

一般情况下,散列表的存储空间是一个一维数组HT[M],散列地址是数组的下标。设计散列方法的目标,就是设计某个散列函数h,0<=h( K ) < M;对于关键码值K,得到HT[i] = K。 在一般情况下,散列表的空间必须比结点的集合大,此时虽然浪费了一定的空间,但换取的是检索效率。设散列表的空间大小为M,填入表中的结点数为N,则称为散列表的负载因子(load factor,也有人翻译为“装填因子”)。建立散列表时,若关键码与散列地址是一对一的关系,则在检索时只需根据散列函数对给定值进行某种运算,即可得到待查结点的存储位置。但是,散列函数可能对于不相等的关键码计算出相同的散列地址,我们称该现象为冲突(collision),发生冲突的两个关键码称为该散列函数的同义词。在实际应用中,很少存在不产生冲突的散列函数,我们必须考虑在冲突发生时的处理办法。

因此,采用散列技术时需要考虑的两个首要问题是:
(1)如何构造(选择)使结点“分布均匀”的散列函数?
(2)一旦发生冲突,用什么方法来解决?

二)散列函数

在以下的讨论中,我们假设处理的是值为整型的关键码,否则我们总可以建立一种关键码与正整数之间的一一对应关系,从而把该关键码的检索转化为对与其对应的正整数的检索;同时,进一步假定散列函数的值落在0到M-1之间。散列函数的选取原则是:运算尽可能简单;函数的值域必须在散列表的范围内;尽可能使得结点均匀分布,也就是尽量让不同的关键码具有不同的散列函数值。需要考虑各种因素:关键码长度、散列表大小、关键码分布情况、记录的检索频率等等。下面我们介绍几种常用的散列函数。
1、除余法
顾名思义,除余法就是用关键码x除以M(往往取散列表长度),并取余数作为散列地址。除余法几乎是最简单的散列方法,散列函数为: h(x) = x mod M。

2、乘余取整法
使用此方法时,先让关键码key乘上一个常数A (0< A < 1),提取乘积的小数部分。然后,再用整数n乘以这个值,对结果向下取整,把它做为散列的地址。散列函数为: hash ( key ) = _LOW( n × ( A × key % 1 ) )。 其中,“A × key % 1”表示取 A × key 小数部分,即: A × key % 1 = A × key - _LOW(A × key), 而_LOW(X)是表示对X取下整。

3、平方取中法
由于整数相除的运行速度通常比相乘要慢,所以有意识地避免使用除余法运算可以提高散列算法的运行时间。平方取中法的具体实现是:先通过求关键码的平方值,从而扩大相近数的差别,然后根据表长度取中间的几位数(往往取二进制的比特位)作为散列函数值。因为一个乘积的中间几位数与乘数的每一数位都相关,所以由此产生的散列地址较为均匀。

4、数字分析法
设有 n 个 d 位数,每一位可能有 r 种不同的符号。这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布均匀些,每种符号出现的几率均等; 在某些位上分布不均匀,只有某几种符号经常出现。可根据散列表的大小,选取其中各种符号分布均匀的若干位作为散列地址。

5、基数转换法

将关键码值看成另一种进制的数再转换成原来进制的数,然后选其中几位作为散列地址。

6、折叠法
有时关键码所含的位数很多,采用平方取中法计算太复杂,则可将关键码分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址,这方法称为折叠法。

7、ELFhash字符串散列函数
ELFhash函数在UNIX系统V 版本4中的“可执行链接格式”( Executable and Linking Format,即ELF )中会用到,ELF文件格式用于存储可执行文件与目标文件。ELFhash函数是对字符串的散列。它对于长字符串和短字符串都很有效,字符串中每个字符都有同样的作用,它巧妙地对字符的ASCII编码值进行计算,ELFhash函数对于能够比较均匀地把字符串分布在散列表中。

三)冲突解决的策略

尽管散列函数的目标是使得冲突最少,但实际上冲突是无法避免的。因此,我们必须研究冲突解决策略。冲突解决技术可以分为两类:开散列方法( open hashing,也称为拉链法,separate chaining )和闭散列方法( closed hashing,也称为开地址方法,open addressing )。这两种方法的不同之处在于:开散列法把发生冲突的关键码存储在散列表主表之外,而闭散列法把发生冲突的关键码存储在表中另一个槽内。

开散列方法:

1、拉链法

开散列方法的一种简单形式是把散列表中的每个槽定义为一个链表的表头。散列到一个特定槽的所有记录都放到这个槽的链表中。图9-5说明了一个开散列的散列表,这个表中每一个槽存储一个记录和一个指向链表其余部分的指针。这7个数存储在有11个槽的散列表中,使用的散列函数是h(K) = K mod 11。数的插入顺序是77、7、110、95、14、75和62。有2个值散列到第0个槽,1个值散列到第3个槽,3个值散列到第7个槽,1个值散列到第9个槽。

2、桶式散列

桶式散列方法的基本思想是把一个文件的记录分为若干存储桶,每个存储桶包含一个或多个页块,一个存储桶内的各页块用指针连接起来,每个页块包含若干记录。散列函数h把关键码值K转换为存储桶号,即h(K)表示具有关键码值K的记录所在的存储桶号。 图9-6表示了一个具有B个存储桶的散列文件组织。有一个存储桶目录表,存放B个指针,每个存储桶一个,每个指针就是所对应存储桶的第一个页块的地址。

有些存储桶仅仅由一个页块组成,如下图中的1号存储桶。有的存储桶由多个页块组成,每一个页块的块头上有一个指向下一个页块的指针,例如,如下图中的第B-1号存储桶由b4,b5,b6三个页块组成,每个存储桶中最后一个页块的头上为空指针。

闭散列方法:

闭散列方法把所有记录直接存储在散列表中。每个记录关键码key有一个由散列函数计算出来的基位置,即h(key)。如果要插入一个关键码,而另一个记录已经占据了R的基位置(发生碰撞),那么就把R存储在表中的其它地址内,由冲突解决策略确定是哪个地址。

闭散列表解决冲突的基本思想是:当冲突发生时,使用某种方法为关键码K生成一个散列地址序列d0,d1,d2,... di ,...dm-1。其中d0=h(K)称为K的基地址地置( home position );所有di(0< i< m)是后继散列地址。当插入K时,若基地址上的结点已被别的数据元素占用,则按上述地址序列依次探查,将找到的第一个开放的空闲位置di作为K的存储位置;若所有后继散列地址都不空闲,说明该闭散列表已满,报告溢出。相应地,检索K时,将按同值的后继地址序列依次查找,检索成功时返回该位置di ;如果沿着探查序列检索时,遇到了开放的空闲地址,则说明表中没有待查的关键码。删除K时,也按同值的后继地址序列依次查找,查找到某个位置di具有该K值,则删除该位置di上的数据元素(删除操作实际上只是对该结点加以删除标记);如果遇到了开放的空闲地址,则说明表中没有待删除的关键码。因此,对于闭散列表来说,构造后继散列地址序列的方法,也就是处理冲突的方法。

形成探查的方法不同,所得到的解决冲突的方法也不同。下面是几种常见的构造方法。

1、线性探查法

将散列表看成是一个环形表,若在基地址d(即h(K)=d)发生冲突,则依次探查下述地址单元:d+1,d+2,......,M-1,0,1,......,d-1直到找到一个空闲地址或查找到关键码为key的结点为止。当然,若沿着该探查序列检索一遍之后,又回到了地址d,则无论是做插入操作还是做检索操作,都意味着失败。 用于简单线性探查的探查函数是: p(K,i) = i

例9.7 已知一组关键码为(26,36,41,38,44,15,68,12,06,51,25),散列表长度M= 15,用线性探查法解决冲突构造这组关键码的散列表。 因为n=11,利用除余法构造散列函数,选取小于M的最大质数P=13,则散列函数为:h(key) = key%13。按顺序插入各个结点: 26: h(26) = 0,36: h(36) = 10, 41: h(41) = 2,38: h(38) = 12, 44: h(44) = 5。 插入15时,其散列地址为2,由于2已被关键码为41的元素占用,故需进行探查。按顺序探查法,显然3为开放的空闲地址,故可将其放在3单元。类似地,68和12可分别放在4和13单元中.

2、二次探查法

二次探查法的基本思想是:生成的后继散列地址不是连续的,而是跳跃式的,以便为后续数据元素留下空间从而减少聚集。二次探查法的探查序列依次为:12,-12,22 ,-22,...等,也就是说,发生冲突时,将同义词来回散列在第一个地址的两端。求下一个开放地址的公式为:

3、随机探查法

理想的探查函数应当在探查序列中随机地从未访问过的槽中选择下一个位置,即探查序列应当是散列表位置的一个随机排列。但是,我们实际上不能随机地从探查序列中选择一个位置,因为在检索关键码的时候不能建立起同样的探查序列。然而,我们可以做一些类似于伪随机探查( pseudo-random probing )的事情。在伪随机探查中,探查序列中的第i个槽是(h(K) + ri) mod M,其中ri是1到M - 1之间数的“随机”数序列。所有插入和检索都使用相同的“随机”数。探查函数将是 p(K,i) = perm[i - 1], 这里perm是一个长度为M - 1的数组,它包含值从1到M – 1的随机序列。

4、双散列探查法

伪随机探查和二次探查都能消除基本聚集——即基地址不同的关键码,其探查序列的某些段重叠在一起——的问题。然而,如果两个关键码散列到同一个基地址,那么采用这两种方法还是得到同样的探查序列,仍然会产生聚集。这是因为伪随机探查和二次探查产生的探查序列只是基地址的函数,而不是原来关键码值的函数。这个问题称为二级聚集( secondary clustering )。

为了避免二级聚集,我们需要使得探查序列是原来关键码值的函数,而不是基位置的函数。双散列探查法利用第二个散列函数作为常数,每次跳过常数项,做线性探查。

四)散列的检索效率分析

我们可以根据完成一次操作,即插入、删除和检索操作,所需要的记录访问次数来衡量散列方法的性能。由于散列表的插入和删除操作都是基于检索进行的:在删除一条记录之前必须先找到该记录,因此删除一条记录之前需要的访问数等于成功检索到它需要的访问数;而插入一条记录时,必须找到探查序列的尾部(对于不考虑删除的情况,是尾部的空槽;对于考虑删除的情况,也要找到尾部,才能确定是否有重复记录),这等于对这条记录进行一次不成功的检索。因此,散列表的效率实质上还是平均检索长度,而且我们需要区别对待成功的检索与不成功的检索。

当散列表比较空的时候,所插入的记录比较容易插入到其空闲的基地址。如果散列表中的记录比较多,插入记录时,很可能要靠冲突解决策略来寻找探查序列中合适的另一个槽。而且,检索记录时,很多时候需要沿着探查序列逐个查找。随着散列表记录不断增加,越来越多的记录有可能放到离其基地址更远的地方。

根据这些讨论,我们可以看到散列方法预期的代价与负载因子α= N/M有关。其中,M是散列表存储空间大小,N是表中当前的记录数目。

从图9-8可以看出,开散列方法的效率最好,实际系统中使用的散列大多都是开散列。开散列方法非常简单、易于实现,它不会产生聚集现象(聚集导致更大的平均检索长度),删除也极为方便。大部分数据结构教材用比较多的篇幅来讨论闭散列方法,是因为闭散列需要考虑的因素更多,因而更需要精心设计,闭散列在某些受限制的系统中(例如不能使用堆栈分配新空间)有独到的用途。并且,经过精心设计的闭散列的效率比开散列稳定。

----

摘录自:http://www.jpk.pku.edu.cn/pkujpk/course/sjjg/frame/index.html

 

转自 http://www.cnblogs.com/huangfox/archive/2012/07/06/2578898.html

<think>首先,用户的问题是:“我想查找C语言中散列函数的相关信息 请问C语言 散列函数”。用户正在询问关于C语言中的散列函数的信息。 从系统级指令中,我有一些引用:引用[1]和引用[2]。这些是关于散列表和散列函数的C语言相关内容。我需要在我的回答中引用这些,并在段落末尾添加引用标识,如[^1]。 关键点来自引用[1]: - 常见的散列函数包括: 1. 除留余数法:H(key) = key % p,其中p是小于或等于散列表长度m的质数。 2. 直接定址法:H(key) = key 或 H(key) = a*key + b,用于连续关键字分布。 3. 数字分析法:基于关键字的数字分布选择均匀部分。 4. 平方取中法:取关键字的平方的中间几位作为散列地址。 引用[2]提供了C语言代码示例,展示了散列表的实现,包括初始化、创建节点等。 我的回答需要遵循以下规则: - 所有行内数学表达式使用 $...$ 格式,例如 $x^2$。 - 独立公式使用 $$...$$ 格式并单独成段。 - 使用中文回答。 - 生成相关问题。 - 回答结构清晰,帮助用户逐步解决问题。 - 引用段落末尾添加引用标识,如[^1]。 所以,结构应该是: 1. 介绍散列函数的概念。 2. 解释常见的散列函数类型,使用数学表达式。 3. 提供C语言实现示例或伪代码。 4. 生成相关问题。 步骤: 1. **介绍散列函数:** 简要解释散列函数散列表中的作用,强调其在C语言中的应用。 2. **详细描述常见散列函数:** 基于引用[1],列出并解释每种方法,使用数学格式。 - 对于除留余数法:$H(key) = key \mod p$ - 直接定址法:$H(key) = key$ 或 $H(key) = a \times key + b$ - 数字分析法:描述但可能不需要数学表达式。 - 平方取中法:$H(key) = \text{middle bits of } key^2$ 3. **C语言实现:** 引用[2]有代码,我可以摘要或解释关键部分。例如,初始化散列表、创建节点。 4. **代码块:** 由于用户提到C语言,我应该包含一个简单的代码示例,比如实现除留余数法。 5. **生成相关问题:** 在最后,添加几个相关问题。 确保数学格式正确: - 行内:$...$ - 独立:$$...$$ 例如: - 除留余数法:$H(key) = key \mod p$ - 对于独立公式,如果必要,但可能不需要,因为大部分是行内。 在回答中,自然地引用来源。 最终回答结构: - 开头:解释散列函数。 - 主体:列出和解释散列函数,使用数学表达式和C语言见解。 - 代码:提供C语言代码片段。 - 结束:生成相关问题。 </think> ### C语言散列函数的相关信息 散列函数(Hash Function)是散列表(Hash Table)的核心组件,它通过将关键字(key)映射到固定范围的索引值,实现高效的数据存储和查找。在C语言中,散列函数常用于优化查找、插入和删除操作的时间复杂度,达到平均 $O(1)$ 的性能。散列函数的设计需尽量减少冲突(即不同关键字映射到相同索引),常见方法包括除留余数法、直接定址法、数字分析法和平方取中法。以下内容基于散列表的原理和C语言实现进行详细说明,并结合代码示例[^2]。 #### 常见散列函数类型 散列函数的选择取决于关键字的分布和存储需求。以下是四种主要方法,每种方法都使用数学表达式描述: 1. **除留余数法(Division Method)** 这是最常用的方法,适用于整数关键字。公式为: $$ H(key) = key \mod p $$ 其中,$p$ 是小于或等于散列表长度 $m$ 的最大质数。选择质数 $p$ 能有效减少冲突,因为质数能更好地分散余数分布。 - **C语言实现建议**:在代码中,$p$ 可通过预计算或运行时确定,例如使用素数表。 2. **直接定址法(Direct Addressing Method)** 此法简单高效,但要求关键字分布连续。公式为: $$ H(key) = key \quad \text{或} \quad H(key) = a \times key + b $$ 其中,$a$ 和 $b$ 是常数。如果关键字不连续(如稀疏数据),会导致存储空间浪费。 - **C语言适用场景**:适合数组索引或固定范围的关键字,例如学生ID。 3. **数字分析法(Digit Analysis Method)** 此法基于关键字的数字分布特征,选取均匀的位数作为索引。例如,对于十进制关键字,分析各位数字的出现频率,选择分布均匀的位段: $$ H(key) = \text{提取关键字中分布均匀的若干位} $$ 此方法需预先知道关键字集合,更换数据集需重新设计函数[^1]。 - **C语言实现提示**:可通过位操作(如 `&` 和 `>>`)提取特定位。 4. **平方取中法(Mid-square Method)** 通过取关键字平方值的中间几位作为散列地址,公式为: $$ H(key) = \text{middle_bits}(key^2) $$ 其中,`middle_bits` 表示平方值中间的若干位(位数取决于散列表大小)。此法能分散非均匀关键字,但计算开销较大。 - **C语言优化**:使用位移和掩码操作提取中间位,例如 `(key * key) >> shift & mask`。 散列表本质上是“空间换时间”的策略,合理设计散列函数能显著降低冲突概率。在C语言中,散列函数通常与拉链法(Chaining)结合处理冲突,即使用链表存储同一索引下的多个关键字。 #### C语言实现示例 以下是一个简单的C语言代码示例,展示散列表的初始化和散列函数(以除留余数法为例)的实现。代码基于引用[2]中的结构,进行简化说明: ```c #include <stdio.h> #include <stdlib.h> #define SIZE 128 // 散列表大小 typedef int K; // 关键字类型 typedef int V; // 值类型 // 散列表节点定义 typedef struct LNode { K key; V value; struct LNode *next; } Node; // 散列表结构 typedef struct HashTable { Node *table; // 数组存储头节点 } HashTable; // 初始化散列表 void init(HashTable *ht) { ht->table = (Node *)malloc(sizeof(Node) * SIZE); for (int i = 0; i < SIZE; i++) { ht->table[i].key = -1; // 头节点初始化为-1 ht->table[i].value = -1; ht->table[i].next = NULL; } } // 散列函数实现(除留余数法) int hashFunction(K key, int tableSize) { // 选择接近tableSize的质数p(此处简化,实际需动态计算质数) int p = 127; // 假设p为127(小于SIZE的质数) return key % p; // H(key) = key % p } // 插入函数(处理冲突使用拉链法) void insert(HashTable *ht, K key, V value) { int index = hashFunction(key, SIZE); // 计算索引 Node *newNode = (Node *)malloc(sizeof(Node)); newNode->key = key; newNode->value = value; newNode->next = NULL; // 如果头节点为空,直接插入 if (ht->table[index].next == NULL) { ht->table[index].next = newNode; } else { // 冲突处理:添加到链表尾部 Node *current = ht->table[index].next; while (current->next != NULL) { current = current->next; } current->next = newNode; } } int main() { HashTable ht; init(&ht); insert(&ht, 10, 100); // 示例插入 insert(&ht, 137, 200); // 可能冲突,但由拉链法处理 // 其他操作如查找可类似实现 return 0; } ``` **代码说明**: - `hashFunction` 函数实现了除留余数法,其中 `p` 需根据表大小选择质数(实际应用中应动态计算)。 - 冲突处理采用拉链法:每个索引位置存储一个链表,冲突元素追加到链表尾部。 - 此代码展示了散列表的基本框架,实际使用时可扩展查找和删除功能[^2]。 #### 设计散列函数的注意事项 在C语言中,设计散列函数时应考虑: - **冲突率**:测试不同数据集下的冲突次数,使用质数 $p$ 能改善分布。 - **性能**:简单函数(如除留余数法)计算高效,适合实时系统。 - **关键字类型**:非整数关键字(如字符串)需先转换为整数,例如通过字符串哈希算法。 - **内存管理**:C语言需手动分配和释放内存,避免内存泄漏(如代码中的 `malloc` 和 `free`)。 通过合理设计散列函数散列表在C语言中可应用于数据库索引、缓存系统和编译器符号表等场景,显著提升效率[^2]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值