Golang标准库深入 - 双向链表(container/list)

1. 什么是双向链表

%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8.png(引用)

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

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

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

    2. 可以双向遍历

    插入数据到中间

删除中间数据

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

1.节点分析

%E8%8A%82%E7%82%B9.png

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

type DNode struct {
 data Object
 prev *DNode
 next *DNode
}

2、双向链表

%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8%E8%A1%A8%E7%A4%BA.png

一个运煤车队就是一个双向链表。车队要有车头、车厢、车尾,作为车队的负责人还得知道车队有多长。在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 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不是并发安全的,所以需要自己手动添加一层并发的包装。

猜你喜欢

转载自my.oschina.net/90design/blog/1813377