本文为“Goalng全面深入系列”中的标准库部分。
1. 什么是双向链表
(引用)
和单链表比较,双向链表的元素不但知道自己的下线,还知道自己的上线(越来越像传销组织了)。小煤车开起来,图里面可以看出,每个车厢除了一个指向后面车厢的箭头外,还有一个指向前面车厢的箭头(车头、车尾除外)。车头只有指向后面车厢的箭头,车尾只有指向前面车厢的箭头。
2. 和单向链表相比的优势
1. 插入删除不需要移动元素外,可以原地插入删除
2. 可以双向遍历
插入数据到中间
删除中间数据
3、双向链表与Go的对应结构
1.节点分析
我们先把车厢分解开来。每节车厢都由煤炭、车体、拉前车厢绳索、拉后车厢绳索这4部分组成。车体是我们的运输工具,在Go语言里我们用结构提DNode表示;煤炭代表运的货物,用data变量表示;拉前车厢绳索和拉后车厢绳索我们分别用指针prev和next表示。这样一节车厢,用Go语言描述如下:
type DNode struct {
data Object
prev *DNode
next *DNode
}
2、双向链表
一个运煤车队就是一个双向链表。车队要有车头、车厢、车尾,作为车队的负责人还得知道车队有多长。在Go语言里,车队用结构体DList表示,车头用head变量表示,车位用tail变量表示,车队长度就用size来表示,把这些表示合起来:
type DList struct {
size uint64
head *DNode
tail *DNode
}
通过找到其中一个节点,就可以通过prev 或 next指向得到指向的数据。
4. Go自定义实现链表
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不是并发安全的,所以需要自己手动添加一层并发的包装。