golang的重要数据结构-slice和map

首先,golang默认都是采用值传递,即拷贝传递。

这时就有人觉得疑惑了,为什么slice和map在局部变量也能改变外部变量???

其实,他们在函数传参的时候,还是值传递,只不过slice和map,channel这些属于指针类型,

每次copy一个指针消耗非常低(因为只开辟了一点的堆内存,并不像数组那种需要开辟一个大的栈内存),指针指向的地址还是原来那些内容。

其实就是传递指向该地址的指针副本,指针占4个字节。

slice的数据结构

type slice struct{
    array unsafe.Point   //底层数组的指针
    len int
    cap int
}

从这里可以看出slice占24个字节(8+8+8)

再看看appendint的操作:

func appendInt(x []int, y int) []int {
    var z []int
    zlen := len(x) + 1
    if zlen <= cap(x) {
        // There is room to grow.  Extend the slice.
        z = x[:zlen]
    } else {
        // There is insufficient space.  Allocate a new array.
        // Grow by doubling, for amortized linear complexity.
        zcap := zlen
        if zcap < 2*len(x) {
            zcap = 2 * len(x)
        }
        z = make([]int, zlen, zcap)
        copy(z, x) // a built-in function; see text
    }
    z[len(x)] = y
    return z
}

其中len是slice中存有数据的大小,cap是底层数组的大小。

当做切片操作时,引用的是源slice的底层数组,只有在做append操作时如果超过了cap,就会自动分配一个新的底层数组,cap大小是原cap的两倍(反正就是2的n次方)。

    sli := make([]int, 0, 1)
    fmt.Printf("%p, len = %v, cap = %v, byte = %v\n", sli, len(sli), cap(sli), unsafe.Sizeof(sli))
    sli = append(sli, 1)
    fmt.Printf("%p, len = %v, cap = %v, byte = %v\n", sli, len(sli), cap(sli), unsafe.Sizeof(sli))
    sli = append(sli, 2)
    fmt.Printf("%p, len = %v, cap = %v, byte = %v\n", sli, len(sli), cap(sli), unsafe.Sizeof(sli))
    sli = append(sli, 3)
    fmt.Printf("%p, len = %v, cap = %v, byte = %v\n", sli, len(sli), cap(sli), unsafe.Sizeof(sli))
    sli = append(sli, 4)
    fmt.Printf("%p, len = %v, cap = %v, byte = %v\n", sli, len(sli), cap(sli), unsafe.Sizeof(sli))
    sli = append(sli, 5)
    fmt.Printf("%p, len = %v, cap = %v, byte = %v\n", sli, len(sli), cap(sli), unsafe.Sizeof(sli))

仔细观察一下,如何扩容的,地址变化了,但是大小永远不会变一直为24个字节:

 

只有sclie, map, chan可以用%p来打印地址

    sli1 := []int{4, 5, 6, 7, 7, 7, 7, 77, 7, 77, 7, 7, 7, 7, 7, 7, 7, 7, 7}
    fmt.Printf("slice : %p, len = %v, cap = %v, byte = %v\n\n", sli1, len(sli1), cap(sli1), unsafe.Sizeof(sli1))    
    arr := [...]int{4, 5, 6, 7, 7, 7, 7, 77, 7, 77, 7, 7, 7, 7, 7, 7, 7, 7, 7}
    fmt.Printf("arr : %p, len = %v, cap = %v, byte = %v\n\n", &arr, len(arr), cap(arr), unsafe.Sizeof(arr))

切片大小一直为24,数组这时候为152,所以说平时如果数据量大,传递切片的指针copy值是要比数组的copy值效率高很多!!!

 

----------------------------------------------------------------- 以下讲一下map -----------------------------------------------------------------

map是必须要通过make初始化的,否则会报错!

golang的map就是用哈希表来实现的,hashmap底层的数据结构:数组 + 链表

hashmap 通过一个 bucket 数组实现,HashMap会首先通过一个哈希函数将key转换为数组下标,所有元素将被 hash 到数组中的 bucket 中,bucket 填满后,

将通过一个 overflow 指针来扩展一个 bucket 出来形成链表,也就是解决冲突问题。

两个键值对哈希运算后的值相同时就会发生哈希碰撞,当发生哈希碰撞时,键值对就会存储在该数组对应链表的下一个节点上。

尽管这样,HashMap的操作效率也是很高的。当不存在哈希碰撞时查找复杂度为O(1),存在哈希碰撞时复杂度为O(N)。所以,但从性能上讲HashMap中的链表出现越少,性能越好;

当然,当存储的键值对非常多时,从存储的角度链表又能分担一定的压力。

 

// HashMap木桶(数组)的个数
const BucketCount = 16

// 链表结构里的数据:键值对
type KV struct {
    Key   string
    Value string
}

// 链表结构
type LinkNode struct {
    Data     KV
    NextNode *LinkNode
}

// 哈希表的结构
type HashMap struct {
    Buckets [BucketCount]*LinkNode // 数组:散列表,里面数据结构是哈希桶,存有键值对的链表
}

//创建只有头结点的链表
func CreateLink() *LinkNode {
    //头结点数据为空 是为了标识这个链表还没有存储键值对
    linkNode := &LinkNode{KV{"", ""}, nil}
    return linkNode
}

// 创建HashMap
func CreateHashMap() *HashMap {
    myMap := &HashMap{}
    //为每个元素添加一个链表对象
    for i := 0; i < BucketCount; i++ {
        myMap.Buckets[i] = CreateLink()
    }
    return myMap
}

 

// 哈希函数,简单的散列算法:它可以将不同长度的key散列成0-BucketCount的整数
func HashCode(key string) int {
    sum := 0
    for i := 0; i < len(key); i++ {
        sum += int(key[i])
    }
    return sum % BucketCount
}

 

//添加键值对
func (myMap *HashMap)AddKeyValue(key string, value string)  {
    //1.将key散列成0-BucketCount的整数作为Map的数组下标
    mapIndex := HashCode(key)

    //2.获取对应数组头结点
    link := myMap.Buckets[mapIndex]

    //3.在此链表添加结点
    if link.Data.Key == "" && link.NextNode == nil {
        //如果当前链表只有一个节点,说明之前未有值插入  修改第一个节点的值 即未发生哈希碰撞
        link.Data.Key = key
        link.Data.Value = value
    }else {
        //发生哈希碰撞
        link.AddNode(KV{key, value})
    }
}

 

//按键取值
func (myMap *HashMap)GetValueForKey(key string) string {
    //1.将key散列成0-BucketCount的整数作为Map的数组下标
    mapIndex := HashCode(key)
    //2.获取对应数组头结点
    link := myMap.Buckets[mapIndex]
    var value string
    //遍历找到key对应的节点(因为有可能是哈希冲突了)
    head := link
    for {
        if head.Data.Key == key {
            value = head.Data.Value
            break
        }else {
            head = head.NextNode
        }
    }
    return value
}

 

 总的来说:就是数组(散列表) + 链表(哈希冲突),  散列表通过散列函数(哈希函数)来确认下标,因为是连续内存,可以根据数组下标偏移量来确定位置,查询复杂度为O(1),

又因为事先已经初始化了数组,只不过存的值为空而已,所以插入的时候只管往数组里面篡改数值就行了,操作复杂度为O(1)。

哈希表的特点是会有一个哈希函数,对你传来的key进行哈希运算,得到唯一的值,一般情况下都是一个数值。Golang的map中也有这么一个哈希函数,也会算出唯一的值,对于这个值的使用,Golang也是很有意思。

Golang把求得的值按照用途一分为二:高位和低位。

  它的tophash 存储的是哈希函数算出的哈希值的高八位。是用来加快索引的。因为把高八位存储起来,这样不用完整比较key就能过滤掉不符合的key,加快查询速度当一个哈希值的高8位和存储的高8位相符合,

再去比较完整的key值,进而取出value。(如果是java的hashmap,比较key是否相等直接用equal,效率会比较低,因为key有可能是一大串字符)

 参考文献:

https://segmentfault.com/a/1190000018380327?utm_source=tag-newest

https://studygolang.com/articles/14583

 

hashmap 作者原文注释:

This file contains the implementation of Go's map type.

A map is just a hash table. The data is arranged
into an array of buckets. Each bucket contains up to
8 key/value pairs. The low-order bits of the hash are
used to select a bucket. Each bucket contains a few
high-order bits of each hash to distinguish the entries
within a single bucket.

If more than 8 keys hash to a bucket, we chain on
extra buckets.

When the hashtable grows, we allocate a new array
of buckets twice as big. Buckets are incrementally
copied from the old bucket array to the new bucket array.

意思为:当bucket数组需要扩容时,它会开辟一倍的内存空间,并且会渐进式的把原数组拷贝,即用到旧数组的时候就拷贝到新数组。

Map iterators walk through the array of buckets and
return the keys in walk order (bucket #, then overflow
chain order, then bucket index). To maintain iteration
semantics, we never move keys within their bucket (if
we did, keys might be returned 0 or 2 times). When
growing the table, iterators remain iterating through the
old table and must check the new table if the bucket
they are iterating through has been moved ("evacuated")
to the new table.

Picking loadFactor: too large and we have lots of overflow
buckets, too small and we waste a lot of space. I wrote
a simple program to check some stats for different loads:

转载于:https://www.cnblogs.com/huangliang-hb/p/11255180.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值