数据结构与算法:8.哈希表

一、何为哈希表

哈希表是一种使用哈希函数组织数据,以支持快速插入搜索的数据结构。有两种不同类型的哈希表:哈希集合和哈希映射:

  • 哈希集合是集合数据结构的实现之一,用于存储非重复值。(C++ unordered_set)
  • 哈希映射是映射数据结构的实现之一,用于存储(key, value)键值对。(C++ unordered_map)

在标准模板库的帮助下,哈希表是易于使用的。大多数常见语言(如Java,C ++ 和 Python)都支持哈希集合和哈希映射。通过选择合适的哈希函数,哈希表可以在插入和搜索方面实现出色的性能。

二、哈希表原理

哈希表的关键思想使用哈希函数将键映射到存储桶。更确切地说,

  1. 当我们插入一个新的键时,哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;
  2. 当我们想要搜索一个键时,哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。

桶的实现主要有一下几个结构:

  • 数组
    查找O(N),插入和删除最坏也是O(N),因为插入和删除可能需要前后移动
  • 链表
    查找O(N),插入和删除是O(1)
  • 二叉搜索树BTS
    查找、删除、插入O(logN),主要缺点是可能退化成链表
  • 高度平衡的二叉搜索树:AVL树

三、哈希操作

  1. 插入

哈希函数将决定该键应该分配到哪个桶中,并将该键存储在相应的桶中;插入操作需要注意存在冲突的情况。

  1. 搜索

哈希表将使用相同的哈希函数来查找对应的桶,并只在特定的桶中进行搜索。

  1. 删除

首先搜索元素,然后在元素存在的情况下从相应位置移除元素。

  1. 哈希表插入、搜索、删除操作示例
    在这里插入图片描述
    在示例中,我们使用 y = x%5 作为哈希函数,让我们使用这个例子来完成插入和搜索策略:
  • 插入:通过哈希函数解析得到键值,此处的键值就是桶的索引,就可以知道key要放入哪个桶中。
    例如,1987 分配给桶 2,而 24 分配给桶 4

  • 搜索:通过相同的哈希函数解析键值,并仅在特定存储桶中搜索
    (1)如果搜索 1987,我们将使用相同的哈希函数将1987 映射到 2。因此我们在桶 2 中搜索,我们在那个桶中成功找到了 1987。
    (2)如果搜索 23,将映射 23 到 3,并在桶 3 中搜索。我们发现 23 不在桶 3 中,这意味着 23 不在哈希表中。

  • 删除:通过哈希函数得到键值,在对应的桶中查找对应的元素,如果有就删除,没有就什么也不做。

在设计哈希表时,需要注意2个最重要的基本因素:哈希函数和哈希冲突

四、哈希函数

哈希函数是哈希表中最重要的组件,该哈希表用于将键映射到特定的桶,常见的哈希函数是hashkey = key%N,其中key是键值,N是桶的数量,hashkey是桶的索引。在使用取余作为哈希函数时,取质数作为N是一个明智的选择,可以减少潜在的碰撞。

哈希函数将取决于键值的范围:key和桶的数量:N,常见的哈希函数有以下几种示例:在这里插入图片描述

五、哈希冲突

如果设计的哈希函数是完美的一对一映射,我们将不需要处理冲突,不幸的是在大多数情况下,冲突几乎是不可避免的。例如,在我们之前的哈希函数(y = x%5)中,1987和2都分配给了桶 2,这是一个冲突。

冲突解决算法应该解决以下几个问题
(1)如何组织在同一个桶中的值?
(2)如果为同一个桶分配了太多的值,该怎么办?
(3)如何在特定的桶中搜索目标值?

解决冲突有一下几种方法:
1.开放地址方法
哈希表中的空闲单元(即为a)既可以被哈希地址为a的关键字使用,也可以被发生冲突的其他关键字使用。谁先找到这个单元谁先占用。每当有碰撞, 则根据我们探查的策略找到一个空的槽为止,开放地址法主要有以下几种:

(1)线性探测
按顺序决定哈希值时,如果某数据的哈希值已经存在,则在原来哈希值的基础上往后加一个单位,直至不发生哈希冲突。

(2)再平方探测
按顺序决定哈希值时,如果某数据的哈希值已经存在,则在原来哈希值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。

(3)伪随机探测
按顺序决定哈希值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来哈希值的基础上加上随机数,直至不发生哈希冲突。

2.链式地址法
对于相同的散列值,我们将它们放到一个桶中,每个桶是相互独立的,链式地址法的桶通常设计为链表的形式,主要有下面两种方法:

(1)单链表桶
单链表的删除和增加操作都是O(1),但是查找效率最坏是O(N)

(2)二叉搜索树桶
可以利用搜索二叉树(BST)作为桶的设计,这样查找的效率是O(logN)。

(3)平衡二叉树(ALV)桶

3.再散列法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。

六、哈希表性能

哈希表的特性决定了其高效的性能,大多数情况下查找或者插入元素的时间复杂度可以达到O(1), 时间主要花在计算hash值上, 然而也有一些极端的情况,最坏的就是hash值全都映射在同一个地址上,这样哈希表就会退化成链表,例如下面的图片:
在这里插入图片描述
当hash表变成图2的情况时,时间复杂度会变为O(n),效率瞬间低下,所以,设计一个好的哈希表尤其重要,如HashMap在jdk1.8后引入的红黑树结构就很好的解决了这种情况。

内置哈希表的原理

内置哈希表的典型设计是

  1. 键值可以是任何可哈希化的类型。并且属于可哈希类型的值将具有哈希码。此哈希码将用于映射函数以获取存储区索引。
  2. 每个桶包含一个数组,用于在初始时将所有值存储在同一个桶中。
  3. 如果在同一个桶中有太多的值,这些值将被保留在一个高度平衡的二叉树搜索树中

插入和搜索的平均时间复杂度仍为 O(1)。最坏情况下插入和搜索的时间复杂度是 O(logN),使用高度平衡的BST。这是在插入和搜索之间的一种权衡。

七、简单哈希表实现(链式桶)

1.哈希集合

class Bucket{
public:
	Bucket(){}
	void insert(int key){
		if (bucket.end() == find(bucket.begin(), bucket.end(), key)){
			bucket.push_front(key);
		}
	}
	void remove(int key){
		bucket.remove(key);
	}
	bool isExist(int key){
		if (bucket.end() != find(bucket.begin(), bucket.end(), key)){
			return true;
		} else {
			return false;
		}
	}
private:
	list<int> bucket;
};

const int N = 5001;
class MyHashSet {
public:
	/** Initialize your data structure here. */
   	MyHashSet() {}
   	int hash(int key){
		return key % N;
	}
	void add(int key) {
		int hashValue = hash(key);
		buckets[hashValue].insert(key);
	}

	void remove(int key) {
		int hashValue = hash(key);
		buckets[hashValue].remove(key);
	}

	/** Returns true if this set contains the specified element */
	bool contains(int key) {
		int hashValue = hash(key);
		return buckets[hashValue].isExist(key);
	}
private:
	Bucket buckets[N];
};

2.哈希映射

class Bucket {
public:
	Bucket(){}
	void put(int key, int value) {
		auto beg = bucket.begin();
		auto end = bucket.end();
		for (; beg != end; beg++) {
			/* 键对应的值已存在,更新 */
			if (beg->first == key) {
				beg->second = value;
                return;
			}
		}
		/* 在桶中没有找到对应的key,插入 */
		if (beg == end) {
			bucket.push_front({ key,value });
		}
	}
	int get(int key) {
		auto beg = bucket.begin();
		auto end = bucket.end();
		for (; beg != end; beg++) {
			/* 返回给定的键所对应的值 */
			if (beg->first == key) {
				return beg->second;
			}
		}
		/* 映射中不包含这个键,返回-1 */
		return -1;
	}
	void remove(int key) {
        bucket.remove_if([key](pair<int,int> p) {
            return key == p.first;
        });
	}
private:
	/* 桶的基本数据结构 */
	list<pair<int,int>> bucket;
};

const int N = 10000;
class MyHashMap {
public:
	/** Initialize your data structure here. */
	MyHashMap() {}
	int hash(int key) {
		return key % N;
	}
	/** 向哈希映射中插入(键,值)的数值对,如果键对应的值已经存在,更新这个值. */
	void put(int key, int value) {
		int hashKey = hash(key);
		buckets[hashKey].put(key, value);
	}
	/** 返回给定的键所对应的值,如果映射中不包含这个键,返回-1 */
	int get(int key) {
		int hashKey = hash(key);
		return buckets[hashKey].get(key);
	}
	/** 如果映射中存在这个键,删除这个数值对 */
	void remove(int key) {
		int hashKey = hash(key);
		buckets[hashKey].remove(key);
	}
private:
	Bucket buckets[N];
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值