从源码分析:Go中的双线链表list

引言

在上一篇文章Go中的双向链表list的使用中,我们已经看过Go的标准库中所提供的双向链表container/list的使用方法了,在这篇文章中,我们来一起看一看这些方法分别是怎样被实现的。

list中的节点Element

之前有对Java了解的同学应该知道,Java中的双向链表LinkedList中是使用了Node来作为链表中的节点的,而在Go中,同样需要一个类型来保存元素中的数据,这个类型便是结构体Element,其内容较简单,以下便是其具体的代码。

type Element struct {
    // 下一个节点与上一个节点的指针
	next, prev *Element

	// 指向该节点所属的双向链表
	list *List

	// 用来储存该节点所存的数据
	Value interface{}
}

同时,Element有两个方法,便是我们在上一篇文章中的遍历部分所列举的两个方式,向后遍历与向前遍历

// 返回下一个节点或nil
func (e *Element) Next() *Element {
	if p := e.next; e.list != nil && p != &e.list.root {
		return p
	}
	return nil
}

// 返回上一个节点或nil
func (e *Element) Prev() *Element {
	if p := e.prev; e.list != nil && p != &e.list.root {
		return p
	}
	return nil
}

在这里可以看到Next()方法与Prev()方法并不是直接返回next或者prev指针所指向的点,这里是因为Go中的list双向链表采用的是一种环形的结构,环的起点(同时也是终点)是list的root元素,因此如果遍历到这个点,说明已经遍历到了链表的尽头,此时返回nil即可。

list的结构

在看完list的每个节点的结构Element之后,我们再来看一下双向链表list自身的结构

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
}

初始化

我们先来看一看平时构建链表时常用的New()方法

func New() *List {
    return new(List).Init()
}

New()中,先是用Go的内置的new来创建一个刚刚所看到的List的实例,然后调用List的Init()方法来对刚刚所创建的实例进行初始化,并返回这个实例。

接下来我们来看看这个对实例进行初始话的方法Init()

// 初始化或者重置链表l
func (l *List) Init() *List {
    // 将root的next与prev均置为root
    // 在外层new(List)时便已经为内层的root分配了内存,其中的值都为零值,但是是可以直接使用的
	l.root.next = &l.root
    l.root.prev = &l.root
    // 将l的长度置0
    l.len = 0
    // 返回l自身
	return l
}

这里需要注意的一点是,在初始化过程中,root节点的next指针与prev指针都指向了自身,也就是说是一个换的形式,而在之后的对于链表的操作过程中,如插入等操作之后,链表依然会维持一个链表的形式,这样的一个方便之处在于,如果我们需要得到链表的头部,只需要调用root的next,而同样的,如果我们需要得到链表的尾部,也只需要调用root的prev,这样,在得到链表尾部的时候,就不需要遍历整个链表,提高了链表使用的效率。

关于这一点,可以从list中获取头节点与尾节点的方法FrontBack中看出

// 返回l的第一个节点,如果l是空的,则返回nil
func (l *List) Front() *Element {
    // 首先判断l是否为空,如果为空,则直接返回
	if l.len == 0 {
		return nil
	}
	return l.root.next
}

// 返回l的最后一个节点,如果l是空的,则返回nil
func (l *List) Back() *Element {
	if l.len == 0 {
		return nil
	}
	return l.root.prev
}

添加元素

首先来看一下在链表的首部和尾部插入元素的方法:

// 将一个值为v的新的元素e插入在链表l的首部,并返回e
func (l *List) PushFront(v interface{}) *Element {
    // 如果链表未经过初始化,则首先进行链表的初始化
	l.lazyInit()
	return l.insertValue(v, &l.root)
}

// 将一个值为v的新的元素e插入在链表l的尾部,并返回e
func (l *List) PushBack(v interface{}) *Element {
	l.lazyInit()
	return l.insertValue(v, l.root.prev)
}

可以看到,在PushFront中首先调用了一个lazyInit()的方法

// lazyInit lazily initializes a zero List value.
func (l *List) lazyInit() {
	if l.root.next == nil {
		l.Init()
	}
}

看过这个方法的代码之后,我们可以发现,这个方法会首先判断l的root节点的next节点是否为nil,在这里可以参考上一节最后所讲的链表的环状的结构。若是nil的话,说明链表l未经过初始化,则调用ListInit()方法来对链表进行初始化。

回到PushFront()中来,在调用lazyInit()方法之后,又调用了一个叫做insertValue()的方法,其与其所调用的代码如下

func (l *List) insertValue(v interface{}, at *Element) *Element {
	return l.insert(&Element{Value: v}, at)
}

// insert方法将e插入在at之后,并增加l.len,之后返回e
func (l *List) insert(e, at *Element) *Element {
    // 首先暂存at的next节点于n
    n := at.next
    // 将e节点插入在at之后,并将e的prev指向at
	at.next = e
    e.prev = at
    // 将e的next指向n,将n的prev指向e
	e.next = n
    n.prev = e
    // 将节点e的list指向当前链表l
    e.list = l
    // 将链表的长度自增
    l.len++
    // 返回e
	return e
}

可以看到,insertValue()方法其实是通过调用insert()方法来实现的,而自身实则为insrert()方法的一个封装好的便利函数,因为只需要输入所需添加的元素的值,而不是包含该值的节点的实例。

那么就主要来看一下insert()方法,其内部实现其实并不复杂,但是有一点设计的比较巧妙,就是并不像Java中的LinkedList或HashMap中每个桶的链表,需要专门地判断插入的是否为头节点或尾节点,并需要进行相应的维护。而在Go中的list的设计中,只通过root一个节点便可以实现对于头节点与尾节点的查找,且更新方式也与其它正常节点完全相同,若一个节点插入在首部,则会将root节点的next指向该节点,这样在调用Front()方法时便可以找到该节点,插入尾节点时同理。

而另外两个插入方法也类似

// 将一个值为v的新的元素e插入到mark节点之前,并返回e
func (l *List) InsertBefore(v interface{}, mark *Element) *Element {
    // 如果mark不是l中的节点,则不会修改l,且返回nil
    // 需要注意,这里mark是直接调用的,并没有判断是否为空,因此需要保证输入的mark不能为nil
	if mark.list != l {
		return nil
	}
	return l.insertValue(v, mark.prev)
}

// 将一个值为v的新的元素e插入到mark节点之后,并返回e
func (l *List) InsertAfter(v interface{}, mark *Element) *Element {
	if mark.list != l {
		return nil
	}
	return l.insertValue(v, mark)
}

InsertBefore()InsertAfter()两个方法其实是调用的刚刚所看的insertValue()方法,不过insertBefore是先取的前一个节点,然后将需要插入的节点插入到前一节点之后,这两个方法需要注意的一点是方法中输入的mark是直接调用的,并没有判断是否为空,因此需要保证输入的mark不能为nil。

删除元素

// 如果元素e在链表l中,则删除元素e,并返回删除的节点的值
// 注意,该元素不能为nil
func (l *List) Remove(e *Element) interface{} {
	// 首先判断e是否是l中的元素,即e的链表指针是否指向l
	if e.list == l {
		// 在删除时必须保证l已经被初始化过,且l不能为nil,否则删除过程可能出问题导致程序报错
		l.remove(e)
	}
	return e.Value
}

// 从链表中删除节点e,将l的长度len减小,并返回e
func (l *List) remove(e *Element) *Element {
	// 将节点e的前一节点的next与后一节点的prev进行修改
	e.prev.next = e.next
	e.next.prev = e.prev
	// 将节点e的next、prev与list指针置空,避免内存泄漏
	e.next = nil // avoid memory leaks
	e.prev = nil // avoid memory leaks
	e.list = nil
	// 将链表的长度减小
	l.len--
	// 返回e
	return e
}

移动元素

// 以下方法中,如果e不是一个l的元素,链表不会被修改,且输入的e不能为nil
func (l *List) MoveToFront(e *Element) {
	if e.list != l || l.root.next == e {
		return
	}
	l.move(e, &l.root)
}


func (l *List) MoveToBack(e *Element) {
	if e.list != l || l.root.prev == e {
		return
	}
	l.move(e, l.root.prev)
}

func (l *List) MoveBefore(e, mark *Element) {
	if e.list != l || e == mark || mark.list != l {
		return
	}
	l.move(e, mark.prev)
}

func (l *List) MoveAfter(e, mark *Element) {
	if e.list != l || e == mark || mark.list != l {
		return
	}
	l.move(e, mark)
}

// 将节点e移动到at之后
func (l *List) move(e, at *Element) *Element {
	// 如果e就是at,则不需要移动节点e,此时可以直接将e返回,不需要其它操作
	if e == at {
		return e
	}
	// 修改e的prev与next的各自的指针,相当于将e取出
	e.prev.next = e.next
	e.next.prev = e.prev

	// 将e插入到at之后
	n := at.next
	at.next = e
	e.prev = at
	e.next = n
	n.prev = e

	// 最后将e返回
	return e
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值