链表
Go语言的链表实现在其标准库的container/list代码包中。
这个包包含了2个程序实体:
- List : 实现了一个双向链表
- Element : 代表了链表中元素的结构
操作链表
移动链表里的元素:
func (l *List) MoveBefore(e, mark *Element) // 把元素e移动到mark元素的前面
func (l *List) MoveAfter(e, mark *Element) // 把元素e移动到mark元素的后面
func (l *List) MoveToFront(e *Element) // 把元素e移动到链表的最前端
func (l *List) MoveToBack(e *Element) // 把元素e移动到链表的最后端
上面的方法都是调整链表l里元素的位置,e和mark原本都是链表里的元素,执行方法后,只是调整元素e在链表中的位置。所以操作前后链表里包含的元素并不会有差别,只是e元素的位置可能变化了。
添加元素
链表里的元素都是*Element类型。List包中那些用于插入新元素的方法都只接收interface{}类型的值。这个方法内部都会用Element包装接收到的新元素:
func (l *List) InsertBefore(v interface{}, mark *Element) *Element // 在mark元素之前插入行元素
func (l *List) InsertAfter(v interface{}, mark *Element) *Element // 在mark元素之后插入行元素
func (l *List) PushFront(v interface{} *Element) *Element // 在链表的最前端添加新元素
func (l *List) PushBack(v interface{} *Element) *Element // 在链表的最后端添加新元素
上面的方法都会返回一个指针*Element。也就是插入元素的*Element类型。
获取元素
通过链表,可以直接取到链表头尾的元素,这是一个双向链表。然后有了链表中的某个元素之后,就可以拿到该元素前一个或后一个元素了:
func (l *List) Front() *Element // 获取到链表中最前端的元素
func (l *List) Back() *Element // 获取到链表中最后端的元素
func (e *Element) Next() *Element // 获取当前元素的下一个元素
func (e *Element) Prev() *Element // 获取当前元素的前一个元素
链表的机制
下面是官方文档里的List类型的描述,隐藏了私有字段:
type List struct {
// contains filtered or unexported fields
}
List这个结构体类型有两个字段,一个是Element类型的字段root,代码链表的根元素;另一个是int类型的字段len,代表链表的长度。都是包级私有的,我们无法查看和修改它们。
下面是Element类型的描述,同样的隐藏了私有字段:
type Element struct {
// The value stored with this element.
Value interface{}
// contains filtered or unexported fields
}
Element类型里分别有一个用于存储前一个元素和后一个元素以及所属链表的指针值。另外还有一个公开字段Value,就是元素的值。
延迟初始化机制
所谓延迟初始化,你可以理解为把初始化操作延后,仅在实际需要的时候才进行。延迟初始化的优点在于“延后”,它可以分散初始化操作带来的计算量和存储空间消耗。
然而,延迟初始化的缺点恰恰也在于“延后”。如果在调用链表的每个方法的时候,都需要去判断链表是否已经被初始化的话,那么也是一个计算量上的浪费。
在这里的链表的实现中,一些方法是无需对是否初始化做判断的。比如:
Front和Back方法,一旦发现链表的长度为0,就可以直接返回nil。
删除、移动、删除链表元素时,判断一下传入元素中的所属链表的指针,是否与当前链表的指针相同。相等,就说明这个链表已经被初始化了,否则说明元素在不要操作的链表中,那么就直接返回。
上面的操作,应该都是要链表是已经完成初始化的,但是未初始化过的链表,通过上面的机制,也能正确返回。这样初始化的操作就可以只在必要的时候才进行,比如:
PushFront、PushBack、PushBackList、PushFrontList,这些方法,会先判断链表的动态。如果没有初始化,就进行初始化。
所以,List利用了自身,以及Element在结构上的特点,平衡了延迟初始化的优缺点。
循环链表
在Go标准库的container/ring包中的Ring类型实现的是一个循环链表。
type Ring struct {
Value interface{} // for use by client; untouched by this library
// contains filtered or unexported fields
}
其实List在内部就是一个循环链表。它的根元素永远不会持有任何实际的元素值,而该元素的存在,就是为了连接这个循环链表的首尾两端。
所以,List的零值是一个只包含了根元素,但不包含任何实际元素值的空链表。
说List在内部就是一个循环链表,是它设计的逻辑,这个在最后我去源码里看了一下。这里并不是指List是通过这里的container/ring包实现的。而是List本身其实也是一个循环链表的结构,Ring是Go提供的一个实现循环链表的标准库,Ring本身当然也是一个循环链表的结构。
Ring和List在本质上都是循环链表,主要有以下的几点不同:
Ring类型的数据结构仅由它自身即可代表,而List类型则需要由它以及Element类型联合表示。这是表示方式上的不同,也是结构复杂度上的不同。
Ring类型的值,只代表了其所属的循环链表中的一个元素,而List类型的值则代表了一个完整的链表。这是表示维度上的不同。
在创建并初始化一个Ring值的时候,要指定它包含的元素的数量,但是List不能也不需要指定数量。这是两个代码包中的New函数在功能上的不同,也是两个类型在初始化值方面的第一个不同。
通过var r ring.Ring语句声明的r将会是一个长度为1的循环链表,而List类型的零值则是一个长度为0的链表。List中的根元素不会持有实际元素值,因此计算长度时不会包含它。这是两个类型在初始化值方面的第二个不同。
Ring值的Len方法的算法复杂度是O(N)的,而List值的Len方法的算法复杂度则是 O(1)的。这是两者在性能方面最显而易见的差别。
关于上的len方法,因为List的结构体里直接就记了表示长度的私有字段len。而Ring不像List那样有一个表示整个链表的结构体。两个包里的len方法的源码如下:
// src/container/ring/ring.go
func (r *Ring) Len() int {
n := 0
if r != nil {
n = 1
for p := r.Next(); p != r; p = p.next {
n++
}
}
return n
}
// src/container/list/list.go
func (l *List) Len() int { return l.len }
总结
上面先讲了链表,并且展开了链表的一些使用技巧和实现特点。由于链表本身内部就是一个循环链表。所以又和container/ring包中的循环链表做了一番比较。
另外,container一共有3个子包,上面讲到了2个,还有一个是container/heap,就是堆。
List的循环结构
关于List内部就是一个循环链表的问题,自己又去源码里探究了一番。
下面是Init方法,把root元素的下一个元素和前一个元素都指向自己,形成了一个环。并且把长度字段len设置成0:
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
虽然List本质是个环,但是使用的时候,不是环而是有头和尾的一条链。在获取下一个元素的时候,如果到了最后端,那么next的下一个元素就是root元素。这时不返回root,而是返回nil。这就是根元素不持有任何元素,只是连接链表的首尾两端:
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
有疑问加站长微信联系(非本文作者)