【数据结构】哈希

引入

二叉搜索树的查找效率为O(log2N)到O(N),二叉平衡树的查找效率为O(log2N),那么有没有一种算法可以不经过任何比较,一次直接从表中得到要搜索的元素呢?事实上这种算法是存在的,就是哈希表


1、哈希的概念

元素的存储位置与它的关键码有一一映射的关系,在查找元素的时候不需要进行任何比较,可以直接从表中直接检索出元素的值。

1.1 哈希表的优点

(1)查找的效率高
(2)存在预缓存机制,提高查找的速度

哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

哈希函数设置:hash(key) = key % capacity; 其中 capacity为存储元素底层空间总的大小
举个栗子:
在这里插入图片描述
从上面的这个例子来看,当 key的值为19时,所得的哈希值为1,此时在1 的这个位置已经存在了数据,那么这样的问题就成为哈希冲突,下面我来介绍哈希冲突。


2、哈希冲突

不同关键字通过相同哈希数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

2.1 引起哈希冲突的原因

引起哈希冲突的主要原因是哈希函数设计的不够合理,才会导致出现多次哈希冲突,一般在设计哈希函数时,选取地址数m附近的一个质数p作为除数(一般是p<=m),就可以尽可能的避免哈希冲突。

设计哈希函数的原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

3、 常用的哈希函数

  1. 直接定制法
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
    优点:简单、均匀
    缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小且连续的情况

  2. 除留余数法
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函
    数:Hash(key) = key% p (p<=m),将关键码转换成哈希地址

以上两种哈希函数是最常用的,还有一些不常用的哈希函数,例如:平方取中法、数学分析法、折叠法、 随机数法。

哈希函数设计的越精妙,产生冲突的几率越小

4、 解决哈希冲突的方法

解决哈希冲突的方法有两种,分别为:开散列和闭散列

4.1 闭散列(开放定址法)

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

查找空位置的方法:
1.线性探测
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  • 插入元素
    通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
    在这里插入图片描述
    以上面这个例子来看,当key的序列值为19时,会产生哈希冲突,所以要寻找下一个空位置,即为3所对应的位置。

  • 删除元素
    在删除的时候会出现一定的问题,所以不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素1,如果直接删除掉,19查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

线性探测的实现

#pragma once
#include<map>
#include<vector>
using namespace std;

namespace tdd
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

class dealInt
{
public:
	int operator()(int n)
	{
		return n;
	}
};

class dealString
{
public:
	int operator()(const string & n)
	{
		int sum = 0;
		int seed = 131;

		for (const char & c : n)
		{
			sum = sum * seed + c;
		}

		return sum & 0x7FFFFFFF;
	}
};

template<class K, class V, class SW>
class hashTable
{
	struct elem
	{
		pair<K, V> m_val;
		State m_state;

		elem(const K & key = K(), const V & val = V(), State state = EMPTY) :
			m_val(key, val),
			m_state(state)
		{

		}
	};

	vector<elem> m_table;
	size_t m_size;
public:
	hashTable(size_t capacity = 11):
		m_table(capacity),
		m_size(0)
		{

		}

	size_t capacity()
	{
		return m_table.size();
	}

private:
	int hashFunc(const K & key)
	{
		SW func;
		return func(key) % capacity();
	}

public:
	bool insert(const pair<K, V> & val)
	{
		int n = hashFunc(val.first);

		while (m_table[n].m_state == EXIST)
		{
			if (m_table[n].m_val.first == val.first)
			{
				return false;
			}

			n++;
			if (n == capacity())
			{
				n = 0;
			}
		}
		m_table[n].m_val = val;
		m_table[n].m_state = EXIST;

		m_size++;
		return true;
	}

	int find(const  K & key)
	{
		int n = hashFunc(key);

		while (m_table[n].m_state != EMPTY)
		{
			if (m_table[n].m_state == EXIST && m_table[n].m_val.first == key)
			{
				return n;
			}
			n++;
			if (n == capacity())
			{
				n = 0;
			}
		}
		return -1;
	}
	bool erase(const K & key)
	{
		int ret = find(key);
		if (ret < 0)
		{
			return false;
		}
		else
		{
			m_table[ret].m_state = DELETE;
			m_size--;
		}
	}

	size_t Size()
	{
		return m_size;
	}

	bool Empty()
	{
		return m_size == 0;
	}

	void Swap(hashTable<K, V, SW> & ht)
	{
		m_table.swap(ht.m_table);
		size_t tmp;

		tmp = m_size;
		m_size = ht.m_size;
		ht.m_size = tmp;
	}
};

};

除留余数法一般选择质数作为除数,原因是为了降低哈希冲突的概率。

闭散列增容

在这里插入图片描述
线性探测的优点:实现简单
线性探测的缺点:一旦发生冲突,所有的冲突会连在一起,容易产生数据的“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

为了缓解这样的数据堆积,下面来看二次探测


2>二次探测
在线性探测中查找空位置是一个一个挨着找,而二次探测中的查找方法是:H (i) = ( H(0)+i^2 )% m,或者: H(i)= (H(0) - i ^2 )% m。其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。通俗的来讲就是先找右边的一个数据,再找左边的一个数据,例如先找1,再找-1,先找3,再找-3……

**注意:**当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5

由上述可知:二次探测的效率要比线性探测的效率高。


4.2 开散列(开链法)

首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

如下图所示:
在这里插入图片描述
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

开散列增容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

4.3 开散列与闭散列的比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上: 由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间

哈希查找是一种通过哈希函数将关键字映射到哈希表中的位置,然后在该位置进行查找的方法。在C语言中,我们可以使用数组来实现哈希表。 首先,需要定义一个哈希表的结构体,包含两个成员:数组和数组长度。数组的每个元素是一个链表的头节点,用于解决哈希冲突。 ```c #define MAX_SIZE 100 // 链表节点 typedef struct ListNode { int key; int value; struct ListNode* next; } ListNode; // 哈希表 typedef struct HashTable { ListNode* array[MAX_SIZE]; int size; } HashTable; ``` 然后,需要实现以下几个操作: 1. 哈希函数:将关键字映射为哈希表的位置。 ```c int hashFunction(int key, int size) { return key % size; } ``` 2. 初始化哈希表。 ```c void initHashTable(HashTable* hashTable, int size) { hashTable->size = size; for (int i = 0; i < size; i++) { hashTable->array[i] = NULL; } } ``` 3. 插入元素。 ```c void insert(HashTable* hashTable, int key, int value) { int index = hashFunction(key, hashTable->size); ListNode* newNode = (ListNode*)malloc(sizeof(ListNode)); newNode->key = key; newNode->value = value; newNode->next = NULL; if (hashTable->array[index] == NULL) { hashTable->array[index] = newNode; } else { ListNode* cur = hashTable->array[index]; while (cur->next != NULL) { cur = cur->next; } cur->next = newNode; } } ``` 4. 查找元素。 ```c int search(HashTable* hashTable, int key) { int index = hashFunction(key, hashTable->size); ListNode* cur = hashTable->array[index]; while (cur != NULL) { if (cur->key == key) { return cur->value; } cur = cur->next; } return -1; // 没有找到 } ``` 这样,我们就可以使用哈希查找来快速检索数据了。注意,在实际应用中,还需要处理哈希冲突的情况,例如使用链地址法解决冲突。另外,还需要实现删除操作等其他功能。以上只是一个简单的哈希查找的示例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值