引言
在上一篇文章Go中的双向链表list的使用中,我们已经看过Go的标准库中所提供的双向链表container/list
的使用方法了,在这篇文章中,我们来一起看一看这些方法分别是怎样被实现的。
list中的节点Element
之前有对Java了解的同学应该知道,Java中的双向链表LinkedList
中是使用了Node
来作为链表中的节点的,而在Go中,同样需要一个类型来保存元素中的数据,这个类型便是结构体Element
,其内容较简单,以下便是其具体的代码。
type Element struct {
// 下一个节点与上一个节点的指针
next, prev *Element
// 指向该节点所属的双向链表
list *List
// 用来储存该节点所存的数据
Value interface{}
}
同时,Element
有两个方法,便是我们在上一篇文章中的遍历部分所列举的两个方式,向后遍历与向前遍历
// 返回下一个节点或nil
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// 返回上一个节点或nil
func (e *Element) Prev() *Element {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
在这里可以看到Next()
方法与Prev()
方法并不是直接返回next或者prev指针所指向的点,这里是因为Go中的list双向链表采用的是一种环形的结构,环的起点(同时也是终点)是list的root元素,因此如果遍历到这个点,说明已经遍历到了链表的尽头,此时返回nil即可。
list的结构
在看完list的每个节点的结构Element之后,我们再来看一下双向链表list自身的结构
type List struct {
root Element // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
初始化
我们先来看一看平时构建链表时常用的New()
方法
func New() *List {
return new(List).Init()
}
在New()
中,先是用Go的内置的new
来创建一个刚刚所看到的List
的实例,然后调用List的Init()
方法来对刚刚所创建的实例进行初始化,并返回这个实例。
接下来我们来看看这个对实例进行初始话的方法Init()
。
// 初始化或者重置链表l
func (l *List) Init() *List {
// 将root的next与prev均置为root
// 在外层new(List)时便已经为内层的root分配了内存,其中的值都为零值,但是是可以直接使用的
l.root.next = &l.root
l.root.prev = &l.root
// 将l的长度置0
l.len = 0
// 返回l自身
return l
}
这里需要注意的一点是,在初始化过程中,root节点的next
指针与prev
指针都指向了自身,也就是说是一个换的形式,而在之后的对于链表的操作过程中,如插入等操作之后,链表依然会维持一个链表的形式,这样的一个方便之处在于,如果我们需要得到链表的头部,只需要调用root的next,而同样的,如果我们需要得到链表的尾部,也只需要调用root的prev,这样,在得到链表尾部的时候,就不需要遍历整个链表,提高了链表使用的效率。
关于这一点,可以从list中获取头节点与尾节点的方法Front
与Back
中看出
// 返回l的第一个节点,如果l是空的,则返回nil
func (l *List) Front() *Element {
// 首先判断l是否为空,如果为空,则直接返回
if l.len == 0 {
return nil
}
return l.root.next
}
// 返回l的最后一个节点,如果l是空的,则返回nil
func (l *List) Back() *Element {
if l.len == 0 {
return nil
}
return l.root.prev
}
添加元素
首先来看一下在链表的首部和尾部插入元素的方法:
// 将一个值为v的新的元素e插入在链表l的首部,并返回e
func (l *List) PushFront(v interface{}) *Element {
// 如果链表未经过初始化,则首先进行链表的初始化
l.lazyInit()
return l.insertValue(v, &l.root)
}
// 将一个值为v的新的元素e插入在链表l的尾部,并返回e
func (l *List) PushBack(v interface{}) *Element {
l.lazyInit()
return l.insertValue(v, l.root.prev)
}
可以看到,在PushFront
中首先调用了一个lazyInit()
的方法
// lazyInit lazily initializes a zero List value.
func (l *List) lazyInit() {
if l.root.next == nil {
l.Init()
}
}
看过这个方法的代码之后,我们可以发现,这个方法会首先判断l的root
节点的next
节点是否为nil,在这里可以参考上一节最后所讲的链表的环状的结构。若是nil的话,说明链表l未经过初始化,则调用List
的Init()
方法来对链表进行初始化。
回到PushFront()
中来,在调用lazyInit()
方法之后,又调用了一个叫做insertValue()
的方法,其与其所调用的代码如下
func (l *List) insertValue(v interface{}, at *Element) *Element {
return l.insert(&Element{Value: v}, at)
}
// insert方法将e插入在at之后,并增加l.len,之后返回e
func (l *List) insert(e, at *Element) *Element {
// 首先暂存at的next节点于n
n := at.next
// 将e节点插入在at之后,并将e的prev指向at
at.next = e
e.prev = at
// 将e的next指向n,将n的prev指向e
e.next = n
n.prev = e
// 将节点e的list指向当前链表l
e.list = l
// 将链表的长度自增
l.len++
// 返回e
return e
}
可以看到,insertValue()
方法其实是通过调用insert()
方法来实现的,而自身实则为insrert()
方法的一个封装好的便利函数,因为只需要输入所需添加的元素的值,而不是包含该值的节点的实例。
那么就主要来看一下insert()
方法,其内部实现其实并不复杂,但是有一点设计的比较巧妙,就是并不像Java中的LinkedList或HashMap中每个桶的链表,需要专门地判断插入的是否为头节点或尾节点,并需要进行相应的维护。而在Go中的list的设计中,只通过root
一个节点便可以实现对于头节点与尾节点的查找,且更新方式也与其它正常节点完全相同,若一个节点插入在首部,则会将root
节点的next指向该节点,这样在调用Front()
方法时便可以找到该节点,插入尾节点时同理。
而另外两个插入方法也类似
// 将一个值为v的新的元素e插入到mark节点之前,并返回e
func (l *List) InsertBefore(v interface{}, mark *Element) *Element {
// 如果mark不是l中的节点,则不会修改l,且返回nil
// 需要注意,这里mark是直接调用的,并没有判断是否为空,因此需要保证输入的mark不能为nil
if mark.list != l {
return nil
}
return l.insertValue(v, mark.prev)
}
// 将一个值为v的新的元素e插入到mark节点之后,并返回e
func (l *List) InsertAfter(v interface{}, mark *Element) *Element {
if mark.list != l {
return nil
}
return l.insertValue(v, mark)
}
InsertBefore()
与InsertAfter()
两个方法其实是调用的刚刚所看的insertValue()
方法,不过insertBefore
是先取的前一个节点,然后将需要插入的节点插入到前一节点之后,这两个方法需要注意的一点是方法中输入的mark是直接调用的,并没有判断是否为空,因此需要保证输入的mark不能为nil。
删除元素
// 如果元素e在链表l中,则删除元素e,并返回删除的节点的值
// 注意,该元素不能为nil
func (l *List) Remove(e *Element) interface{} {
// 首先判断e是否是l中的元素,即e的链表指针是否指向l
if e.list == l {
// 在删除时必须保证l已经被初始化过,且l不能为nil,否则删除过程可能出问题导致程序报错
l.remove(e)
}
return e.Value
}
// 从链表中删除节点e,将l的长度len减小,并返回e
func (l *List) remove(e *Element) *Element {
// 将节点e的前一节点的next与后一节点的prev进行修改
e.prev.next = e.next
e.next.prev = e.prev
// 将节点e的next、prev与list指针置空,避免内存泄漏
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
// 将链表的长度减小
l.len--
// 返回e
return e
}
移动元素
// 以下方法中,如果e不是一个l的元素,链表不会被修改,且输入的e不能为nil
func (l *List) MoveToFront(e *Element) {
if e.list != l || l.root.next == e {
return
}
l.move(e, &l.root)
}
func (l *List) MoveToBack(e *Element) {
if e.list != l || l.root.prev == e {
return
}
l.move(e, l.root.prev)
}
func (l *List) MoveBefore(e, mark *Element) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark.prev)
}
func (l *List) MoveAfter(e, mark *Element) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark)
}
// 将节点e移动到at之后
func (l *List) move(e, at *Element) *Element {
// 如果e就是at,则不需要移动节点e,此时可以直接将e返回,不需要其它操作
if e == at {
return e
}
// 修改e的prev与next的各自的指针,相当于将e取出
e.prev.next = e.next
e.next.prev = e.prev
// 将e插入到at之后
n := at.next
at.next = e
e.prev = at
e.next = n
n.prev = e
// 最后将e返回
return e
}