go的hashmap如何实现的

题目来源:

答案1:

goLang中的map来存储键值对类型的数据,但是它的内部实现是怎么样的?

其实map是一种HashMap,表面上看它只有键值对结构,实际上在存储键值对的过程中涉及到了数组和链表。HashMap之所以高效,是因为其结合了顺序存储(数组)和链式存储(链表)两种存储结构。数组是HashMap的主干,在数组下有有一个类型为链表的元素。

这是一个简单的HashMap的结构图:

image-20220417213546938

当我们存储一个键值对时,HashMap会首先通过一个哈希函数将key转换为数组下标,真正的key-value是存储在该数组对应的链表里。

HashMap的数组往往是有限的,那当要存储的键值对很多数组不够或者两个键值对哈希运算后的值相同时,不就会有不同的键值对存储在同一个数组下吗?是的,这个就叫做哈希碰撞。当发生哈希碰撞时,键值对就会存储在该数组对应链表的下一个节点上。

尽管这样,HashMap的操作效率也是很高的。当不存在哈希碰撞时查找复杂度为O(1),存在哈希碰撞时复杂度为O(N)。所以,但从性能上讲HashMap中的链表出现越少,性能越好;当然,当存储的键值对非常多时,从存储的角度链表又能分担一定的压力。

代码实现

KVMap

首先,HashMap存储的是键值对,所以需要一个键值对类型。

  1. //链表结构里数据的数据类型 键值对
  2. type KV struct {
  3. Key string
  4. Value string
  5. }

LinkNode

键值对又是主要存储在链表里的,所以需要一个链表类。

  1. //链表结构
  2. type LinkNode struct {
  3. //节点数据
  4. Data KV
  5. //下一个节点
  6. NextNode *LinkNode
  7. }
  8. //创建只有头结点的链表
  9. func CreateLink() *LinkNode {
  10. //头结点数据为空 是为了标识这个链表还没有存储键值对
  11. var linkNode = &LinkNode{KV{"",""}, nil}
  12. return linkNode
  13. }

当发生哈希碰撞时,键值对会存储在新建的链表节点上。这里需要一个添加节点的功能,我们这里采用尾插法添加节点。

  1. //尾插法添加节点,返回链表总长度
  2. func (link *LinkNode) AddNode(data KV) int {
  3. var count = 0
  4. //找到当前链表尾节点
  5. tail := link
  6. for {
  7. count += 1
  8. if tail.NextNode == nil {
  9. break
  10. }else {
  11. tail = tail.NextNode
  12. }
  13. }
  14. var newNode = &LinkNode{data, nil}
  15. tail.NextNode = newNode
  16. return count+1
  17. }

HashMap

接下来,就是主角HashMap登场了。

  1. //HashMap木桶(数组)的个数
  2. const BucketCount = 16
  3. type HashMap struct {
  4. //HashMap木桶
  5. Buckets [BucketCount]*LinkNode
  6. }
  7. //创建HashMap
  8. func CreateHashMap() *HashMap {
  9. myMap := &HashMap{}
  10. //为每个元素添加一个链表对象
  11. for i := 0; i < BucketCount ; i++ {
  12. myMap.Buckets[i] = CreateLink()
  13. }
  14. return myMap
  15. }

我们需要一个哈希散列算法,将key转化为一个0-BucketCount的整数,作为存放它的数组的下标。这里这个散列算法,应尽可能随机地使新增的键值对均匀地分布在每个数组下。

一般像go的map和Java的HashMap都会有一个复杂的散列算法来达到这个目的,我们这里只是为了讲HashMap原理,暂且就用一个简单的方法来求出下标。

  1. //自定义一个简单的散列算法,它可以将不同长度的key散列成0-BucketCount的整数
  2. func HashCode(key string) int {
  3. var sum = 0
  4. for i := 0; i < len(key); i++ {
  5. sum += int(key[i])
  6. }
  7. return (sum % BucketCount)
  8. }

往HashMap里添加键值对

  1. //添加键值对
  2. func (myMap *HashMap)AddKeyValue(key string, value string) {
  3. //1.将key散列成0-BucketCount的整数作为Map的数组下标
  4. var mapIndex = HashCode(key)
  5. //2.获取对应数组头结点
  6. var link = myMap.Buckets[mapIndex]
  7. //3.在此链表添加结点
  8. if link.Data.Key == "" && link.NextNode == nil {
  9. //如果当前链表只有一个节点,说明之前未有值插入 修改第一个节点的值 即未发生哈希碰撞
  10. link.Data.Key = key
  11. link.Data.Value = value
  12. fmt.Printf("node key:%v add to buckets %d first node
  13. ", key, mapIndex)
  14. }else {
  15. //发生哈希碰撞
  16. index := link.AddNode(KV{key, value})
  17. fmt.Printf("node key:%v add to buckets %d %dth node
  18. ", key, mapIndex, index)
  19. }
  20. }

根据键从HashMap里取出对应的值

  1. //按键取值
  2. func (myMap *HashMap)GetValueForKey(key string) string {
  3. //1.将key散列成0-BucketCount的整数作为Map的数组下标
  4. var mapIndex = HashCode(key)
  5. //2.获取对应数组头结点
  6. var link = myMap.Buckets[mapIndex]
  7. var value string
  8. //遍历找到key对应的节点
  9. head := link
  10. for {
  11. if head.Data.Key == key {
  12. value = head.Data.Value
  13. break
  14. }else {
  15. head = head.NextNode
  16. }
  17. }
  18. return value
  19. }

Main_test

  1. package main
  2. import (
  3. "chaors.com/LearnGo/BlockchainCryptography/HashMap"
  4. )
  5. func main() {
  6. myMap := HashMap.CreateHashMap()
  7. myMap.AddKeyValue("001", "1")
  8. myMap.AddKeyValue("002", "2")
  9. myMap.AddKeyValue("003", "3")
  10. myMap.AddKeyValue("004", "4")
  11. myMap.AddKeyValue("005", "5")
  12. myMap.AddKeyValue("006", "6")
  13. myMap.AddKeyValue("007", "7")
  14. myMap.AddKeyValue("008", "8")
  15. myMap.AddKeyValue("009", "9")
  16. myMap.AddKeyValue("010", "10")
  17. myMap.AddKeyValue("011", "11")
  18. myMap.AddKeyValue("012", "12")
  19. myMap.AddKeyValue("013", "13")
  20. myMap.AddKeyValue("012", "14")
  21. myMap.AddKeyValue("015", "15")
  22. }

RUN

img

一个简单的HashMap就实现了,虽然我们的散列算法只是用了一个简单的转换算法,这对我们理解HashMap原理已经足够了。