IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 基于CRC64的通用哈希表(HashMap)的实现(使用golang) -> 正文阅读

[数据结构与算法]基于CRC64的通用哈希表(HashMap)的实现(使用golang)

简介

学过数据结构的同学可能知道,哈希(hash)是数据查询的最快方式。具有 O ( 1 ) O(1) O(1)的时间复杂度。
原理如下:

在这里插入图片描述
首先需要定义一个哈希函数(又称散列函数),它的功能是把我们要存的数据x, 映射到数组上的某一个位置y。例如:

y = x ? % ? 19 y = x\ \%\ 19 y=x?%?19

这个时候会有一个问题:会有多个x映射到同一个y。假设 x 1 = 7 , x 2 = 19 x_1 = 7, x_2 = 19 x1?=7,x2?=19, 那么它们经过上述映射后都会放在数组下标7的位置
x 1 ? % ? 19 = 7 x 2 ? % ? 19 = 7 x_1\ \%\ 19 = 7 \\ x_2\ \%\ 19 = 7 x1??%?19=7x2??%?19=7

这时就会产生冲突。解决冲突的办法有:

  • 开地址法:
    • 线性探测
    • 二次探测
    • 再哈希
  • 链地址法

已有的博文对相关知识的介绍已经清晰易懂,本文不再赘述。但大多重在理论,而轻实践,示例代码多是教学性质的,无法实际用于生产环境。经过本人的观察,具有以下问题:

  • 只支持单一的数据类型。实际中,我们不可能为每种类型都实现一个hashmap,
  • 使用非常简单的散列函数。例如上述的取模。实际上,取模运算对整数类型的数据散列情况比较好,不支持其它类型。

本文就这两个问题进行解决,首先是使用泛型来支持任意类型,然后使用CRC64散列函数将任意二进制数据散列到64位整数再对数组长度取模。由于CRC64具有良好的散列性质,因此实现的哈希表不容易出现冲突。

原理

我们使用链地址法解决冲突。如下图:
数组中每个元素都相当于是一个链表, 当某个位置发生冲突时,直接把数据挂在链表上即可。
假设哈希表中已经有了16, 18两个数据,现在需要把32放入,经过哈希函数映后应该放在数据下标为0的位置。那把32放入链表中元素16后面即可。
在这里插入图片描述
在Java语言(版本1.8)中,HashMap也是使用链地址法解决冲突,当链表长度大于8时,链表将转换成红黑树。显然红黑树的平衡性质使得在红黑树上查找一个元素远快于链表。但是我认为如果能够选择一个较均匀的哈希函数,那么链表长度通常不会很长(3左右),使用红黑树提升的性能并不明显。

哈希函数有如下选择:

  • DJB2 系列
  • FNV 系列
  • SDBM
  • CRC32、CRC64
  • Murmur 系列

有人已经测过这些函数的各种性能,包括碰撞率、速度、多种数据集上的分散程度:

还有一些比较新的哈希函数,如SipHash、HighwayHash、xxHash、MetroHash

本文选择碰撞率看起来较少的CRC64。读者可根据实际性况选择。

实现

基于go 进行实现

结构

首先定义链表的结点结构。next表示下一结点,key表示用来作索引的键,value表示数据。

type _HashEntry[K comparable, V interface{}] struct {
	next  *_HashEntry[K, V]
	key   K
	value V
}

接下来定义HashMap的结构。其中需要维护一个array数组,数组的数据类型为链表结点的指针。
size表示哈希表中数据的个数。

type HashMap[K comparable, V interface{}] struct {
	threshhold    int                 //  数组长度超过此阈值则扩容
	inflateFactor float64             //  百分比,扩容时需要用
	array         []*_HashEntry[K, V] //  数组
	size          int                 //  实际数据量
}

然后是HashMap的构造函数。
初始的数组大小为16,指定当数组使用率 inflateFactor为75%时,数组需要扩容。threshhold由以下公式计算得到,为了避免多次计算下面的公式,实际是根据它来判断是否需要扩容的。

t h r e s h h o l d = l e n ( a r r a y ) × i n f l a t e F a c t o r \rm{threshhold} = len(array) \times inflateFactor threshhold=len(array)×inflateFactor

func NewHashMap[K comparable, V interface{}]() *HashMap[K, V] {
	initSize := 16
	return &HashMap[K, V]{
		threshhold:    12,
		inflateFactor: 0.75,
		array:         make([]*_HashEntry[K, V], initSize),
		size:          0,
	}
}

哈希函数

实现得比较粗暴,接直将结构体转字符串,然后字符串转二进制。再利用go自带的crc64包散列得到无符号整数。

func hash(key interface{}) uint64 {
	var table = crc64.MakeTable(crc64.ECMA)
	bytesBuffer := bytes.NewBuffer([]byte{})
	switch key.(type) {
	case string:
		str := key.(string)
		err := binary.Write(bytesBuffer, binary.BigEndian, []byte(str))
		if err != nil {
			panic("invalid key!")
		}
	default:
		err := binary.Write(bytesBuffer, binary.BigEndian, []byte(fmt.Sprintf("%v", key)))
		if err != nil {
			panic("invalid key!")
		}
	}
	return crc64.Checksum(bytesBuffer.Bytes(), table)
}

获取大小

直接返回size即可

func (this *HashMap[K, V]) Size() int {
	return this.size
}

放置数据

首先利用哈希函数计算哈希值,因为是一个无符号整数,范围非常大,因此再对数组长度取模确定最终位置 index

放入数组中的链表中后,数据量加1,如果发现数据量大小 size 大于等于threshhold, 则扩充数组。如何扩充数组,将在下面介绍。

func (this *HashMap[K, V]) Set(key K, value V) {
	index := hash(key) % uint64(len(this.array))
	node := &_HashEntry[K, V]{
		key:   key,
		value: value,
	}
	if this.array[index] == nil {
		this.array[index] = node
	} else {
		for i := this.array[index]; i != nil; i = i.next {
			if i.key == key {
				i.value = value
				break
			}
			if i.next == nil {
				i.next = node
			}
		}
	}
	this.size++
	if this.size >= this.threshhold {
		this.inflate()
	}
}

取数据

计算得到位置后,遍历数据取出即可。

func (this *HashMap[K, V]) Get(key K) (V, bool) {
	index := hash(key) % uint64(len(this.array))
	if this.array[index] == nil {
		return *new(V), false
	}

	for node := this.array[index]; node != nil; node = node.next {
		if node.key == key {
			return node.value, true
		}
	}
	return *new(V), false
}

删数据

计算得到位置后,从链表中删除结点,size 减1

func (this *HashMap[K, V]) Delete(key K) {
	index := hash(key) % uint64(len(this.array))
	if this.array[index] == nil {
		return
	}
	if this.array[index].key == key {
		this.array[index] = this.array[index].next
		this.size--
		return
	}
	for node := this.array[index]; node.next != nil; node = node.next {
		if node.next.key == key {
			node.next = node.next.next
			this.size--
			return
		}
	}
}

数组扩充

当数据量大小size 到一定程度时(由inflateFactor和threshhold决定),需要扩充数组以较少哈希冲突。

这一步将重新生成一个长度为原来两倍的数组,然后遍历原数组,将所有数据重新放入新的数组中。

/*
*
扩充表格,并重新hash所有数据
*/
func (this *HashMap[K, V]) inflate() {
	if len(this.array) <= (1 << 61) {
		newLength := len(this.array) * 2
		newThresh := int(float64(newLength) * this.inflateFactor)
		oldArray := this.array
		this.threshhold = newThresh
		this.array = make([]*_HashEntry[K, V], newLength)
		this.size = 0
		// re hashing
		for i := 0; i < len(oldArray); i++ {
			for node := oldArray[i]; node != nil; node = node.next {
				this.Set(node.key, node.value)
			}
		}
	}
}

测试

下面代码测试了以上实现的正确性:

func TestHashMap(t *testing.T) {
	m := structure.NewHashMap[int, string]()

	for i := 0; i < 100; i++ {
		m.Set((i), fmt.Sprint(i+100))
		fmt.Println("insert: ", i)
	}

	m.Delete(38)
	m.Delete(54)
	m.Delete(86)
	m.Delete(101)
	m.Delete(-1)

	for i := 0; i < 100; i++ {
		v, ok := m.Get((i))
		if ok {
			fmt.Println(v)
		} else {
			fmt.Println("error !")
		}
	}

	fmt.Println("size:", m.Size())
}

结果:

在这里插入图片描述

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-09-21 00:52:38  更:2022-09-21 00:56:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 21:24:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码