Go语言核心36讲,Go语言进阶技术二)--学习笔记

技术Go语言核心36讲,Go语言进阶技术二)--学习笔记 Go语言核心36讲(Go语言进阶技术二)--学习笔记08 | container包中的那些容器
我们在上次讨论了数组和切片,当我们提到数组的时候

围棋语言核心36(围棋语言高级技术二)——学习笔记。

08 | container包中的那些容器

上次我们讨论了数组和切片。当我们提到数组时,我们经常会想到链表。那么Go语言的链表是什么呢?

Go语言的链表在标准库的容器/列表代码包中实现。在这个代码包中有两个公共程序实体——List和Element。List实现双向链表(以下简称链表),Element表示链表中元素的结构。

那么,我今天的问题是:可以把自己生成的Element类型值传给链表吗

这里我们使用列表的四种方法。

MoveBefore方法和MoveAfter方法分别用于将给定元素移动到另一个元素的前面和后面。

MoveToFront方法和MoveToBack方法分别用于将给定的元素移动到链表的前面和后面。

在这些方法中,“给定元素”是元素类型,元素类型是元素类型的指针类型,而*Element的值是元素的指针。

函数(l *列表)移动前(e,标记*元素)

函数(l *List)移动后(e,标记*元素)

函数(l *列表)移动到前面(e *元素)

函数(l *列表)移动框(e *元素)

具体的问题是,如果我们自己生成这样一个值,然后把它作为“给定元素”传递给链表方法,如果链表接受了会怎么样?

在这里,给出一个典型的答案:不会,这些方法不会对链表做任何改动。因为我们自己生成的Element值不在链表中,所以谈不上“在链表中移动元素”。此外,链表不允许我们将自己生成的元素值插入其中。

问题解析

在列表中包含的方法中,用于插入新元素的方法只接受接口{}类型的值。这些方法在内部使用元素值包装接收到的新元素。

这只是为了避免直接使用自己生成的元素,主要原因是为了避免链表的内部关联和外部破坏,这对链表本身和我们用户都是有利的。

列表有以下几种方法:

“前”和“后”方法用于获取链表中最前面和最后的元素,

InsertBefore和InsertAfter方法用于在指定元素的前后插入新元素,而PushFront和PushBack方法分别用于在链表的最前端和最后一端插入新元素。

函数(l *列表)前()*元素

func(l * List)Back()*元素

func (l *List) InsertBefore(v接口{},mark *Element) *Element

函数(l *List)插入在(v接口{}之后,标记*元素)*元素

func (l *List) PushFront(v接口{ })*元素

函数(l *List)推回(v接口{ })*元素

这些方法将返回一个指向元素值的指针,它们是链表留下的安全“接口”。通过获取指向这些内部元素的指针,我们可以调用前面提到的移动元素的方法。

知识扩展

1. 问题:为什么链表可以做到开箱即用

listelement和listelement都是结构类型。类型有一个特点,即它们的零值是具有特定结构但没有任何自定义内容的值,这相当于一个空壳。值中的字段也被赋予各自类型的零值。

广义上来说,所谓的零值是赋予只声明而不初始化的变量的默认值。每种类型的零值是根据该类型的特性设置的。例如,由语句var a [2]int声明的变量a的值将是一个包含两个零的整数数组。例如,由语句var s []int声明的变量s的值将是值为零的[]int类型的切片。

那么由语句var l列表声明的变量l的值是多少呢?列表?

列表结构类型有两个字段,一个是元素类型字段根,另一个是int类型字段len。顾名思义,前者表示根元素,而后者用于存储链表的长度。请注意,它们在包级别都是私有的,这意味着用户不能查看和修改它们。

如前所述,其字段root和len将被分配相应的零值。len的零值是0,这只是表示链表还没有包含任何元素。因为root是Element类型的,所以它的零值就是这个类型的空壳,如果按照字面意思来表达,就是Element{}。

元素类型包含几个。

个包级私有的字段,分别用于存储前一个元素、后一个元素以及所属链表的指针值。另外还有一个名叫Value的公开的字段,该字段的作用就是持有元素的实际值,它是interface{}类型的。在Element类型的零值中,这些字段的值都会是nil。

这个零值将会是一个长度为0的链表。这个链表持有的根元素也将会是一个空壳,其中只会包含缺省的内容。那这样的链表我们可以直接拿来使用吗

答案是,可以的。这被称为“开箱即用”。Go 语言标准库中很多结构体类型的程序实体都做到了开箱即用。这也是在编写可供别人使用的代码包(或者说程序库)时,我们推荐遵循的最佳实践之一。那么,语句var l list.List声明的链表l可以直接使用,这是怎么做到的呢

关键在于它的“延迟初始化”机制。

所谓的延迟初始化,你可以理解为把初始化操作延后,仅在实际需要的时候才进行。延迟初始化的优点在于“延后”,它可以分散初始化操作带来的计算量和存储空间消耗。

例如,如果我们需要集中声明非常多的大容量切片的话,那么那时的 CPU 和内存空间的使用量肯定都会一个激增,并且只有设法让其中的切片及其底层数组被回收,内存使用量才会有所降低。

如果数组是可以被延迟初始化的,那么计算量和存储空间的压力就可以被分散到实际使用它们的时候。这些数组被实际使用的时间越分散,延迟初始化带来的优势就会越明显。

实际上,Go 语言的切片就起到了延迟初始化其底层数组的作用,你可以想一想为什么会这么说的理由。延迟初始化的缺点恰恰也在于“延后”。你可以想象一下,如果我在调用链表的每个方法的时候,它们都需要先去判断链表是否已经被初始化,那这也会是一个计算量上的浪费。在这些方法被非常频繁地调用的情况下,这种浪费的影响就开始显现了,程序的性能将会降低。

在这里的链表实现中,一些方法是无需对是否初始化做判断的。比如Front方法和Back方法,一旦发现链表的长度为0, 直接返回nil就好了。

又比如,在用于删除元素、移动元素,以及一些用于插入元素的方法中,只要判断一下传入的元素中指向所属链表的指针,是否与当前链表的指针相等就可以了。

如果不相等,就一定说明传入的元素不是这个链表中的,后续的操作就不用做了。反之,就一定说明这个链表已经被初始化了。

原因在于,链表的PushFront方法、PushBack方法、PushBackList方法以及PushFrontList方法总会先判断链表的状态,并在必要时进行初始化,这就是延迟初始化。

而且,我们在向一个空的链表中添加新元素的时候,肯定会调用这四个方法中的一个,这时新元素中指向所属链表的指针,一定会被设定为当前链表的指针。所以,指针相等是链表已经初始化的充分必要条件。

明白了吗List利用了自身以及Element在结构上的特点,巧妙地平衡了延迟初始化的优缺点,使得链表可以开箱即用,并且在性能上可以达到最优。

问题 2:Ring与List的区别在哪儿

container/ring包中的Ring类型实现的是一个循环链表,也就是我们俗称的环。其实List在内部就是一个循环链表。它的根元素永远不会持有任何实际的元素值,而该元素的存在就是为了连接这个循环链表的首尾两端。

所以也可以说,List的零值是一个只包含了根元素,但不包含任何实际元素值的空链表。那么,既然Ring和List在本质上都是循环链表,那它们到底有什么不同呢

最主要的不同有下面几种。

  • Ring类型的数据结构仅由它自身即可代表,而List类型则需要由它以及Element类型联合表示。这是表示方式上的不同,也是结构复杂度上的不同。
  • 一个Ring类型的值严格来讲,只代表了其所属的循环链表中的一个元素,而一个List类型的值则代表了一个完整的链表。这是表示维度上的不同。
  • 在创建并初始化一个Ring值的时候,我们可以指定它包含的元素的数量,但是对于一个List值来说却不能这样做(也没有必要这样做)。循环链表一旦被创建,其长度是不可变的。这是两个代码包中的New函数在功能上的不同,也是两个类型在初始化值方面的第一个不同。
  • 仅通过var r ring.Ring语句声明的r将会是一个长度为1的循环链表,而List类型的零值则是一个长度为0的链表。别忘了List中的根元素不会持有实际元素值,因此计算长度时不会包含它。这是两个类型在初始化值方面的第二个不同。
  • Ring值的Len方法的算法复杂度是 O(N) 的,而List值的Len方法的算法复杂度则是 O(1) 的。这是两者在性能方面最显而易见的差别。

其他的不同基本上都是方法方面的了。比如,循环链表也有用于插入、移动或删除元素的方法,不过用起来都显得更抽象一些,等等。

总结

我们今天主要讨论了container/list包中的链表实现。我们详细讲解了链表的一些主要的使用技巧和实现特点。由于此链表实现在内部就是一个循环链表,所以我们还把它与container/ring包中的循环链表实现做了一番比较,包括结构、初始化以及性能方面。

思考题

  • container/ring包中的循环链表的适用场景都有哪些
  • 你使用过container/heap包中的堆吗它的适用场景又有哪些呢

参考阅读

切片与数组的比较

切片本身有着占用内存少和创建便捷等特点,但它的本质上还是数组。切片的一大好处是可以让我们通过窗口快速地定位并获取,或者修改底层数组中的元素。

不过,当我们想删除切片中的元素的时候就没那么简单了。元素复制一般是免不了的,就算只删除一个元素,有时也会造成大量元素的移动。这时还要注意空出的元素槽位的“清空”,否则很可能会造成内存泄漏。

另一方面,在切片被频繁“扩容”的情况下,新的底层数组会不断产生,这时内存分配的量以及元素复制的次数可能就很可观了,这肯定会对程序的性能产生负面的影响。

尤其是当我们没有一个合理、有效的”缩容“策略的时候,旧的底层数组无法被回收,新的底层数组中也会有大量无用的元素槽位。过度的内存浪费不但会降低程序的性能,还可能会使内存溢出并导致程序崩溃。

由此可见,正确地使用切片是多么的重要。不过,一个更重要的事实是,任何数据结构都不是银弹。不是吗数组的自身特点和适用场景都非常鲜明,切片也是一样。它们都是 Go 语言原生的数据结构,使用起来也都很方便. 不过,你的集合类工具箱中不应该只有它们。这就是我们使用链表的原因。

不过,对比来看,一个链表所占用的内存空间,往往要比包含相同元素的数组所占内存大得多。这是由于链表的元素并不是连续存储的,所以相邻的元素之间需要互相保存对方的指针。不但如此,每个元素还要存有它所属链表的指针。

有了这些关联,链表的结构反倒更简单了。它只持有头部元素(或称为根元素)基本上就可以了。当然了,为了防止不必要的遍历和计算,链表的长度记录在内也是必须的。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/37482.html

(0)

相关推荐

  • qt串口消息模拟器怎么实现

    技术qt串口消息模拟器怎么实现本篇内容介绍了“qt串口消息模拟器怎么实现”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!/

    攻略 2021年11月30日
  • 如何理解epoll原理

    技术如何理解epoll原理这篇文章将为大家详细讲解有关如何理解epoll原理,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。epoll的系统调用很简单,只有三个,其定义如下

    攻略 2021年11月19日
  • 国际象棋怎么玩,磁石国际象棋磁性折叠怎么玩儿

    技术国际象棋怎么玩,磁石国际象棋磁性折叠怎么玩儿棋子和棋盘国际象棋由黑白两棋组成国际象棋怎么玩,执白先行,国际象棋的对局目的是把对方的国王将死。以下三点如果全行不通,国王就算将死:
    1.挡住“将军”的局势
    2.离开“将军

    生活 2021年10月23日
  • 立体爱心,怎样用吸管折星星拼出立体桃心

    技术立体爱心,怎样用吸管折星星拼出立体桃心1、先把吸管压平了,对折成一个直角,中心点会有一个小三角立体爱心。(打开时你会发现有一条斜的折痕) 2、把压在底下那根吸管折到上方,再把下面那根也折到上方。3、然后原本的小三角变

    生活 2021年10月24日
  • JS日期控件My97DatePicker怎么用(my97datepicker用法)

    技术JS日期控件My97DatePicker怎么用这篇文章主要介绍了JS日期控件My97DatePicker怎么用,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解

    攻略 2021年12月20日
  • 我最喜欢的一本书英语作文,我最喜欢的故事书英语范文

    技术我最喜欢的一本书英语作文,我最喜欢的故事书英语范文My Favorite Book Harry Potter Do you know Harry Potter? Its one of my favorite read

    生活 2021年10月28日