本文为“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) (data {}) { // 创建一个节点 := &DNode{data: data}
if (*dlist).() == 0 { //只有一个节点 (*dlist).head = (*dlist).tail = // 头尾节点都是自己 (*).prev = nil // 但是头尾的指向是nil (*).next = nil } else { // 接到尾部 // 新节点的指向修改 (*).prev = (*dlist).tail (*).next = nil
// 之前尾节点的指向修改 (*(*dlist).tail).next = // 将之前的尾节点的next指向新增节点
// 更新链表的尾节点 (*dlist).tail = }
// 更新链表的节点计数 (*dlist).size++ }
// 在节点后面**数据InsertNext //param // - ele 所要**的位置 // - data 所要**的节点数据 // func (dlist *DList) InsertNext(ele *DNode, data interface{}) bool { if ele == nil { return false }
if dlist.(ele) { //正好在尾部 dlist.(data) } else { // 在中间** //构造新节点 := new(DNode) (*).data = data (*).prev = ele // 上一个节点就是ele (*).next = (*ele).next // 下一个节点就是ele原来的下一个节点
// ele的下一个指向新节点 (*ele).next =
// ele之前下节点的prev重新指向新节点 *((*).next).prev =
// 更新链表计数 (*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 = (slice, i) } fmt.("slice 创建成功:", time.Now().Sub(t).())
// list创建添加 t = time.Now() l := list.New() for i := 0; i 1*100000*1000; i++ { l.(i) } fmt.("list创建成功:", time.Now().Sub(t).()) } func T2() { sli := make([]int, 10) for i := 0; i 1*100000*1000; i++ { sli = (sli, 1) }
l := list.New() for i := 0; i 1*100000*1000; i++ { l.(1) } // 比较遍历 t := time.Now() for _, _ = range sli { //fmt.("[%d]=%dn", i, item) } fmt.("遍历slice的速度:" + time.Now().Sub(t).()) t = time.Now() for e := l.Front(); e != nil; e = e.Next() { //fmt.(e.Value) } fmt.("遍历list的速度:" + time.Now().Sub(t).()) } func T3() { sli := make([]int, 10) for i := 0; i 1*100000*1000; i++ { sli = (sli, 1) }
l := list.New() for i := 0; i 1*100000*1000; i++ { l.(1) } //比较** t := time.Now() slif := sli[:100000*500] slib := sli[100000*500:] slif = (slif, 10) slif = (slif, slib...) fmt.("slice 的**速度" + time.Now().Sub(t).())
var em *list. 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.(2) l.(ef, em) fmt.("list: " + time.Now().Sub(t).()) }
简单的测试下,如果频繁的**和删除建议用list,频繁的遍历查询选slice。
由于/list不是并发安全的,所以需要自己手动添加一层并发的包装。