[数据结构 - 第8章] 查找之哈希表概念

一、什么是哈希表?

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系 f,使得每个关键字 key 对应一个存储位置 f(key)。

我们把这种对应关系 f 称为哈希函数(也叫散列函数)。采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表(Hash table,也叫散列表)。关键字对应的记录存储位置我们称为散列地址

整个散列过程其实就是两步:

  • 插入元素时:根据待插入元素的关键码,用哈希函数计算出该元素的存储位置并按此位置进行存放;
  • 查找元素时:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则查找成功。

顺序查找的时间复杂度为 O(N) ,二分查找查找树的时间复杂度为 O(logN),而哈希表的时间复杂度为 O(1) 。不过这只是理想状态,实际并不那么完美。

二、哈希函数的构造方法

根据前人经验,统计出如下几种常用 hash 函数的构造方法:


1. 直接定址法

取关键字的某个线性函数值为散列地址,即:f(key) = a*key + b (a、b为常数)

这样的散列函数优点就是简单、均匀,也不会产生冲突。但问题是这需要事先知道关键字的分布情况适合查找表较小且连续的连续的情况。因此,在现实应用中,此方法虽然简单,但却并不常用


2. 数字分析法

抽取方法是使用关键字的一部分来计算散列存储位置的方法,通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。

比如我们现在要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是相同的。那么我们选择后面的四位成为散列地址就是不错的选择。如果这样的抽取工作还是容易出现冲突问题,还可以对抽取出来的数字再进行反转(如 1234 改成 4321)、右环位移(如 1234 改成 4123)等方法。总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各位置。


3. 平方取中法

关键字平方后再抽取中间几位。比较适合于不知道关键字的分布,而位数又不是很大的情况。

比如关键字是 4321,那么它的平方就是 18671041,抽取中间的 3 位就可以是 671,也可以是 710,用做散列地址。


4. 折叠法

将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址

比如我们的关键字是 9876543210,散列表表长为三位,我们将它分为四组,987|654|321|0,然后将它们叠加求和 987+654+321+0=1962,再求后 3 位得到散列地址为 962。

有时可能这还不能够保证分布均匀,不妨从一端向另一端来回折叠后对齐相加。比如我们将 987 和 321 反转,再与 654 和 0 相加,变成 789+654+123+0=1566,此时散列地址为 566。

折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况


5. 除留余数法

对于散列表长为m的散列函数公式为:f(key)=key mod p(p≤m)

该方法不仅可以对关键字直接取模,也可在折叠、平方取中后再取模

本方法的关键就在于选择合适的pp如果选得不好,就可能会容易产生同义词

根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数


6. 随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key)=random(key)。这里 random 是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的

那如果关键字是字符串如何处理?其实无论是英文字符,还是中文字符,也包括各种各样的符号,它们都可以转化为某种数字来对待,比如 ASCII 码或者 Unicode 码等,因此也就可以使用上面的这些方法。


总结

总之,现实中,应该视不同的情况采用不同的散列函数。我们只能给出一些考虑的因素来提供参考:1.计算散列地址所需的时间。 2.关键字的长度。 3.散列表的大小。 4.关键字的分布情况。 5.记录查找的频率。综合这些因素,才能决策选择哪种散列函数更合适。


三、处理哈希冲突的方法

我们时常会碰到两个关键字key1≠key2,但是却有f(key1)=f(key2),这种现象我们称为哈希冲突(collision),并把key1和key2称为这个哈希函数的同义词


1. 开放定址法

所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

它的公式是:fi(key)=(f(key)+di) MOD m(di=1,2,3,......,m-1)

比如说,我们的关键字集合为 {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表长为 12。我们用散列函数f(key)=key mod 12


2. 再散列函数法

对于我们的散列表来说,我们可以事先准备多个散列函数:fi(key)=RHi(key)(i=1,2,...,k)

这里 RHi 就是不同的散列函数,每当发生散列地址冲突时,就换一个散列函数计算,相信总会有一个可以把冲突解决掉。这种方法能够使得关键字不产生聚集,当然,相应地也增加了计算的时间。


3. 链地址法

将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。

对于关键字集合 {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},我们用前面同样的 12 为除数,进行除留余数法,可得到如下图结构,此时,已经不存在什么冲突换址问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题

dataStruct_chap8_hashTable_A.png


链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链表的性能损耗


4. 公共溢出区法

为所有冲突的关键字建立了一个公共的溢出区来存放。

在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表去进行顺序查找。如果对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。


四、哈希表查找性能分析

如果没有冲突,哈希表查找是所有查找中效率最高的,因为它的时间复杂度为O(1)。可惜,冲突是无法避免的。哈希表查找的平均长度取决于以下因素:

1、哈希表是否均匀

哈希函数的好坏直接影响着出现冲突的频繁程度,不过,由于不同的哈希函数对同一组随机的关键字,产生冲突的可能性是相同的,因此我们可以不考虑它对平均查找长度的影响

2、处理冲突的方法

相同的关键字、相同的哈希函数,但处理冲突的方法不同,会使得平均查找长度不同。比如线性探测处理冲突可能会产生堆积,显然就没有二次探测法好,而链地址法处理冲突不会产生任何堆积,因而具有更佳的平均查找性能。

3、哈希表的装填因子

装填因子 α =填入表中的记录个数/散列表长度。α 标志着哈希表的装满的程度。当填入表中的记录越多,α 就越大,产生冲突的可能性就越大。故哈希表的平均查找长度取决于装填因子,而不是取决于查找集合中的记录个数

不管记录个数 n 有多大,我们总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围之内此时我们散列查找的时间复杂度就真的是 O(1) 了。通常我们都将哈希表的空间设置得比查找集合大,此时虽然是浪费了一定的空间,但换来的是查找效率的大大提升。


参考:

《大话数据结构 - 第8章》 查找


转载于:https://www.cnblogs.com/linuxAndMcu/p/11558619.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 以下是使用C语言实现的顺序查找代码: ```c int sequentialSearch(int arr[], int n, int x) { for(int i = 0; i < n; i++) { if(arr[i] == x) { return i; } } return -1; } ``` 其中 `arr` 是要查找的数组,`n` 是数组的大小,`x` 是要查找的元素。这个函数会在数组中查找元素 `x`,如果找到了则返回该元素在数组中的下标,否则返回 -1。 ### 回答2: 顺序查找,也叫线性查找,是一种基本的查找,适用于无序或有序数据集合。其原理是从数据集合的第一个元素开始逐个比较,直到找到目标元素或遍历完整个集合。 以下是一个简单的数据结构顺序查找的代码示例: ```python def sequential_search(array, target): for i in range(len(array)): if array[i] == target: return i # 返回目标元素在数组中的位置索引 return -1 # 如果目标元素不存在于数组中,返回-1 # 测试示例 array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] target = 6 result = sequential_search(array, target) if result != -1: print("目标元素在数组中的位置是:%d" % result) else: print("目标元素不存在于数组中") ``` 在上述代码中,我们定义了一个名为`sequential_search`的函数,接受两个参数:`array`表示待查找的数组,`target`表示目标元素。函数通过`for`循环遍历数组,逐个比较元素与目标元素是否相等,若相等则返回该元素在数组中的位置索引;如果遍历完整个数组都没有找到目标元素,则返回-1表示目标元素不存在于数组中。最后,我们通过调用`sequential_search`函数并传入测试数据,判断目标元素是否在数组中,并打印相应的结果。 顺序查找的时间复杂度为O(n),其中n为数据集合中的元素数量。这是因为无论目标元素是第一个还是最后一个,都需要遍历整个数组才能确定。因此,在大规模数据集合中,顺序查找的效率较低,可以考虑其他更高效的查找,如二分查找哈希表等。 ### 回答3: 数据结构中的顺序查找是一种简单直观的查找,适用于无序的线性表结构。其基本思想是从头到尾依次遍历待查询的线性表,逐个与目标元素进行比较,直到找到目标元素或遍历结束。 顺序查找的代码实现如下: ```python def sequential_search(arr, target): for i in range(len(arr)): if arr[i] == target: return i # 返回目标元素的索引 return -1 # 如果未找到目标元素,则返回-1 # 示例调用 arr = [2, 5, 7, 9, 1, 3, 6, 8, 4] target = 6 index = sequential_search(arr, target) if index != -1: print("目标元素在数组中的索引为:", index) else: print("目标元素不在数组中") ``` 以上代码首先定义了一个`sequential_search`函数,该函数接受一个待查询的线性表`arr`和目标元素`target`作为参数。函数内部通过`for`循环依次遍历`arr`,并与`target`进行比较。如果找到目标元素,就返回该元素在数组中的索引;如果遍历完整个数组仍未找到目标元素,则返回-1表示目标元素不在数组中。 在示例调用中,我们定义了一个无序的整数数组`arr`,并设定目标元素为6。调用`sequential_search`函数进行查找,并根据返回结果判断是否找到目标元素。最后根据返回的索引值或者-1输出结果。 顺序查找虽然简单,但是时间复杂度较高,为O(n),其中n为线性表的长度。因此在大规模数据的查询中,效率相对较低,对于有序的线性表,使用二分查找等更高效的算会更合适。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值