前言
最近因业务需要,使用到了链表结构,本来想是不是还需要自己简单实现一下,上网学习了一下,发现golang提供了一个双向链表的包container/list,里面的方法也很全面,因此学习了一下源码。本文主要是记录一下实用方法的使用,还有container/list的编码思路。
以下内容都是基于container/list包的实现思路做的,也就是 带头节点 的双向循环链表
双向循环链表 基础 增删图解
这个一级标题下是 我先用画图 + 文字描述的方式描述一下我一开始想的比较基础的思路
节点的结构
双向循环链表中每一个元素都要包含着 自身的值、前驱节点、后驱节点,用golang简单做一个struct表示就是
type Element struct {
// 前驱节点
next *Element
// 后驱节点
prev *Element
// 当前节点的值
Value any
}
用图表示就是
初始化
双向循环链表初始化时,只会有一个空值节点(value是golang默认值,不赋予该节点的value任何意义,就是一个默认值),该节点的next和prev都指向自己
用代码描述就是
func (e *Element) Init() {
e.next = e
e.prev = e
}
func testInit() {
e := new(Element)
e.Init()
}
插入
尾部插入
第一个节点的插入(头节点的下一个)
刚接触链表的朋友对于这种 “第一个” 可能会觉得别扭,可以先看不是 “第一个” 插入的
1、创建出来新节点
2、将新节点的prev指向链表尾部节点
3、将 新节点的next指向 头节点,因为要循环嘛,所以作为新的尾部的next肯定要指向头部
4、将旧的尾部节点的next指向新节点
5、头节点的prev指向新的节点
6、太丑了,整理一下
不是第一个节点
1、创建新节点
2、新节点的prev指向链表末尾节点
3、新节点的next指向头部
4、链表末尾节点指向新节点
5、头节点的prev指向新节点
代码怎么写
不难看出,上面涉及到的点主要有三个节点,分别是:头部、尾部、新节点。
那么这个函数(或者方法)我们要怎么去写呢,把头部,尾部,新节点都传进去嘛,而且,尾部节点要靠遍历去获取嘛,那样做一次尾部插入就是一个O(n)的复杂度了,代价有点高了。
别忘了,这是一个双向且循环的链表,我们只要一个头节点,是不是就有了尾部节点了,因为头节点的prev,就是尾部节点。
func (e *Element) insertEnd(head *Element) {
// 新节点prev指向尾部节点,也就是头节点的next
e.prev = head.next
// 新节点的next指向头节点
e.next = head
// 尾部节点的next指向新节点
// 这里也可以写成 head.next.next = e,但是看起来很笨
e.prev.next = e
// 头节点的prev指向新节点
// 这里也可以写成 head.prev = e
e.next.prev = e
}
注意这里不要先把头节点的prev指向新节点,那样尾部节点就丢失啦,只能遍历才可以找到
头部插入
文章太长啦,就不做第一个节点插入的演示了,一下想不通的朋友自己推演吧先~,代码、思路和普通节点的是一样的
1、创建新节点
2、新节点的prev指向头节点,新节点next指向头节点的next
3、头节点的next指向新节点,新的节点的next(已经指向了头节点的下一个节点)的prev指向新节点
4、太丑了,整理一下
代码怎么写
func (e *Element) insertFront(head *Element) {
// 新节点prev指向头节点
e.prev = head
// 新节点的next指向头节点的next
e.next = head.next
// 原来的head.next的prev指向新节点
e.next.prev = e
// 头节点的next指向新节点
e.prev.next = e
}
删除
以。除上文 新值3 为例
1、要删除的节点的前一个节点的next指向 要删除节点的 后一个节点,要删除的节点的后一个节点的prev指向 要删除的节点的前一个节点
2、删除节点的值滞空,释放内存
代码怎么写
func remove(e *Element) {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil
e.prev = nil
e = nil
}
还有不少有用的函数,比如获取第一个节点的值,获取最后一个节点的值等等,这里不一一去写了,下面看一下golang给的container/list包的编码思路
container/list
节点结构
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
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
}
// Element is an element of a linked list.
type Element struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element
// The list to which this element belongs.
list *List
// The value stored with this element.
Value any
}
这是包内主要用到的两个结构体,List结构体就是封装了头节点和链表长度,Element节点比我自己想的节点要多出一个 list属性,用于表示该节点属于哪个链表,其实说白了就是要List结构里的root,就是链表的头部,相当于链表内的每个元素都有一个指向头节点的指针,注意这里的list属性很有说法,后面还会再提到
初始化
// New returns an initialized list.
func New() *List { return new(List).Init() }
// 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函数即可获得一个初始化好的List实例的引用,我们虽然无法直接访问到root和len属性,但是有对应的方法可以调用,这两个属性对于使用者来说,不需要直接去访问。
插入
经过学习可以看出,最终调用的都是同一个插入方法insertValue
func (l *List) insertValue(v any, at *Element) *Element {
return l.insert(&Element{Value: v}, at)
}
// insert inserts e after at, increments l.len, and returns e.
func (l *List) insert(e, at *Element) *Element {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l
l.len++
return e
}
尾部插入
// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List) PushBack(v any) *Element {
l.lazyInit()
return l.insertValue(v, l.root.prev)
}
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
func (l *List) insertValue(v any, at *Element) *Element {
return l.insert(&Element{Value: v}, at)
}
头部插入
// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List) PushFront(v any) *Element {
l.lazyInit()
return l.insertValue(v, &l.root)
}
插入某个节点之后
func (l *List) InsertAfter(v any, mark *Element) *Element {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark)
}
插入某个节点之前
func (l *List) InsertBefore(v any, mark *Element) *Element {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark.prev)
}
删除节点
func (l *List) Remove(e *Element) any {
if e.list == l {
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e)
}
return e.Value
}
在链表前拼接新链表
// PushFrontList inserts a copy of another list at the front of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushFrontList(other *List) {
l.lazyInit()
// 这里是将other链表从后往前,不停的对l链表做头插
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
l.insertValue(e.Value, &l.root)
}
}
// Back returns the last element of list l or nil if the list is empty.
func (l *List) Back() *Element {
if l.len == 0 {
return nil
}
return l.root.prev
}
// Prev returns the previous list element or nil.
func (e *Element) Prev() *Element {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
1、这里可以看到PushFrontList方法对other链表从后往前遍历,不停对l链表做头部插入
2、也可以看出Element结构体中list元素的作用,我们可以随时获取Element所在链表的头节点,并且可以通过e.prev == e.list.root来判断链表为空,这样不需要头节点,也可以判断链表是否为空,同时,在删除时,为了安全也可以通过list元素来判断删除的节点是否合法
分析
可以一开始我的思路插入头部和插入尾部是两个几乎不能复用代码,当时container/list提供的方法所有的插入最后调用的方法相同,看看人家是怎么个思路
1、将所有的插入都看作是向某个节点的后面插入
2、翻译代码
// 在at节点后插入新节点e
func (l *List) insert(e, at *Element) *Element {
// 新节点的prev指向at
e.prev = at
// 新节点的next指向at的next
e.next = at.next
// at节点的next指向新节点
e.prev.next = e
// at节点之后的节点的prev指向新节点
e.next.prev = e
// 绑定所属链表
e.list = l
// 长度增加
l.len++
return e
}
3、以插入尾部为例,要怎么获得尾部的节点呢?还是那句话,这是个循环链表,头节点的prev就是尾部节点,所以将l.root.prev作为at节点传入就可以了,简单翻译一下代码
// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List) PushBack(v any) *Element {
l.lazyInit()
// l.root.prev就是尾部节点
return l.insertValue(v, l.root.prev)
}
从官方代码获得的启发
1、发觉通用逻辑,尽量做代码复用 ,官方就是将所有插入都当作某个节点之后的插入,所有的插入使用的都是同一个方法,还有移动元素也是,只不过本文不做详细叙述啦
2、封装思想,官方提供的包封装的非常好,List和Element两个结构体内的元素都是私有的(除了Element的Value,毕竟这个值最后我们肯定还得用),无法访问到,使用者也无需过于关心里面是怎样,只需要根据官方文档调用对应的方法来操作链表即可
3、对于一些常用的,但是每次获取都需要经过一定计算的数据,单独做成一个字段,比如List的链表长度len 和 Element中的所属链表list。
(1)链表长度没得说,肯定是要遍历的,单独做一个字段,在修改链表的时候做计算,获取长度的时候就很方便了
(2)而且比如在判断双向循环链表是否为空,我们不需要做 (L->next = = L)&&(L->prior = = L)(也就是头节点next、prev都是自己)这样的判断,只需要通过L.len()获取长度,长度为0就为空,很简单了;
(3)Element中做了一个list不仅可以用来保证链表操作的安全,也能轻易获取到element所属的链表;
4、还有就是操作双向循环链表的一些技巧。
(1)首先这里要强调一点,一定注意在更改前驱后继节点的时候,别把顺序搞错,很容易造成元素丢失,比如插入的时候,要先把新元素的前驱后继节点做好,然后再去操作原链表,尤其是头插,现将root.next改了,整个链表就丢了
(2)双向循环链表因为每个元素都有前驱后继,非常灵活,使用的时候多关注前驱节点,比如可以通过头节点的prev直接获得尾节点