双向循环链表和golang的container/list

前言

最近因业务需要,使用到了链表结构,本来想是不是还需要自己简单实现一下,上网学习了一下,发现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直接获得尾节点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值