散列表 哈希表 HaskTable

本文深入探讨了散列表的工作原理,包括如何通过散列函数将键值映射为数组下标,以及散列冲突的解决方法,如开放寻址法和链表法。此外,还讨论了工业级散列表的设计考量,以及散列表与链表结合使用实现LRU缓存的实例。

散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。

我们通过散列函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们用同样的散列函数,将键值转化数组下标,从对应的数组下标的位置取数据。散列函数,顾名思义,它是一个函数。我们可以把它定义成 hash(key),其中 key 表示元素的键值,hash(key) 的值表示经过散列函数计算得到的散列值。

在这里插入图片描述散列函数设计的基本要求:
1.散列函数计算得到的散列值是一个非负整数;
2.如果 key1 = key2,那 hash(key1) == hash(key2);
3.如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。

一个不同的 key 对应的散列值都不一样的散列函数,几乎是不可能的,无法完全避免这种冲突,这种冲突称之为散列冲突

我们常用的散列冲突解决方法有两类,开放寻址法(open addressing)和链表法(chaining)。
对于开放寻址冲突解决方法,有线性探测法(Linear Probing),二次探测(Quadratic probing)和双重散列(Double hashing)。

开放寻址法,数组内存空间连续,有效利用CPU缓存;结构简单,序列化起来简单等优点。删除数据比较麻烦;比链表冲突代价更高;比链表更浪费内存空间等缺点。

开放寻址法插入
开放寻址法插入开放寻址法查找
开放寻址法查找开放寻址法删除
开放寻址法删除链表法,内存利用率高;对大装载因子的容忍度更高;不适用于较小的对象的存储,消耗更多内存;对CPU缓存不友好。

链表法插入
链表法插入当数据量比较小、装载因子小的时候,适合采用开放寻址法。基于链表的散列冲突处理方法比较适合存储大对象、大数据量的散列表,而且,比起开放寻址法,它更加灵活,支持更多的优化策略,比如用红黑树代替链表。

工业级的散列表特性:
1.支持快速的查询、插入、删除操作;
2.内存占用合理,不能浪费过多的内存空间;
3.性能稳定,极端情况下,散列表的性能也不会退化到无法接受的情况

三个方面考虑设计工业级散列表的思路:
1.设计一个合适的散列函数。我们要尽可能让散列后的值随机且均匀分布,这样会尽可能地减少散列冲突,即便冲突之后,分配到每个槽内的数据也比较均匀。除此之外,散列函数的设计也不能太复杂,太复杂就会太耗时间,也会影响散列表的性能。
2.定义装载因子阈值,并且设计动态扩容策略。
3.选择合适的散列冲突解决方案。对比了开放寻址法和链表法两种方法的优劣和适应的场景。大部分情况下,链表法更加普适。而且,我们还可以通过将链表法中的链表改造成其他动态查找数据结构,比如红黑树,来避免散列表时间复杂度退化成 O(n),抵御散列碰撞攻击。但是,对于小规模数据、装载因子不高的散列表,比较适合用开放寻址法。

散列表和链表(跳表)的结合使用。散列表用数组实现,可以通过下标随机访问;而通过散列函数之后数据被打乱不再有序。链表(跳表)可以使数据有序。所以两者结合使用可以发挥各自优势实现一些功能。

散列表和链表结合使用实现LRU:

package lru_cache

const (
	hostbit = uint64(^uint(0)) == ^uint64(0)
	LENGTH  = 100
)

type lruNode struct {
	prev *lruNode
	next *lruNode

	key   int // lru key
	value int // lru value

	hnext *lruNode // 拉链
}

type LRUCache struct {
	node []lruNode // hash list

	head *lruNode // lru head node
	tail *lruNode // lru tail node

	capacity int //
	used     int //
}

func Constructor(capacity int) LRUCache {
	return LRUCache{
		node:     make([]lruNode, LENGTH),
		head:     nil,
		tail:     nil,
		capacity: capacity,
		used:     0,
	}
}

func (this *LRUCache) Get(key int) int {
	if this.tail == nil {
		return -1
	}

	if tmp := this.searchNode(key); tmp != nil {
		this.moveToTail(tmp)
		return tmp.value
	}
	return -1
}

func (this *LRUCache) Put(key int, value int) {
	// 1. 首次插入数据
	// 2. 插入数据不在 LRU 中
	// 3. 插入数据在 LRU 中
	// 4. 插入数据不在 LRU 中, 并且 LRU 已满

	if tmp := this.searchNode(key); tmp != nil {
		tmp.value = value
		this.moveToTail(tmp)
		return
	}
	this.addNode(key, value)

	if this.used > this.capacity {
		this.delNode()
	}
}

func (this *LRUCache) addNode(key int, value int) {
	newNode := &lruNode{
		key:   key,
		value: value,
	}

	//列表中的拉链的处理
	tmp := &this.node[hash(key)]
	newNode.hnext = tmp.hnext
	tmp.hnext = newNode
	this.used++
	
	//链表的处理
	if this.tail == nil {
		this.tail, this.head = newNode, newNode
		return
	}
	this.tail.next = newNode
	newNode.prev = this.tail
	this.tail = newNode
}

func (this *LRUCache) delNode() {
	if this.head == nil {
		return
	}
	prev := &this.node[hash(this.head.key)]
	tmp := prev.hnext

	//散列表拉链中查找
	for tmp != nil && tmp.key != this.head.key {
		prev = tmp
		tmp = tmp.hnext
	}

	//散列表拉链删除tmp节点的处理
	if tmp == nil {
		return
	}
	prev.hnext = tmp.hnext
	
	//链表的处理
	this.head = this.head.next
	this.head.prev = nil
	this.used--
}

func (this *LRUCache) searchNode(key int) *lruNode {
	if this.tail == nil {
		return nil
	}

	// 查找
	tmp := this.node[hash(key)].hnext
	for tmp != nil {
		if tmp.key == key {
			return tmp
		}
		tmp = tmp.hnext
	}
	return nil
}

func (this *LRUCache) moveToTail(node *lruNode) {
	//要移动到链表尾部的节点已经在尾部了
	if this.tail == node {
		return
	}
	
	//要移动到链表尾部的节点在头部
	if this.head == node {
		this.head = node.next
		this.head.prev = nil
	} else {
		node.next.prev = node.prev
		node.prev.next = node.next
	}

	node.next = nil
	this.tail.next = node
	node.prev = this.tail

	this.tail = node
}

func hash(key int) int {
	if hostbit {
		return (key ^ (key >> 32)) & (LENGTH - 1)
	}
	return (key ^ (key >> 16)) & (LENGTH - 1)
}

以上内容摘自《数据结构与算法之美》课程,来学习更多精彩内容吧。

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值