链表(Linked list)是一种常见数据结构,但并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针。

由于不必须按顺序存储,链表在插入的时候可以达到O(1),比顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间。

Go语言的container包中,目前有heap,list,ring三种数据结构。

Heap 堆 (可实现最大堆和最小堆)
List 双向链表
Ring 环形链表

链表实现标准库的container/list代码包中。
代码包中2个程序实体List和Element
List:实现一个双向链表
Element:代表链表中元素的结构
List实现了一个双向链表(每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。)

特点1:链表占用的内存空间要比包含相同元素的数组所占的内存要大得多,因为其元素不是连续存储的,相邻元素之间需要保存对方的指针。
特点2:每个元素要保存所属链表的指针,在初始化时就拥有了头部元素(根元素),还记录了链表的长度。

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

List有4种方法:
MoveBefore:把给定元素移动到另一个元素前面
MoveAfter:把给定元素移动到另一个元素后面
MoveFront:把给定元素移动到链表的最前端
MoveToBack:把给定元素移动到链表的最后端

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类型的指针类型,该指针类型的值就是元素的指针。

回答:对于我们在代码中生成的值,链表不会接受,也不会对链表产生任何影响。

因为代码生成的Element值并不在链表中,而且链表也不允许我们把自己生成的Element值插入链表。

问题解析:
List类型中插入新元素的方法只接受interface{}类型的值,这些方法内部会使用Element值包装接受到的新元素。这样做避免了直接使用我们自己生成的元素,也避免了链表的内部关联遭到破坏。

List.Front:获取链表中最前端元素,理解为List.GetFront
List.Back:获取链表中最后端元素,理解为List.GetBack
List.InsertBefore:在指定的元素之前插入新元素
List.InsertAfter:在指定的元素之后插入新元素
List.PushFront:在链表的最前端插入新元素
List.PushBack:在链表的最后端插入新元素

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值的指针(*Element),它们也是链表给出的安全接口,有了这些内部元素的指针,我们就可以去调用那些用于移动元素的方法了。

扩展知识:

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

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

一个是Element类型的字段root(根元素),
一个是int类型的字段len(链表长度),它们属于包级私有,使用者无法查看和修改。

例如:

var l list.List

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

Element类型包含几个包级私有字段,分别用于存储前一个元素、后一个元素以及所属链表的指针值;

还有一个Value公开字段,该字段代表了持有元素的实际值,也是interface{}类型。在Element类型的零值中,这些字段的默认值都是nil。

延迟初始化:

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

例如,如果我们在初始化时,需要集中声明大量的大容量切片的时候,此时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用于保持固定数量的元素,保存最近10天日志,最近5天登陆信息等。

08-1 Ring环形链表(另外总结)

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

本学习笔记仅为了总结自己学到的Go语言核心知识,方便以后回忆,文中部分内容摘录自极客时间的《Go语言核心36讲》专栏,如有侵权,请联系我删除。

[Golang学习笔记] 08 链表的更多相关文章

  1. golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题

    golang学习笔记20 一道考察对并发多协程操作一个共享变量的面试题 下面这个程序运行的能num结果是什么? package main import ( "fmt" " ...

  2. golang学习笔记19 用Golang实现以太坊代币转账

    golang学习笔记19 用Golang实现以太坊代币转账 在以太坊区块链中,我们称代币为Token,是以太坊区块链中每个人都可以任意发行的数字资产.并且它必须是遵循erc20标准的,至于erc20标 ...

  3. golang学习笔记18 用go语言编写移动端sdk和app开发gomobile

    golang学习笔记18 用go语言编写移动端sdk和app开发gomobile gomobile的使用-用go语言编写移动端sdk和app开发https://blog.csdn.net/u01249 ...

  4. golang学习笔记17 爬虫技术路线图,python,java,nodejs,go语言,scrapy主流框架介绍

    golang学习笔记17 爬虫技术路线图,python,java,nodejs,go语言,scrapy主流框架介绍 go语言爬虫框架:gocolly/colly,goquery,colly,chrom ...

  5. golang学习笔记16 beego orm 数据库操作

    golang学习笔记16 beego orm 数据库操作 beego ORM 是一个强大的 Go 语言 ORM 框架.她的灵感主要来自 Django ORM 和 SQLAlchemy. 目前该框架仍处 ...

  6. golang学习笔记14 golang substring 截取字符串

    golang学习笔记14 golang substring 截取字符串golang 没有java那样的substring函数,但支持直接根据 index 截取字符串mystr := "hel ...

  7. golang学习笔记13 Golang 类型转换整理 go语言string、int、int64、float64、complex 互相转换

    golang学习笔记13 Golang 类型转换整理 go语言string.int.int64.float64.complex 互相转换 #string到intint,err:=strconv.Ato ...

  8. golang学习笔记12 beego table name `xxx` repeat register, must be unique 错误问题

    golang学习笔记12 beego table name `xxx` repeat register, must be unique 错误问题 今天测试了重新建一个项目生成新的表,然后复制到旧的项目 ...

  9. golang学习笔记11 golang要用jetbrain的golang这个IDE工具开发才好

    golang学习笔记11   golang要用jetbrain的golang这个IDE工具开发才好  jetbrain家的全套ide都很好用,一定要dark背景风格才装B   从File-->s ...

随机推荐

  1. AD账户锁定策略

    AD账户锁定策略在一个域中可以有多套,密码策略只能有一套

  2. 什么是TTL值?(简单明了的解释)

    什么是TTL值? TTL值全称是“生存时间(Time To Live)”,简单的说它表示DNS记录在DNS服务器上的缓存时间. 要理解TTL值,请先看下面的一个例子:假设,有这样一个域名myhost. ...

  3. MVC与WebApi中的异常过滤器

    一.MVC的异常过滤器   1.自定义MVC异常过滤器 创建一个类,继承HandleErrorAttribute即可,如果不需要作为特性使用直接实现IExceptionFilter接口即可, 注意,该 ...

  4. C# 冒泡排序法、插入排序法、选择排序法

    冒泡排序法 是数组等线性排列的数字从大到小或从小到大排序. 以从小到大排序为例. 数据 11, 35, 39, 30, 7, 36, 22, 13, 1, 38, 26, 18, 12, 5, 45, ...

  5. Spark SQL on Yarn-Cluster

    Spark SQL 或者Hive SQL在yarn-client模式下运行正常,但是在yarn-cluster模式下总是报 status failed错误, 需要把$SPARK_HOME/lib/sp ...

  6. python 爬虫部分解释

    example:self.file = www.baidu.com存有baidu站的index.html def parseAndGetLinks(self): # parse HTML, save ...

  7. calayer 的特殊属性整理

    calayer: An object that manages image-based content and allows you to perform animations on that con ...

  8. VMware Harbor 学习

    Harbor简介 Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器,通过添加一些企业必需的功能特性,例如安全.标识和管理等,扩展了开源Docker Distributio ...

  9. 4、Web Service-Jaxws(Eclipse版本)实现查看天气和手机归属地

    1.前提概要 免费的官网:http://www.webxml.com.cn/zh_cn/web_services.aspx 官网提供了各种免费的webservice 我们使用的是:http://ws. ...

  10. Python 多线程 start()和run()方法的区别(三)

    上一篇文章最后只是简单介绍了start()方法和run()方法,这篇文章再详细地看下start()和run()的区别. 在实例调用的函数中加入打印当前线程的名字,分别用start()方法和run()方 ...