Go 标准库-双向链表 (container/list) 源码解析

container/list双向链表解析

概述

container/list包实现了基本的双向链表功能,包括元素的插入、删除、移动功能。

链表

链表是一种非连续存储的容器,由多个节点组成,节点通过一些变量记录彼此之间的关系。列表有多种实现方法,如单链表、双链表等

列表的原理可以这样理解:假设 A、B、C 三个人都有电话号码,如果 A 把号码告诉给 B,B 把号码告诉给 C,这个过程就建立了一个单链表结构,如下图所示。
请添加图片描述
如果在这个基础上,再从 C 开始将自己的号码给自己知道号码的人,这样就形成了双链表结构,如下图所示
请添加图片描述
那么如果需要获得所有人的号码,只需要从 A 或者 C 开始,要求他们将自己的号码发出来,然后再通知下一个人如此循环。这个过程就是列表遍历

如果 B 换号码了,他需要通知 A 和 C,将自己的号码移除。这个过程就是列表元素的删除操作,如下图所示
请添加图片描述
在 Go 语言中,链表使用 container/list 包来实现,内部的实现原理是双链表。列表能够高效地进行任意位置的元素插入和删除操作

源码解析

实现原理

// Element 元素结构体
type Element struct {
  // 上一个和下一个元素节点的指针
  next, prev *Element
  // 这个元素所属的列表
  list       *List
  // 该节点存储的数据
  Value      interface{}
}

请添加图片描述
根据节点的定义可以得出以上示意图,各个节点之间有两个引用分别指向上一个节点和下一个节点,由此可看到该链表可以在左边第一个元素或向右进行遍历, 也可以在右边第一个元素向左开始遍历

这时便有一个问题,怎么记录该链表的的头节点和尾节点?这就需要用到我们上面提过的list了

先来看一下List的定义:

type List struct {
  // 哨兵元素,仅使用&root、root.prev和root.next
  root Element
  // 列表长度,不包括哨兵(root)元素
  len  int
}

List 是一个相当简单的结构体,其中只包含了一个root的节点和一个整型的len。一个List代表一条链表,而len就是该链表的元素个数

root是一个虚拟节点(哨兵仅当做标记来使用),即root这个节点不包含在链表元素内,它仅仅是用来记录链表的头和尾的一个辅助节点,加上了虚拟节点的链表如下图所示:在这里插入图片描述

由上图可以看到,通过一个root节点便将链表的头尾连接起来,实现了一个循环链表。

通过获取root的prev和next就能快速地获取链表的头、尾两个节点,这两个操作都是O(1)复杂度,所以后面的链表插入、删除、遍历很多都是以root来做坐标后进行移动了。

那么在使用List 时需要先对root进行初始化

// Init initializes or clears list l.
func (l *List) Init() *List {
  l.root.next = &l.root
  l.root.prev = &l.root
  l.len = 0
  return l
}

// New returns an initialized list.
func New() *List { return new(List).Init() }

使用New函数去初始化一个List链表,该函数会new一个List,返回的指针调用了Init方法。从该方法我们可以看到,初始化是root节点的prev和next都是指向自身

插入/移动 元素

假如我们需要在整个链表尾部插入一个新的元素节点e4,那么我们需要知道目前列表的最后一个元素e3的位置,然后在e3后面插入一个元素e4,具体怎么做呢?

  1. 找到最后一个元素e3,我们的已知条件为元素root的位置,那么元素e3的位置就等于e3:=root.prev
    请添加图片描述

  2. 设置新元素e4,e4.prev设置为上一个新元素e3,e4.next 设置为e3当前的下一个元素。
    请添加图片描述
    总结一下表达式:

    e4.prev=e3
    e4.next=e3.next
    
  3. 设置完新元素e4的prev/next之后,设置原来元素e3的next为新元素e4:e3.next=e4 ,也可以写为 e4.prev.next=e4
    请添加图片描述

  4. 这时还没有结束,看上图可以发现还有root没有设置。同理,设置原来元素p的下一个的上一个为e,那么表达式就是

    e.next.prev=e
    

请添加图片描述
到这里就完成了在整个链表尾部插入一个新的元素节点。

其实,这里的本质就是找到一个元素在其后插入新元素,换成完整代码则是

// PushFront 将输入塞入尾部
func (l *List) PushBack(v interface{}) *Element {
  e := &Element{Value: v} // 新元素e4
  // 第一步
  at := l.root.prev // 最后一个元素e3
  // 第二步
  e.prev = at
  e.next = at.next
  // 第三步
  e.prev.next = e
  // 第四步
  e.next.prev = e
  // ...
  return e
}

同理,将数据插入头部的话,代码为

func (l *List) PushFront(v interface{}) *Element {
  e := &Element{Value: v} // 新元素
  // 第一步
  at := &l.root // 第一个元素
  // 第二步
  e.prev = at
  e.next = at.next
  // 第三步
  e.prev.next = e
  // 第四步
  e.next.prev = e
  // ...
  return e
}

删除元素

了解了添加元素的原理后,删除元素就相对来说比较简单了,只需要将要删除元素的上一个元素的next指向下一个元素,同时将要删除元素的下一个元素的prev指向上一个元素即可。同时别忘了将删除元素的指向都设置为nil,并维护一下list的len

// remove removes e from its list, decrements l.len, and returns e.
func (l *List) remove(e *Element) *Element {
  e.prev.next = e.next // 上一个元素的next指向下一个元素
  e.next.prev = e.prev // 下一个元素的prev指向上一个元素
  e.next = nil // 防止内存逃逸
  e.prev = nil // 防止内存逃逸
  e.list = nil
  l.len--
  return e
}

在这里插入图片描述

使用方法

参考这里

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值