Golang标准库container/list的用法图文详解
作者:画个一样的我
提到单向链表,大家应该是比较熟悉的了。今天介绍的是 golang 官方库提供的 双向链表。
1、基础介绍
单向链表中的每个节点包含数据和指向下一个节点的指针。其特点是每个节点只知道下一个节点的位置,使得数据只能单向遍历。
示意图如下:
双向链表中的每个节点都包含指向前一个节点和后一个节点的指针。这使得在双向链表中可以从前向后或从后向前遍历。
示意图如下:
结合上面的图就很容易明白单、双链表的定义。其中双向链表可以从前向后,也可以从后向前遍历,操作起来也更加方便。
接下来我们看看官方给的例子:
import ( "container/list" "fmt" ) func Example() { // Create a new list and put some numbers in it. l := list.New() e4 := l.PushBack(4) e1 := l.PushFront(1) l.InsertBefore(3, e4) l.InsertAfter(2, e1) // Iterate through list and print its contents. for e := l.Front(); e != nil; e = e.Next() { fmt.Println(e.Value) } // Output: // 1 // 2 // 3 // 4 }
首先调用list.New()
创建一个双向链表,然后添加元素Element
,最后从头遍历链表,打印每个元素的值。
从上可以看出,container/list
提供了两个结构 List、Element。
- List
- Element
平常自己学习算法实现的双向链表也是这样做的,只是元素一般命名为Node而已。
接下来,看看官方为 List 类型提供了哪些方法。
官方还是提供了丰富的API,接下来我们就一起看看源码吧。
2、源码分析
2.1、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 }
Element 一共定义了四个字段,分别是指向前一个节点的 prev,指向下一个节点的 next,存储值的 Value,以及 此元素属于哪个list。
平常自己在定义双向链表 Node 的结构的时候,一般是不会有 list 这个元素的,为什么官方给的有这个元素呢?
说说自己的理解,很有可能有误!
Element 的 list 字段是小写的,那意味着外部使用者是无法获取和定义此字段的,也就是说外部使用者无法通过 Element 来操作 链表。在通篇读过源码后,发现 Element.list 是用于判断插入、移动、删除等操作的元素是否属于此链表,所以我认为增加 list 字段的原因主要是安全性。
比如防止在多维链表操作的时候,错误的加入了不属于此链表的节点,有了 list 字段后,就可以做判断,防止这类情况产生。
Element 只有两个方法,即 Next()、Prev()
,源代码如下:
// Next returns the next list element or nil. func (e *Element) Next() *Element { if p := e.next; e.list != nil && p != &e.list.root { return p } return nil } // 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 }
看到这里,官方给的实现方式,并不是简单的 e.prev、e.next
,而是多了p != &e.list.root
的判断,为什么会有这个判断呢?
因为container/list
起始是一个环形链表,那么就需要有一个特殊的节点切断这种环形关系,root
就是用来做这个标识的节点。
这样做有什么好处呢?
root
字段是链表的根节点,它并不直接存储数据,而是一个空节点(Element
类型)。这个空节点被用作链表的哨兵节点(Sentinel Node)或者叫做标志节点(Dummy Node)。
这个哨兵节点的作用是为了简化链表的操作。通过将哨兵节点作为链表的根节点,在实际的链表操作中,就无需考虑头节点为空的情况,即空链表和非空链表的操作逻辑变得更加统一和简化。
- 简化逻辑: 哨兵节点的引入避免了对空链表的特殊处理。无论链表是否为空,头节点(哨兵节点之后的第一个节点)始终存在,这样在操作链表时就无需针对空链表做额外的判断。
- 边界条件更清晰: 有了哨兵节点,链表的头部和尾部都有了固定的节点作为标志,使得链表操作时边界条件更加清晰。
- 提高代码的一致性: 通过哨兵节点,链表的操作逻辑更加统一,减少了特殊情况下的代码分支,提高了代码的一致性和可读性。
2.2、List
2.2.1 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 } // 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() } // Len returns the number of elements of list l. // The complexity is O(1). func (l *List) Len() int { return l.len }
因为container/list
是一个环形链表,所以只用提供一个节点就可以了。
注意:刚初始化时,即调用New生成的链表对象,此时的 root.next、root.prev 都是指向root 自己的 。当使用 PushBack或者PushFront方法后
,root.next 表示 Head Node,root.prev 表示 Tail Node。注意 List.len 的长度是不包含 root 节点的。
2.2.2、获取头尾节点
// Front returns the first element of list l or nil if the list is empty. func (l *List) Front() *Element { if l.len == 0 { return nil } return l.root.next } // 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 }
有上面的介绍后,看这里的代码就很简单了。root.next 表示 Head Node,root.prev 表示 Tail Node。
2.2.3、链表基础操作
在自己实现双向链表时,主要难度在 插入、移动、删除操作的实现,不注意就会出现bug。看看官方是如何做的。
insert
将元素 e 插入到 元素 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 已经是 at 节点 e.prev.next = e e.next.prev = e e.list = l l.len++ return e } // 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) }
remove
// remove removes e from its list, decrements l.len func (l *List) remove(e *Element) { e.prev.next = e.next e.next.prev = e.prev e.next = nil // avoid memory leaks e.prev = nil // avoid memory leaks e.list = nil l.len-- }
move
// move moves e to next to at. func (l *List) move(e, at *Element) { if e == at { return } // 移除到 e 在原来链表中的关系 e.prev.next = e.next e.next.prev = e.prev // 这里和 insert 操作是一样的 e.prev = at e.next = at.next e.prev.next = e e.next.prev = e }
因为这里是移动节点位置,不是新增元素,所以链表长度不用调整。
2.2.4、常用API
下面这些对外提供的 API 就是基于上面的基础操作实现的,自行阅读即可。
// Remove removes e from l if e is an element of list l. // It returns the element value e.Value. // The element must not be nil. 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 } // 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) } // 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) } // InsertBefore inserts a new element e with value v immediately before mark and returns e. // If mark is not an element of l, the list is not modified. // The mark must not be nil. 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) } // InsertAfter inserts a new element e with value v immediately after mark and returns e. // If mark is not an element of l, the list is not modified. // The mark must not be nil. 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) } // MoveToFront moves element e to the front of list l. // If e is not an element of l, the list is not modified. // The element must not be nil. func (l *List) MoveToFront(e *Element) { if e.list != l || l.root.next == e { return } // see comment in List.Remove about initialization of l l.move(e, &l.root) } // MoveToBack moves element e to the back of list l. // If e is not an element of l, the list is not modified. // The element must not be nil. func (l *List) MoveToBack(e *Element) { if e.list != l || l.root.prev == e { return } // see comment in List.Remove about initialization of l l.move(e, l.root.prev) } // MoveBefore moves element e to its new position before mark. // If e or mark is not an element of l, or e == mark, the list is not modified. // The element and mark must not be nil. func (l *List) MoveBefore(e, mark *Element) { if e.list != l || e == mark || mark.list != l { return } l.move(e, mark.prev) } // MoveAfter moves element e to its new position after mark. // If e or mark is not an element of l, or e == mark, the list is not modified. // The element and mark must not be nil. func (l *List) MoveAfter(e, mark *Element) { if e.list != l || e == mark || mark.list != l { return } l.move(e, mark) } // PushBackList inserts a copy of another list at the back of list l. // The lists l and other may be the same. They must not be nil. func (l *List) PushBackList(other *List) { l.lazyInit() for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() { l.insertValue(e.Value, l.root.prev) } } // 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() for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { l.insertValue(e.Value, &l.root) } }
3、案例
有了上面的基础后,我们再来实战下。
需求:实现一个二维链表,要求第一维以价格从低到高排序,第二维以时间从小到大排序。
package main import ( "container/list" "fmt" "sort" "strings" "time" ) type Order struct { Price float64 CreatedTime time.Time } // TwoDList 二维链表,要求第一维以价格从低到高排序,第二维以时间从小到大排序。 type TwoDList struct { // 索引相同,即表示价格相同,同一索引的链表节点,越靠后时间越大 // 索引越大,价格越高 Rows []*list.List } func NewTwoDList() *TwoDList { return &TwoDList{ Rows: make([]*list.List, 0), } } func (tdl *TwoDList) AddNode(price float64, createdTime time.Time) { order := &Order{Price: price, CreatedTime: createdTime} // 1、 index := sort.Search(len(tdl.Rows), func(i int) bool { return tdl.Rows[i].Front().Value.(*Order).Price >= order.Price }) if index == len(tdl.Rows) { // 此价格不存在 tdl 中, 新增 newList := list.New() newList.PushFront(order) tdl.Rows = append(tdl.Rows, newList) return } // 判断 index 处的价格是否和 order.Price 相等, // 相等, 则往链表添加 // 不相等, 则需要先将 index 之后的往后移一位 if order.Price != tdl.Rows[index].Front().Value.(*Order).Price { newList := list.New() newList.PushFront(order) // 插入元素 newList tdl.Rows = append(tdl.Rows[:index], append([]*list.List{newList}, tdl.Rows[index:]...)...) return } // 时间从小到大排 curRow := tdl.Rows[index] insertPosition := curRow.Front() for insertPosition != nil && order.CreatedTime.After(insertPosition.Value.(*Order).CreatedTime) { insertPosition = insertPosition.Next() } if insertPosition == nil { curRow.PushBack(order) } else { curRow.InsertBefore(order, insertPosition) } } func (tdl *TwoDList) Print() { for i, row := range tdl.Rows { fmt.Printf("index: %d\n", i) for node := row.Front(); node != nil; node = node.Next() { order := node.Value.(*Order) fmt.Printf("order price: %f, time: %v \n", order.Price, order.CreatedTime) } fmt.Println(strings.Repeat("-", 20)) } } func main() { // 创建一个新的二维链表 myTwoDList := NewTwoDList() // 向二维链表添加节点 myTwoDList.AddNode(100, time.Now()) myTwoDList.AddNode(75, time.Now().Add(time.Hour)) myTwoDList.AddNode(75, time.Now().Add(time.Hour)) myTwoDList.AddNode(150, time.Now().Add(2*time.Hour)) myTwoDList.AddNode(75, time.Now().Add(3*time.Hour)) myTwoDList.AddNode(200, time.Now().Add(4*time.Hour)) // 打印二维链表 myTwoDList.Print() }
运行结果:
index: 0
order price: 75.000000, time: 2024-01-02 12:27:34.2398306 +0800 CST m=+3600.004429301
order price: 75.000000, time: 2024-01-02 12:27:34.2398306 +0800 CST m=+3600.004429301
order price: 75.000000, time: 2024-01-02 14:27:34.2398306 +0800 CST m=+10800.004429301
--------------------
index: 1
order price: 100.000000, time: 2024-01-02 11:27:34.2398306 +0800 CST m=+0.004429301
--------------------
index: 2
order price: 150.000000, time: 2024-01-02 13:27:34.2398306 +0800 CST m=+7200.004429301
--------------------
index: 3
order price: 200.000000, time: 2024-01-02 15:27:34.2398306 +0800 CST m=+14400.004429301
--------------------
到此这篇关于Golang标准库container/list的用法图文详解的文章就介绍到这了,更多相关Go container/list内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!