数据结构期末复习——哈希表

哈希表

哈希表通过哈希函数为各个值重新分配物理索引,哈希函数通常与取模密切相关。例如,给定哈希函数 i n d e x = k e y   m o d   m a x _ s i z e index= key \space mod\space max\_size index=key mod max_size,其中 m a x _ s i z e max\_size max_size为哈希表的存储容量。

假若容量为15,则哈希函数为 f ( x ) = x   m o d   15 f(x)=x \space mod \space 15 f(x)=x mod 15,插入 k e y = 3 , v a l = 5 key=3,val=5 key=3,val=5 k e y = 15 , v a l = 1 key=15,val=1 key=15,val=1,则得到 H a s h t a b l e [ 3 ] = 3 , H a s h t a b l e [ 18 ] = H a s h t a b l e [ 0 ] = 1 Hashtable[3]=3,Hashtable[18]=Hashtable[0]=1 Hashtable[3]=3,Hashtable[18]=Hashtable[0]=1,且 v a l u e [ 3 ] = 5 , v a l u e [ 18 ] = 1 value[3]=5,value[18]=1 value[3]=5,value[18]=1.

使用哈希表实际上是对数据的离散化,由此数据范围很大,但数据个数不多时可以不用开数据范围那么大的数组,可以用很小的存储空间存储,并且随机访问效率接近于数组,优于链表。

哈希冲突

接上文,如果又出现了 k e y = 18 , v a l = 4 key=18,val=4 key=18,val=4,由于 k e y   m o d   15 = 3 key \space mod \space 15=3 key mod 15=3,而 H a s h t a b l e [ 3 ] Hashtable[3] Hashtable[3]已经被占用了,故出现哈希冲突,此时需要其他方法安顿 ( 18 , 4 ) (18,4) (18,4).

解决哈希冲突有开散列法和闭散列法两种方法。

开散列法

将每个存储槽展开为单链表。例如,现在 H a s h t a b l e [ 3 ] Hashtable[3] Hashtable[3]是一个链表,它的头节点存储了索引3,当 ( 18 , 4 ) (18,4) (18,4)遇到已经有值的 H a s h t a b l e [ 3 ] Hashtable[3] Hashtable[3]头结点时,其沿着链表向下访问,遇到空的链表节点就安顿下来。访问时,键入 18 18 18 18   m o d   15 = 3 18 \space mod \space 15 = 3 18 mod 15=3沿着 H a s h t a b l e [ 3 ] Hashtable[3] Hashtable[3],发现头结点是3,非18, 故继续向下,访问到第二个为18,则找到值。

开散列法随机访问的时间复杂度为 O ( D ) O(D) O(D),其中 D D D为各个存储槽的最长链表长度。当数据较为极端时,开散列法将可能退化为链表,访问复杂度达到 O ( N ) O(N) O(N).

闭散列法

线性探查

与开散列法不同,闭散列法不使用链表,而是通过冲突解决策略寻找其他空槽。最简单的闭散列法是线性探查法。即当前槽被占用时,沿着散列数组向后继续找,找到一个空槽就马上停下并存储。(当找到边界了还没找到,就从数组开头开始继续找)。线性探查的操作过程保证其会访问数组中所有可能的空槽。

简单的线性探查哈希表实现

#include <bits/stdc++.h>
#define int long long
using namespace std;
struct Hash
{
	private:
	int size;
	int* table; // 计算结果
	int* value; // 值

	public:
	Hash( int sz = 6151 ) { 
		size = sz;
		table = new int[(size << 2) + 3];
		value = new int[(size << 2) + 3];
		for(int i = 0; i < size; ++i) table[i] = 0;
	}
    ~Hash(){ delete[] table; delete[] value; }

	void set(int key, int val){ // 修改值
		int p = key % size;
		int cnt = 0;
		while(table[p] != key && cnt < size) p = (p + 1) % size, cnt++;
		if(cnt == size) return;
		value[p] = val;
	}

	void add(int key, int val){ // 新建
		// 线性探查
		int p = key % size;
		int cnt = 0;
		while(table[p] && cnt < size) p = (p + 1) % size, cnt++;
		if(cnt == size) return;
		table[p] = key, value[p] = val;
	}

	const int& operator[](int key) const{ // 通过重载[]实现随机访问
		int p = key % size;
		int cnt = 0;
		while(table[p] != key && cnt < size) p = (p + 1) % size, cnt++;
		if(cnt == size){
			cout << "Illegal Index!" << endl;
		}
		return value[p];
	}
};

signed main()
{
    // 主函数用于测试
	Hash t;
	t.add(1, 5);
	t.add((int)1e9 + 7, 123);
	cout << t[1] << endl;
	cout << t[(int)1e9 + 7] << endl;
	return 0;
}

二次探查

显然,在冲突次数增多后,尤其是在同一个槽发生的冲突增多后,线性探查的探查时间会比较长,甚至可能比闭散列还要差,需要设计更好的冲突解决策略。二次探查函数规定,遇到冲突后,指针每次偏移 c 1 i 2 + c 2 i + c 3 c_1i^2+c_2i+c_3 c1i2+c2i+c3,其中 i i i是探查次数。二次探查可以避免线性探查导致的关键码聚集。

二次探查的缺陷是,它或许并不能像线性探查那样访问每一个可能的空槽,因此可能在哈希表还有空间未用时就插入失败了。

随机探查

除了二次探查,还可以通过生成随机的排列来进行伪随机探查。设有随机序列 p p p,则探查时每次指针偏移 p [ i ] p[i] p[i].

双散列探查

继续考虑以上两种探查方案。由于每个槽的探查点由探查函数确定好了,而二次探查函数和伪随机探查序列都可能在某个槽“聚集”,产生类似于线性探查的较差结果。为了避免其发生,可以再设置一个散列函数 h h h h [ i ] h[i] h[i]的值可以由随机序列产生,也可以由其他方法产生。最后 p = i ∗ h ( k ) p=i*h(k) p=ih(k),各个关键码之间就很难再共享一个探查序列了。

哈希表大小的设置

由于哈希函数多为取模函数,如果给一个随机序列,其间隔为1,那么经过哈希函数的映射序列间隔也为1。但是是用哈希表多是为了离散化,间隔为1的情况不多。

若序列之间的间隔为哈希表长度的因子大小,则将有机会产生冲突。

故将哈希表的大小设置为素数,可以保证哈希表的因子最少,最不容易产生关键码集群或哈希冲突。通常会取一个素数 N N N,然后设哈希表大小为 4 N + 3 4N+3 4N+3.

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值