程序员社区

Go:双向链表

一、双向链表简介

1、单链表的缺陷

单链表只能从头结点开始访问链表中的数据元素,如果需要逆序访问单链表中的数据元素将极其低效。

2、双向链表的结构

双链表是链表的一种,由节点组成,每个数据结点中都有两个指针,分别指向直接后继和直接前驱。

Go:双向链表插图

定义链表中的节点结构体

type Element struct {
    next, prev *Element
    //元素所在链表
    list *List
    Value interface{}
}

链表结构体

// List 表示双向链表
type List struct {
    // 根节点,链表的起点,不存值。
    //直接访问root.pre和root.next分别是链表的最后一个节点和第一个节点
    root Element
    len  int     // 当前链表长度,不包含根节点
}

这里我们将链表的首位相连,形成一个环方便使用。

Go:双向链表插图1
初始化或清空链表方法
func (l *List) Init() *List {
    l.root.next = &l.root
    l.root.prev = &l.root
    l.len = 0
    return l
}

这里我们将根节点的前驱和后继指向自己,形成一个环型链表。

Go:双向链表插图2
环形双向链表
创建一个新链表
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的实现。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Go:双向链表

一个分享Java & Python知识的社区