在 Go 语言的 SDK 中其实已经帮助我们实现了双向链表,该包位于 container/list 中。我们可以看看官方的大佬是怎么写双向链表的:
type Element struct {
next, prev *Element
// The list to which this element belongs.
list *List
// The value stored with this element.
Value interface{}
}
type List struct {
root Element
len int
}
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// 判断初始化函数,如果没有被初始化就立即初始化
func (l *List) lazyInit() {
if l.root.next == nil {
l.Init()
}
}
func New() *List { return new(List).Init() }
func (l *List) Len() int { return l.len }
// 获取链表表头元素
func (l *List) Front() *Element {
if l.len == 0 {
return nil
}
return l.root.next
}
// 获取链表表尾元素
func (l *List) Back() *Element {
if l.len == 0 {
return nil
}
return l.root.prev
}
我们可以发现 List 这个链表结构其实内部只有两个东西,一个 “前哨” (root)存放了对应的元素和整个链表的长度值。在链表调用初始化函数的时候,我们会发现,它把链表前哨元素的前后指针指向自己,作为一个初始化的流程。这里比较有意思的一点是,这个"前哨"的功能。
你会发现,前哨(root)在初始化的时候并没有给对应的 Value 赋值,开发者的本意其实是利用前哨来存储整个链表的表头和表尾(只用 root 的 prev 和 next 指针)。
前哨的前驱其实是链表的表头,前哨的后继其实是链表的表尾,root 的 prev 存放表尾, next 存放表头。
我们再回头看看之前的节点的两个方法实现:
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
func (e *Element) Prev() *Element {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
我们就明白他这里的处理是为什么了,还是要加层 p != &e.list.root 判断,因为List在初始化的时候会把前哨的前后指针指向自己。也就是说,如果当前元素的前后指针指向了 List 的前哨了,也就表示当前节点不存在前驱元素或者是后继元素。
Push 操作
在此之前我们先看两个插入函数:
// insert 代表把 e 插到 at 的后面
func (l *List) insert(e, at *Element) *Element {
n := at.next // 保存at的后继
at.next = e // 将 at 后继指向 e
e.prev = at // 将 e 前驱指向 at
e.next = n // 将 e 后继指向原理at的后继
n.prev = e // 将 原来at的后继指向 e
e.list = l // 将 e 的 list 指针指向 l
l.len++ // 链表数量增加
return e
}
// insertValue 其实是对 insert 方法的封装
func (l *List) insertValue(v interface{}, at *Element) *Element {
return l.insert(&Element{Value: v}, at)
}
func (l *List) MoveAfter(e, mark *Element) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark)
}
func (l *List) move(e, at *Element) *Element {
if e == at {
return e
}
e.prev.next = e.next
e.next.prev = e.prev
n := at.next
at.next = e
e.prev = at
e.next = n
n.prev = e
return e
}
而且很多地方做了 lazyInit(很多地方也叫:延迟初始化),即在必要时才初始化链表(申请内存资源),因为 Go 本身也不知道你存的数据到底是个什么东西,如果存放的值很大,那么一锅脑初始化可能会花费很多时间,而且为了避免声明出来没人使用的尴尬局面,所以通过 lazyInit去分散初始化的操作带来的计算量和存储空间消耗。
比如集中声明非常多的大容量切片, CPU 和内存空间的使用量肯定都会一个激增,并且只有设法让其中的切片及其底层数组被回收,内存使用量才会有所降低。所以把初始化分散再配合 GC 可以更好的利用内存资源同时减少CPU压力。
其实,Go SDK 实现的这个链表不仅仅是双向链表,也可以看成是循环链表 List 的 root 元素恰好把链表的对头和队尾元素连起来,但是 root 本身并不存储元素,它仅仅是个“前哨”。