go语言学习笔记(八)——List和Element

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_32023305/article/details/82384124
  • 前言

       Go语言的链表实现在其标准库的container/list代码包中。这个包包含了两个公开的程序实体:List和Element。前者实现了一个双向链表,而后者代表了链表中元素的结构

链表特点:

1.链表元素不是连续存储的,相邻元素之间需要互相保存对方的指针,所以链表所占用的内存空间,往往要比包含相同元素的数组所占的内存大得多

2.每个元素存有它所属的那个链表的指针,在初始化时就拥有了头部元素(根元素),也记录了链表的长度(方便遍历和计算)。

  • 问题:可以把自己生成的Element类型值传给链表吗?

先来看几个方法

func (l *List) MoveBefore(e,mark *Element) //表示把给定的元素移到另一个元素的前面
func (l *List) MoveAfter(e, mark *Element) //表示把给定的元素移到另一个元素的后面
func (l *List) MoveToFront(e *Element) //把给定的元素移到最前面
func (l *List) MoveToBack(e *Element) //把给定的元素移到最后面

如果我们自己生成这样的值,然后把它作为“给定的元素”传给链表的这些方法,那么链表会接受它吗?

答案是:不会接受,这些方法将不会对链表做出任何改动,因为我们自己生成的Element值并不在链表中,所以也就谈不上“在链表中移动元素”。更何况链表不允许我们生成的Element值插入其中

  • 问题解析

      在List包含的方法中,用于插入新元素的那些方法都只接受interface{}类型的值,这些方法在内部会使用Element值包装接收到的新元素。这样做正是为了避免直接使用我们自己生成的元素,主要原因是避免链表的内部关联遭到外界破坏。

func (l *List) Front() *Element //获取链表中最前端元素
func (l *List) Back() *Element //获取链表中最后端元素
func (l *List) InsertBefore(v interface{}, mark *Element) *Element //在指定元素之前插入新元素
func (l *List) InsertAfter(v interface{}, mark *Element) *Element //在指定元素之后插入新元素
func (l *List) PushFront(v interface{}) *Element //在链表的最前端插入新元素
func (l *List) PushBack(v interface{}) *Element //在链表的最后端插入新元素

这些方法都会把一个Element值的指针作为结果返回,它们就是链表留给我们的安全“接口”。拿到这些内部元素的指针,我们就可以去调用前面提到的那些用于移动元素的方法了。

  • 知识拓展

List和Element都属于结构体类型(关键字struct,是由一系列具有相同类型或不同类型的数据构成的数据集合),特点有:

1、它们的零值都会拥有其特定结构,但是没有任何内容。

2、值中的字段被赋予各自类型的零值(只做声明,未做初始化),每个类型的零值都会依据该类型的特性而被设定。

var a [2]int : 变量a包含了两个0的整数数组

var b []int: 变量b是一个[]int类型,值为nil的切片

var l list.List: 变量l是一个长度为0的链表,根元素为缺省空壳的内容,可以拿来“开箱即用”。

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

List这个“结构体类型”(关键字struct)有两个字段:

一个是Element类型的字段root(根元素),

一个是int类型的字段len(链表长度)

它们属于包级私有,使用者无法查看和修改。

例如:var l list.List中,l有字段root和len,都被赋予相应的零值,len为0,表示该链表未包含任何元素;root的零值表示该类型的空壳,用字面量表示为:Element{}。

Element类型包含几个包级私有字段,分别用于存储前一个元素、后一个元素以及所属链表的指针值;还有一个Value公开字段,该字段代表了持有元素的实际值,也是interface{}类型。在Element类型的零值中,这些字段的默认值都是nil。

对于List类型,有一个“延迟初始化”机制。

延迟初始化就是把初始化操作延后,仅在实际需要使用的时候才进行,可以分散初始化操作过于集中带来的计算量和存储空间的密集消耗,优点就在于“延后”。

例如,如果我们在初始化时,需要集中声明大量的大容量切片的时候,此时CPU和内存空间的使用会激增,只有设法让其中的切片及其底层数组被及时回收,内存的使用量才会有所降低。

而延迟初始化操作,可以将计算量和存储空间的压力分散到实际使用它们的时候,这些数组或切片被实际使用的时间越分散,延迟初始化带来的优势会越明显。

但是,延迟初始化的缺点,也在于“延后”,试想如果在调用链表的每个方法的时候,都需要去判断链表是否已经被初始化,那么也是一个计算量上的浪费。这些方法被调用的越频繁,程序的性能也会降低的越多。

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

又如,在删除元素、移动元素,以及一些插入元素的方法时,只要判断一下传入的元素指向所属链表的指针,是否与当前链表的指针相等就可以了。如果不相等,就说明传入的元素不是这个链表的,就可以不用进行后续操作。反之,就一定说明这个链表已经被初始化了。

原因在于,链表的PushFront,PushBack,PushBackList,PushFrontList方法总会先判断链表的状态,并在必要时进行初始化,这就是延迟初始化;而且,在向一个空链表中添加新元素时,必定会先调用这四个方法中的一个,这时新元素中指向所属链表的指针,一定会被设定为当前链表的指针,所以说,链表初始化后当前指针肯定会等于新元素指针。

List类型利用了自身以及Element在结构上的特点,平衡了“延迟初始化”的利弊,使得链表可以“开箱即用”,并在性能上达到最优。

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

Ring类型是一个循环链表,也叫“环”。

List在内部也是一个循环链表根元素永远不会持有任何实际的元素值,而该元素的存在,就是为了连接这个循环链表的首尾两端。所以说,List的零值是一个只包含了根元素root,但不包含任何实际元素值的空链表。

Ring和List的不同主要体现在:

1、Ring类型的数据结构由它自身即可代表,而List类型则需要由它及Element类型联合表示;

2、一个Ring类型的值只代表了其所属的循环链表中的一个元素,而一个List类型的值则代表了一个完整的链表;

3、在创建并初始化(New函数)一个Ring值时可以指定它包含的元素的数量,而一个List值却不需要。循环链表一旦被创建,其长度是不可变的。

4、通过ring.Ring声明的变量将会是一个长度为l的循环链表,而List类型的零值则是一个长度为0的链表。

5、Ring值的Len方法的算法复杂度是O(N),而List值的Len方法复杂度则是O(1),性能上差别较大。

  • 思考题

1、container/ring包中的循环链表的适用场景有哪些?

答:ring可以用来保存固定数量的元素,构造定长环回队列,例如保存最近100条日志,用户最近10次操作,网页上的轮播

2、使用过container/heap包中的堆吗?适用场景有哪些?

答:heap可以用来排序,构造优先级队列。游戏编程中是一种高效的定时器实现方案

猜你喜欢

转载自blog.csdn.net/sinat_32023305/article/details/82384124