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 类型提供了哪些方法。

container/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(双向链表) 的图文解说的更多相关文章

  1. golang 标准库间依赖的可视化展示

    简介 国庆看完 << Go 语言圣经 >>,总想做点什么,来加深下印象.以可视化的方式展示 golang 标准库之间的依赖,可能是一个比较好的切入点.做之前,简单搜了下相关的内 ...

  2. Golang 标准库log的实现

      原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://gotaly.blog.51cto.com/8861157/1406905 前 ...

  3. golang标准库分析之net/rpc

    net/rpc是golang提供的一个实现rpc的标准库.

  4. Golang 标准库提供的Log(一)

      原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://gotaly.blog.51cto.com/8861157/1405754 G ...

  5. golang 标准库

    前言 不做文字搬运工,多做思路整理 就是为了能速览标准库,只整理我自己看过的...... 最好能看看英文的 标准库 fmt strconv string 跳转 golang知识库总结

  6. golang标准库 context的使用

    本文索引 问题引入 context包简介 示例 问题引入 goroutine为我们提供了轻量级的并发实现,作为golang最大的亮点之一更是备受推崇. goroutine的简单固然有利于我们的开发,但 ...

  7. Golang标准库——io-接口

    接口 Read相关 Reader Reader接口用于包装基本的读取方法. type Reader interface { //将数据读入到p中,直到读满p或者读取的过程中遇到错误,此时返回的n< ...

  8. Golang标准库——io-结构

    结构 LimitedReader 定义 限制从Reader中读取的字节数. type LimitedReader struct { R Reader // underlying reader N in ...

  9. golang 标准库 sync.Map 中 nil 和 expunge 区别

    本文不是 sync.Map 源码详细解读,而是聚焦 entry 的不同状态,特别是 nil 状态和 expunge 状态的区分. entry 是 sync.Map 存放值的结构体,其值有三种,分别为 ...

  10. golang标准库中有些函数只有签名没有函数体是怎么回事?

随机推荐

  1. Solution -「GXOI / GZOI 2019」宝牌一大堆

    Description Link. Summarizing the fucking statement is the last thing in the world I ever want to do ...

  2. Springboot+Guava实现单机令牌桶限流

    令牌桶算法 系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时 ...

  3. rte-rtc

          活动内容 个人中心 立即报名    活动详情 RTE大会(原"RTC大会")创立于2015年,是亚太首个.迄今为止规模最大的实时互联网技术盛会,覆盖200+行业场景 ...

  4. 使用 TensorFlow 进行机器学习

    使用 TensorFlow 进行机器学习 这是使用 TensorFlow 进行机器学习的官方代码存储库. 使用 TensorFlow(Google 最新.最好的机器学习库)开始进行机器学习. 概括 第 ...

  5. gitbook在线记事本

    https://app.gitbook.com/ About this template: An Internal Wiki to lay out everything anyone needs to ...

  6. 2020/4/26 2-sat 学习笔记

    2-sat 吧.... 其实我jio得它一点都不难 嗯 2-sat是个啥东西呢?其实就是有很多人,他们每个人有两个要求,一个要求可以说是要求一个数为0或1而对于第i个数,我们可以选择为0或为1最终询问 ...

  7. 惊奇!Android studio内部在调用Eclipse

    现在用Android studio的人越来越多,主要是说谷歌不再支持Eclipse,而力推Android studio.但是as也太不给力了,我之前写过一篇博客提到. 今天要说的是一个惊天的消息,如题 ...

  8. js数据结构--树

    <!DOCTYPE html> <html> <head> <title></title> </head> <body&g ...

  9. keepalived部署+nginx高可用

    nginx+keepalived搞性能web网络架构实战配置: 环境准备: keepalived+nginx-1: 192.168.1.23 keepalived+nginx-2: 192.168.1 ...

  10. SQL改写案例3(递归查询开窗案例)

    没错,又是京华的开发老哥,这次找我问个SQL实现逻辑的案例. 我博客的案例基本都是他给我的,真的是又要帮他优化SQL还要教他实现SQL逻辑. 开发老哥写的SQL: SELECT ROW_NUMBER( ...