前言
-🏀大家好,我是BXuan,热爱编程与篮球的软件工程大二学生一名
-📚近期在准备4月份的蓝桥省赛,本章与大家一起聊聊有关散列表的问题!如文有误,请大家指出并多多包涵。
-🏃放弃不难,但坚持一定很酷。
文章目录
📗知识点
- Hash 的概念
- 构造方法
- 冲突处理
📗概念
采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间即称为散列表,用数学关系理解,即是X与Y的映射关系,即Y=F(X)。
📗思考
-
散列技术仅仅是一种查找技术吗?
应该说,散列既是一种查找技术,也是一种存储技术。
-
散列是一种完整的存储结构吗?
散列只是通过记录的关键码定位该记录,没有完整地表达记录之间的逻辑关系,即通过关键码能推出 Key 值,但是通过关键码对应的值(即位置处的值)不能推出关键码,所以散列存储的关键码和值之间并不对称,因此散列主要是面向查找的存储结构。
📗散列表
一.散列表的缺陷
散列表并不是适用于所有的需求场景,那么哪些情况下不适合使用呢?
-
散列技术一般不适合在允许多个记录有同样关键码的情况下使用。
因为这种情况下,通常会有冲突存在,将会降低查找效率,体现不出散列表查找效率高的优点。
并且如果一定要在这个情况下使用的话,还需要想办法消除冲突,这将花费大量时间,那么就失去了 O(1) 时间复杂度的优势,所以在存在大量的冲突情况下,我们就要弃用散列表。 -
散列方法也不适用于范围查找,比如以下两个情况。
-
查找最大值或者最小值
因为散列表的值是类似函数的,映射函数一个变量只能对应一个值,不知道其他值,也不能查找最大值、最小值,RMQ(区间最值问题)可以采用 ST 算法、树状数组和线段树解决。
-
也不可能找到在某一范围内的记录
比如查找小于 N 的数有多少个,是不能实现的,原因也是映射函数一个变量只能对应一个值,不知道其他值。
二.散列技术的关键问题
在使用散列表的时候,我们有两个关键的技术问题需要解决:
- 散列函数的设计,如何设计一个简单、均匀、存储利用率高的散列函数?
- 冲突的处理,如何采取合适的处理冲突方法来解决冲突。
①如何设计实现散列函数
在构建散列函数时,我们需要秉持两个原则:
-
简单
- 散列函数不应该有很大的计算量,否则会降低查找效率。
-
均匀:
- 函数值要尽量均匀散布在地址空间,这样才能保证存储空间的有效利用并减少冲突。
🙇♂️散列函数实现的方法
1. 直接定址法
散列函数是关键码(Key)的映射的线性函数,形如:
H(key)=a∗key+b
eg:
如果关键码的集合已知且为 [11,22,33,66,88,44,99]
H(key)=11∗key+0
缺点:
- 我们是看到了这个集合,然后想到他们都是 11 的倍数才想到这 Hash 函数。我们在平常的使用中一般不会提前知道 Key 值集合,所以使用较少。
适用范围:
- 事先知道关键码,关键码集合不大且较为连续而不离散。
2.除留余数法
会发现产生了很多相同的 H(K),这就是发生冲突,因为一个位置只能放一个数,有两个值对应这里一个位置,是不可以的。
这种方法是最常用的方法,这个方法的关键在于如何选取 P,使得利用率较高并且冲突率较低,一般情况下,我们会选取最接近表长且小于等于表长的最大素数。
缺点:
- P 选取不当,会导致冲突率上升。
适用范围:
- 除留余数法是一种最简单、也是最常用的构造散列函数的方法,并且不要求事先知道关键码的分布。
3.数字分析法
比如我将我的集合全部转化为 16 进制数,根据关键码在各个位上的分布情况,选取分布比较均匀的若干位组成散列地址。或者将 N 位 10 进制数,观察各各位的数字分布,选取分布均匀的散列地址。
eg:
首先考虑一位作为散列函数,发现都是很多冲突,选取两位时,百位和十位组合最适宜,分布均匀且没有冲突。
当然,我们说的是这一方法的一个具体实列,既然叫做数字分析法,那么只有对于不同数据的不同分析,才能写出更是适配的 H(x)。
4.平方取中法
5.折叠法
②冲突的处理方法
🙋♀️开散列方法
open hashing 也称为拉链法,separate chaining 称为链地址法,简单来说,就是由关键码得到的散列地址一旦产生了冲突,就去寻找下一个空的散列地址,并将记录存入。
寻找下一个空的散列地址的方法:
1.线性探测法
当发生冲突时,从冲突位置的下一个位置起,依次寻找空的散列地址。
对于键值 key,设 H(key)=d,闭散列表的长度为 m,则发生冲突时,寻找下一个散列地址的公式为:
Hi=(H(key)+di) MOD m(di=1,2,…,m−1)
堆积现象:
在处理冲突的过程中出现的非同义词之间对同一个散列地址争夺的现象。
eg:
Key 集合为 47, 7, 29, 11, 27, 92, 22, 8, 3。
P 值为 11,进行 Hash 映射,采用线性探测法处理冲突。
2.二次探测法
即当发生冲突时,寻找下一个散列地址的公式为:
Hi=(H(key)+di)
其中(di=1,-1,2,-2,…,q,-q且q≤m/2)
3.随机探测法
当发生冲突时,下一个散列地址的位移量是一个随机数列,即寻找下一个散列地址的公式为:
Hi=(H(key)+round)
其中round为随机数
4.再 hash 法
注意:用开放定址法处理冲突得到的散列表叫闭散列表。
🙋♂️闭散列方法
closed hashing 也称为开地址方法,open addressing 开放地址法,开放地址法中涵盖了以下两种实现方式;
1.拉链法(链地址法)
将所有散列地址相同的记录即 Key 值相同的项目,坠成一个链表,每个链表的头指针存放位置为 Key 值对应的位置。
eg:
2.建立公共溢出区
散列表包含基本表和溢出表两部分(通常溢出表和基本表的大小相同),将发生冲突的记录存储在溢出表中。
查找时,如果在基本表里找的到就返回成功,没找到就在溢出区顺序查找,注意这里不再是映射而是顺序查找,放置时也是按照顺序的方式。
🚀真题巩固
一、弗里的的语言
问题描述
小发明家弗里想创造一种新的语言,众所周知,发明一门语言是非常困难的,首先你就要克服一个困难就是,有大量的单词需要处理,现在弗里求助你帮他写一款程序,判断是否出现重复的两个单词。
输入描述
第 1 行,输入 N,代表共计创造了多少个单词。
第 2 行至第 N+1 行,输入 N 个单词。
1 <= N <= 10^4,保证字符串的总输入量不超过 10^6 。
输出描述
输出仅一行。若有重复的单词,就输出重复单词,没有重复单词,就输出 NO,多个重复单词输出最先出现的。
运行限制
- 最大运行时间:1s
- 最大运行内存: 512M
输入输出示例
示例1
输入:
6
1fagas
dsafa32j
lkiuopybncv
hfgdjytr
cncxfg
sdhrest
输出:
NO
示例2
输入:
5
sdfggfds
fgsdhsdf
dsfhsdhr
sdfhdfh
sdfggfds
输出:
sdfggfds
代码示例1:
下列代码为系统所给答案,但在蓝桥自带系统上无法正常通过(有可能是系统的原因),逻辑结构没问题!
package E_lanqiao;
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
/**\
*
*@Description
*@author BXuan Email:ybxuan2002@163.com
*@version
*@date 2022年3月5日下午2:26:36
*
*/
public class Main {
// 1、需要先建立一个散列表和公共溢出区
static final int h = 12582917;
static String[] Value = new String[h];
static String[] upValue = new String[h];
static int upValueCount = 0;
// 2、定义哈希列表函数
private static int Hx(String s) {
int n = s.length();
int sum1=0;
for (int i = 0; i < n; i++){
sum1 = sum1 * 131%h + (s.charAt(i)-'a'+1)%h;
}
return (sum1+h)%h;
}
// 3、定义查询函数
private static boolean isAt(String s) {
int n = Hx(s);
if(Value[n] == null) {
return false;
}else if(Value[n].equals(s)) {
return true;
}else {
for(int i = 0;i < upValueCount;i++) {
if(upValue[n].equals(s)) {
return true;
}
return false;
}
}
return false;
}
// 4、定义插入查询散列表函数
private static boolean in(String s) {
// 进入Hx函数查询该字符串应插入的位置
int n=Hx(s);
if(Value[n]==null) {
Value[n]=s;
return true;
}
else if(Value[n].equals(s)) return false;
else {
for(int i=0;i<upValueCount;i++)
if(upValue[n].equals(s)) return false;
upValue[upValueCount++]=s;
return true;
}
}
// 5、主函数
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
int N = scan.nextInt();
boolean isRight = false;
String ans = "NO";
for(int i = 0;i < N;i++) {
String wordString = scan.next();
if(isRight) {
continue;
}
if(isAt(wordString)) {
isRight = true;
ans = wordString;
}else {
// 将字符串存入哈希表中
in(wordString);
}
}
System.out.println(ans);
scan.close();
}
}
代码示例2:
下列代码是运用java内置哈希表类进行实现,能正常运行并通过。
package E_lanqiao;
import java.util.HashSet;
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
/**
*
*@Description
*@author BXuan Email:ybxuan2002@163.com
*@version
*@date 2022年3月5日下午3:26:02
*
*/
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
HashSet<String> hashSet = new HashSet<String>();
int N = scan.nextInt();
scan.nextLine();// 吸收换行
int temp = N;
String ansString = "NO";
while(temp > 0) {
String string = scan.next();
if(!(hashSet.add(string))) {
ansString = string;
}
temp--;
}
if(hashSet.size() == N) {
System.out.println("NO");
}else {
System.out.println(ansString);
}
scan.close();
}
}
👏小结
本章节哈希表内容较多,其实现方法以及处理方法也较为复杂,需理清逻辑,慢慢思考,才能将其最好地掌握下来。冲冲冲!