一、双向链表简介
1、单链表的缺陷
单链表只能从头结点开始访问链表中的数据元素,如果需要逆序访问单链表中的数据元素将极其低效。
2、双向链表的结构
双链表是链表的一种,由节点组成,每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
定义链表中的节点结构体
type Element struct {
next, prev *Element
//元素所在链表
list *List
Value interface{}
}
链表结构体
// List 表示双向链表
type List struct {
// 根节点,链表的起点,不存值。
//直接访问root.pre和root.next分别是链表的最后一个节点和第一个节点
root Element
len int // 当前链表长度,不包含根节点
}
这里我们将链表的首位相连,形成一个环方便使用。
初始化或清空链表方法
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
这里我们将根节点的前驱和后继指向自己,形成一个环型链表。
创建一个新链表
func New() *List { return new(List).Init() }
查看链表长度方法
func (l *List) Len() int { return l.len }
返回链表首节点元素方法
func (l *List) Front() *Element {
if l.len == 0 {
return nil
}
return l.root.next
}
首节点就是根节点指向的后继,注意链表为空的情况处理。
返回链表末节点方法
func (l *List) Back() *Element {
if l.len == 0 {
return nil
}
return l.root.prev
}
向链表特定元素后面插入新元素
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
}
插入元素注意处理顺序:1、先将新添加元素e的前驱指向指定元素at;2、新元素e的后继指向at的后继;这两步是处理新增元素的前驱和后继的指向。下面还要处理指定元素at的前驱和候继。3、e.pre.next就是元算at前一个元素的后继,将其指向新增元素e;4、e.next.pre是元素at后一个元素的前驱,也指向e;5、最后将元素所属链表赋值
直接向链表插入一个值方法
func (l *List) insertValue(v interface{}, at *Element) *Element {
return l.insert(&Element{Value: v}, at)
}
该方法可以省去初始化节点的工作,直接将要插入的值作为参数插入。先创建一个Element实例,然后调用插入方法。
删除链表指定节点
func (l *List) remove(e *Element) *Element {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // 避免内存泄漏
e.prev = nil // 避免内存泄漏
e.list = nil
l.len--
return e
}
先删除节点,并清理内存后返回元素
将一个节点e移到另一节点后面方法
func (l *List) move(e, at *Element) *Element {
if e == at {
return e
}
e.prev.next = e.next
e.next.prev = e.prev
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
return e
}
这里其实就是操作两步:1、将e先从链表移除,类似删除操作;2、将e插入到at元素后面,类似新元素插入。
删除节点并返回元素值方法
func (l *List) Remove(e *Element) interface{} {
if e.list == l {
// 判断节点是否在该链表中
// 如果链表是空的话,而且e是nil的话会panic
l.remove(e)
}
return e.Value
}
插入一个值到链表头
func (l *List) PushFront(v interface{}) *Element {
return l.insertValue(v, &l.root)
}
链表头即首节点,也就是插入链表的root后面一个节点。
插入一个值到链表末尾
func (l *List) PushBack(v interface{}) *Element {
return l.insertValue(v, l.root.prev)
}
末节点也就是root到前驱。这里使用环形双向链表操作比较方便。
插入一个值到某个节点前面
func (l *List) InsertBefore(v interface{}, mark *Element) *Element {
if mark.list != l {
return nil
}
return l.insertValue(v, mark.prev)
}
插入值到元素mark到前面,也就是插入到mark.pre的后面。
插入一个值到某节点后面
func (l *List) InsertAfter(v interface{}, mark *Element) *Element {
if mark.list != l {
return nil
}
return l.insertValue(v, mark)
}
将一个节点移到链表首位置
func (l *List) MoveToFront(e *Element) {
if e.list != l || l.root.next == e {
return
}
l.move(e, &l.root)
}
已经实现了将元素移动到另一个元素后面move方法,所以移动元素到链表首位置,也就是root到后面。
将一个节点移动到链表末尾
func (l *List) MoveToBack(e *Element) {
if e.list != l || l.root.prev == e {
return
}
l.move(e, l.root.prev)
}
等同与将节点移动到root.pre的后面。理解root的后面是首节点,root前面是尾节点。
将一个节点移到另一节点前面
func (l *List) MoveBefore(e, mark *Element) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark.prev)
}
先判断特殊情况,然后等同于移到mark前面一个元素的后面。
移动一个节点到另一个节点的后面
func (l *List) MoveAfter(e, mark *Element) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark)
}
在链表后面添加另一个链表
func (l *List) PushBackList(other *List) {
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
l.insertValue(e.Value, l.root.prev)
}
}
循环将other链表的首节点插入到原链表的尾节点。
将一个链表逆序添加到链表前面
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)
}
}
本文代码参考go-zero框架:go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。
go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。
github地址:github.com/tal-tech/go-zero
这里实现了双向链表的很多方法,可以用在LRU算法中。后面我们会介绍LRU的实现。