论一个算法【哈希】

hash的概念

即通过一种计算方式,将每个值映射到一个唯一对应的键值。并以关键字在地址集中的“象”作为记录在表中的存储位置,这种表便称为哈希表,这一映象过程称为哈希造表或散列,所得存储位置称为哈希地址或散列地址

举个栗子:设哈希函数为: H ( K ) = K / 3 + 1 H(K)=K/3+1 H(K)=K/3+1,则构造关键字序列为 1、2、5、9、11、13、16、21、27 的哈希表(散列表)为:

不难发现。许多不同值的 H a s h Hash Hash键值相同,这时就要引入一个新的概念

冲突

理解起来并不难,即两个不同的关键字具有相同的存储位置,问题就在于如何解决,既然冲突无法避免的话,那又如何降低冲突的概率。在解决之前,我们还需知道冲突的可能性与哪些因素有关

  1. 装填因子:装填因子是指哈希表中己存入的元素个数 n n n与哈希表的大小 m m m 的比值,即 α = n / m α=n/m α=n/m α α α越小,发生冲突的可能性越小,反之,发生冲突的可能性就越大(很好理解吧)
  2. 所构造的哈希函数:构造的哈希函数不同,冲突的可能性也会不同,由此可见,一个好的 H a s h Hash Hash函数有多么重要

处理冲突的方法

1.开放地址法

开放地址就是,当新插入的记录所选地址已被占用时,便找另一个空的地址让它存储

(1)线性探测法

设散列函数 H ( K ) = H(K)= H(K)= K K K m o d mod mod m m m,若发生冲突,则沿着一个探查序列逐个探查(也就是加上一个增量)
优点:只要哈希表未被填满,保证能找到一个空地址单元存放有冲突的元素

缺点:可能使第 i i i个哈希地址的同义词存入第 i + 1 i+1 i+1个哈希地址,这样本应存入第 i + 1 i+1 i+1个哈希地址的元素变成了第 i + 2 i+2 i+2个哈希地址的同义词,因此,可能出现很多元素在相邻的哈希,地址上“堆积”起来,大大降低了查找效率

(2)二次探测法

H i = ( H ( k ) + d i ) Hi=(H(k)+di) Hi=(H(k)+di) m o d mod mod m m m其中 d i = 1 2 , − 1 2 , 2 2 , − 2 2 , … , j 2 , − j 2 ( j ≤ m / 2 ) di =1^2,-1^2,2^2,-2^2,…,j^2,-j^2(j≤m/2) di=12,12,22,22,,j2,j2(jm/2)

2.链地址法

将具有相同哈希地址的记录链成一个单链表, m m m个哈希地址就设 m m m个单链表,然后用一个数组将 m m m个单链表的表头指针存储起来,形成一个动态的结构

优点:插入、删除方便
缺点:占用存储空间多

举个栗子: 设 47 , 7 , 29 , 11 , 16 , 92 , 22 , 8 , 3 , 50 , 37 , 89 设{47,7,29,11,16,92,22,8,3,50,37,89} 47,7,29,11,16,92,22,8,3,50,37,89的哈希函数为: H a s h ( k e y ) = k e y Hash(key)=key Hash(key)=key m o d mod mod 11 11 11

那么,经过拉链法处理过后的数据应如下图

3.再哈希法

H i = R H i ( k e y ) Hi=RHi(key) Hi=RHi(key) i = 1 , 2 , 3 , … … , k i=1,2,3,……,k i=1,2,3,……,k

优点:不易产生“聚集”

缺点:增加了计算时间

4.建立一个公共溢出区(似乎没什么用,跳过)

H a s h Hash Hash函数的构造方法

1. 直接定址法

取关键字或关键字的某个线性函数值为散列地址,即 H ( K ) = K H(K)=K H(K)=K H ( K ) = a ∗ K + b H(K)=a * K+b H(K)=aK+b(其中 a , b a,b a,b为常数)

由名字就可以看出来该方法简单粗暴,所以一般不适用

2. 除后余数法

H ( K ) = K H(K)=K H(K)=K m o d mod mod p p p ( p ≤ m ) (p≤m) (pm)
ps: p一般选取质数,如131,1331,13331(不要问我为什么)

该方法看似简单,实则非常有效,而一个好的模数则会让事半功倍

3. 平方取中法

取关键字平方后的中间几位为哈希函数,因为中间几位与数据的每一位都相关
举个栗子:1314的平方是1726596,那么即可取265作为它的地址‬

无法评价,毕竟本人从未使用过,有兴趣的可以自己去尝试

4. 数字分析法

即选用关键字的某几位组合成哈希地址,选用的这些数字应具有很高的辨识度,比如说一串数的前四位大致相同,而后四位毫无关联的话,理应优先取后四位

也许等你分析完了,别人已经 A C AC AC了,无语,这方法

5.折叠法

是将关键字按要求的长度分成位数相等的几段,最后一段如不够长可以短些,然后把各段重叠在一起相加并去掉进位,以所得的和作为地址
举个栗子: 42751896 = 427 + 518 + 96 = 1041 42751896=427+518+96=1041 42751896=42751896=1041

代码有点难处理,不过还好

6.随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H ( k e y ) = r a n d o m ( k e y ) H(key)=random (key) H(key)=random(key)

总的来说,这六种方法各有千秋,考试时应视不同种情况采用不同的方法

我们需考虑的因素有如下几条:

  1. 计算哈希函数所需时间;
  2. 关键字的长度;
  3. 哈希表的大小;
  4. 关键字的分布情况;
  5. 记录的查找频率。

例题讲解

Crazy Search

题目大意:求一个字符串中长度为N的子串(不包含重复)的数量
思路:把每一个子串都 h a s h hash hash N C NC NC进制的整数 s s s,判断 h a s h [ s ] hash[s] hash[s]是否出现过,再进行统计

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e8 + 5;
bool vis[MAXN];
int n , m , ans;
char s[MAXN];
int main() {
	cin >> n >> m >> s;
	for (int i = 0 ; i <= strlen(s) - n ; i ++) {
		int hash = 0 , base = 1;
		for (int j = i ; j < i + n ; j ++) {
			hash += s[j] * base;
			base *= m;
		}
		if (!vis[hash]) {
			ans ++;
			vis[hash] = 1;
		}
	}
	printf("%d", ans);
} 

兔子与兔子

题目大意:判断两个等长区间里的字符串是否一致

思路:将一个字符串通过一种映射关系,转化为一个整数,通过整数对比来反映字符串关系

代码实现:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 5 , MOD = 13331;
int m , l1 , r1 , l2 , r2; 
int hash2[MAXN] , jc[MAXN] = {1};
char s[MAXN];
int main() {
   scanf("%s", s + 1);
   scanf("%d", &m);
   int n = strlen(s + 1);
   for (int i = 1 ; i <= n ; i ++) {
   	jc[i] = jc[i - 1] * 131;
   	hash2[i] = hash2[i - 1] * 131 + (s[i] - 'a');
   }
   for (int i = 1 ; i <= m ; i ++) {
   	scanf("%d %d %d %d", &l1 , &r1 , &l2 , &r2);
   	int k = hash2[r1] - hash2[l1 - 1] * jc[r1 - l1 + 1];
   	int k2 = hash2[r2] - hash2[l2 - 1] * jc[r2 - l2 + 1];
   	if (k == k2) puts("Yes");
   	else puts("No");
   }
} 

难题典例

Hash 键值

虽然题目名称是关于 H a s h Hash Hash的,但实际上与 H a s h Hash Hash半毛钱关系都没有

按照正常模拟,我们很容易写出30分的 T L E TLE TLE代码,但拿满分还需一个玄学优化

定义一个状态 s u m [ i ] [ j ] sum[i][j] sum[i][j]为所有% i i i j j j的数的总和,所以就可以写出如下代码:

for (int i = 1 ; i <= n ; i ++) {
   scanf("%d", &a[i]);
   for (int j = 1 ; j <= n ; j ++) {
    	sum[j][i % j] += a[i];
	}
}

不难发现时间复杂度为 O ( n 2 ) O(n^2) O(n2)白优化一场,所以这时我们分两类讨论就行了,当 j ∗ j j * j jj<=n时存在 s u m sum sum数组里,而大于的部分直接用循环枚举(因为 x > s q r t ( n ) x>sqrt(n) x>sqrt(n),所以不会超时),最后输出的时候也分两类情况输出,修改元素时则修改 s u m sum sum数组与原数组即可

奉上代码

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;
int n , q , a[MAXN] , hash[MAXN] , y , opt , x , sum[1005][MAXN];
int main() {
   freopen("hash.in" , "r" , stdin);
   freopen("hash.out" , "w" , stdout);
   scanf("%d %d", &n , &q);
   for (int i = 1 ; i <= n ; i ++) {
   	scanf("%d", &a[i]);
   	for (int j = 1 ; j * j <= n ; j ++) {
   		sum[j][i % j] += a[i];
   	}
   }
   for (int i = 1 ; i <= q ; i ++) {
   	scanf("%d %d %d", &opt , &x , &y);
   	if (opt == 1) {
   		if (x * x > n) {
   			int ans = 0;
   			for (int j = y ; j <= n ; j += x) {
   				ans += a[j];
   			}
   			printf("%d\n", ans);
   		} else {
   			printf("%d\n", sum[x][y]);
   		}
   	} else {
   		for (int j = 1 ; j * j <= n ; j ++) {
   			sum[j][x % j] -= a[x];
   			sum[j][x % j] += y;
   		}
   		a[x] = y;
   	}
   }
   return 0;
}
又双叒叕更完了一个知识点,蒟蒻的不易
posted @   Fracture_Dream  阅读(5)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示