本文为“Goalng全面深入系列”中的标准库部分。

1. 什么是双向链表

0a056ecf71346b9141d05812e10f8d1e.png(引用)

和单链表比较,双向链表的元素不但知道自己的下线,还知道自己的上线(越来越像传销组织了)。小煤车开起来,图里面可以看出,每个车厢除了一个指向后面车厢的箭头外,还有一个指向前面车厢的箭头(车头、车尾除外)。车头只有指向后面车厢的箭头,车尾只有指向前面车厢的箭头。

2. 和单向链表相比的优势

1. 插入删除不需要移动元素外,可以原地插入删除

2. 可以双向遍历

8fec31ce9dd1eb7a21c3c793a9354790.png

插入数据到中间

825bb1bdb55962079627377beba48bf0.png

删除中间数据

88f11eb048951c990c6fb961e32cf88d.png

3、双向链表与Go的对应结构

1.节点分析

8a83c4ae519469514c1dfa4d103a372b.png

我们先把车厢分解开来。每节车厢都由煤炭、车体、拉前车厢绳索、拉后车厢绳索这4部分组成。车体是我们的运输工具,在Go语言里我们用结构提DNode表示;煤炭代表运的货物,用data变量表示;拉前车厢绳索和拉后车厢绳索我们分别用指针prev和next表示。这样一节车厢,用Go语言描述如下:

type DNode struct {

data Object

prev *DNode

next *DNode

}

2、双向链表

d6900fdf03729fa88e60cf1a5a401037.png

一个运煤车队就是一个双向链表。车队要有车头、车厢、车尾,作为车队的负责人还得知道车队有多长。在Go语言里,车队用结构体DList表示,车头用head变量表示,车位用tail变量表示,车队长度就用size来表示,把这些表示合起来:

type DList struct {

size uint64

head *DNode

tail *DNode

}

通过找到其中一个节点,就可以通过prev 或 next指向得到指向的数据。

4. Go自定义实现链表

bc28a592a758c8ad59285e2e5b947f5a.png

1.初始化Init

双向链表的初始化,可以理解成大卫哥准备买一个车队准备运煤。第一步,得获得国家有关部门的批准,有了批准大卫哥就可以买车厢运煤了。但是,批准下来的时候,大卫哥的车队啥都没有,没有车头、车尾,连一节车厢也没有。Go语言代码实现:

package main

//节点数据结构

type DNode struct {

data interface{}

prev *DNode

next *DNode

}

// 链表数据结构

type DList struct {

size uint64

head *DNode

tail *DNode

}

// 链表的初始化

func InitList() (list *DList) {

list = *(DList)

list.size = 0

list.head = nil

list.tail = nil

return

}

// 新增数据

func (dlist *DList) Append(data interface{}) {

// 创建一个节点

newNode := &DNode{data: data}

if (*dlist).GetSize() == 0 { //只有一个节点

(*dlist).head = newNode

(*dlist).tail = newNode // 头尾节点都是自己

(*newNode).prev = nil // 但是头尾的指向是nil

(*newNode).next = nil

} else { // 接到尾部

// 新节点的指向修改

(*newNode).prev = (*dlist).tail

(*newNode).next = nil

// 之前尾节点的指向修改

(*(*dlist).tail).next = newNode // 将之前的尾节点的next指向新增节点

// 更新链表的尾节点

(*dlist).tail = newNode

}

// 更新链表的节点计数

(*dlist).size++

}

// 在节点后面插入数据InsertNext

//param

//- ele 所要插入的位置

//- data 所要插入的节点数据

//

func (dlist *DList) InsertNext(ele *DNode, data interface{}) bool {

if ele == nil {

return false

}

if dlist.isTail(ele) { //正好在尾部

dlist.Append(data)

} else { // 在中间插入

//构造新节点

newNode := new(DNode)

(*newNode).data = data

(*newNode).prev = ele // 上一个节点就是ele

(*newNode).next = (*ele).next // 下一个节点就是ele原来的下一个节点

// ele的下一个指向新节点

(*ele).next = newNode

// ele之前下节点的prev重新指向新节点

*((*newNode).next).prev = newNode

// 更新链表计数

(*dlist).size++

}

}

//*====

节点之前插入接口都是类似的方式:

1. 首先根据数据创建新节点, 并设置指向

2. 更新位置节点的指向数据

3. 更新链表head , tail ,size数据

删除节点:

1. 首先获取要删除节点指向数据

(验证头尾)

2. 更新要删除节点的prev节点的next为要删除节点的next节点( 有点乱啊!!)

3. 更新链表数据

记得return要删除的节点数据(否则数据丢失)

查找节点:

type MatchFun func (data1 interface{}, data2 interface{}) int

func (dList *DList) Search(data Object, yourMatch MatchFun) *DNode

*=====*//

// 获取链表长度GetSize

func (dList *DList) GetSize() uint64 {

return (*dList).size

}

//获取头部节点GetHead

func (dList *DList) GetHead() *DNode {

return (*dList).head

}

//获取尾部节点GetTail

func (dList *DList) GetTail() *DNode {

return (*dList).tail

}

通过自己实现链表,来更深入了解链表的结构后, 我们使用go的 container/list 库实现。

5.Go库container/list 实现链表操作

关于库的成员函数,我就不一一列举了, 看一看文档很详细,也很简单。

下面直接上案例:

func main() {

link := list.New()

// 循环插入到头部

for i := 0; i <= 10; i++ {

link.PushBack(i)

}

// 遍历链表

for p := link.Front(); p != link.Back(); p = p.Next() {

fmt.Println("Number", p.Value)

}

}

6. slice和list的性能比较

1. 创建、 添加元素的比较

package main

import (

"container/list"

"fmt"

"time"

)

func main(){

T1()

}

func T1() {

t := time.Now()

//1亿创建添加测试

// slice 创建

slice := make([]int, 10)

for i := 0; i < 1*100000*1000; i++ {

slice = append(slice, i)

}

fmt.Println("slice 创建成功:", time.Now().Sub(t).String())

// list创建添加

t = time.Now()

l := list.New()

for i := 0; i < 1*100000*1000; i++ {

l.PushBack(i)

}

fmt.Println("list创建成功:", time.Now().Sub(t).String())

}

func T2() {

sli := make([]int, 10)

for i := 0; i < 1*100000*1000; i++ {

sli = append(sli, 1)

}

l := list.New()

for i := 0; i < 1*100000*1000; i++ {

l.PushBack(1)

}

// 比较遍历

t := time.Now()

for _, _ = range sli {

//fmt.Printf("values[%d]=%d\n", i, item)

}

fmt.Println("遍历slice的速度:" + time.Now().Sub(t).String())

t = time.Now()

for e := l.Front(); e != nil; e = e.Next() {

//fmt.Println(e.Value)

}

fmt.Println("遍历list的速度:" + time.Now().Sub(t).String())

}

func T3() {

sli := make([]int, 10)

for i := 0; i < 1*100000*1000; i++ {

sli = append(sli, 1)

}

l := list.New()

for i := 0; i < 1*100000*1000; i++ {

l.PushBack(1)

}

//比较插入

t := time.Now()

slif := sli[:100000*500]

slib := sli[100000*500:]

slif = append(slif, 10)

slif = append(slif, slib...)

fmt.Println("slice 的插入速度" + time.Now().Sub(t).String())

var em *list.Element

len := l.Len()

var i int

for e := l.Front(); e != nil; e = e.Next() {

i++

if i == len/2 {

em = e

break

}

}

//忽略掉找中间元素的速度。

t = time.Now()

ef := l.PushBack(2)

l.MoveBefore(ef, em)

fmt.Println("list: " + time.Now().Sub(t).String())

}

简单的测试下,如果频繁的插入和删除建议用list,频繁的遍历查询选slice。

由于container/list不是并发安全的,所以需要自己手动添加一层并发的包装。