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,具体怎么做呢?
-
找到最后一个元素e3,我们的已知条件为元素root的位置,那么元素e3的位置就等于
e3:=root.prev
-
设置新元素e4,
e4.prev
设置为上一个新元素e3,e4.next
设置为e3当前的下一个元素。
总结一下表达式:e4.prev=e3 e4.next=e3.next
-
设置完新元素e4的prev/next之后,设置原来元素e3的next为新元素e4:
e3.next=e4
,也可以写为e4.prev.next=e4
。
-
这时还没有结束,看上图可以发现还有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
}
使用方法
参考这里。