Go实现双向链表

本文介绍什么是链表,常见的链表有哪些,而后介绍链表这种数据结构会在哪些地方能够用到,以及 Redis 队列是底层的实现,经过一个小实例来演示 Redis 队列有哪些功能,最后经过 Go 实现一个双向链表。node

链表

目录

  • 一、链表
    • 1.1 说明
    • 1.2 单向链表
    • 1.3 循环链表
    • 1.4 双向链表
  • 二、redis队列
    • 2.1 说明
    • 2.2 应用场景
    • 2.3 演示
  • 三、Go双向链表
    • 3.1 说明
    • 3.2 实现
  • 四、总结
  • 五、参考文献

一、链表

1.1 说明

链表

链表(Linked list)是一种常见的基础数据结构,是一种线性表,可是并不会按线性的顺序存储数据,而是在每个节点里存到下一个节点的指针(Pointer)。因为没必要须按顺序存储,链表在插入的时候能够达到O(1)的复杂度,比另外一种线性表顺序表快得多,可是查找一个节点或者访问特定编号的节点则须要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。git

链表有不少种不一样的类型:单向链表,双向链表以及循环链表。github

  • 优点:

能够克服数组链表须要预先知道数据大小的缺点,链表结构能够充分利用计算机内存空间,实现灵活的内存动态管理。链表容许插入和移除表上任意位置上的节点。golang

  • 劣势:

因为链表增长了节点指针,空间开销比较大。链表通常查找数据的时候须要从第一个节点开始每次访问下一个节点,直到访问到须要的位置,查找数据比较慢。redis

  • 用途:

经常使用于组织检索较少,而删除、添加、遍历较多的数据。数组

如:文件系统、LRU cache、Redis 列表、内存管理等。bash

1.2 单向链表

链表中最简单的一种是单向链表,数据结构

一个单向链表的节点被分红两个部分。它包含两个域,一个信息域和一个指针域。第一个部分保存或者显示关于节点的信息,第二个部分存储下一个节点的地址,而最后一个节点则指向一个空值。单向链表只可向一个方向遍历。并发

单链表有一个头节点head,指向链表在内存的首地址。链表中的每个节点的数据类型为结构体类型,节点有两个成员:整型成员(实际须要保存的数据)和指向下一个结构体类型节点的指针即下一个节点的地址(事实上,此单链表是用于存放整型数据的动态数组)。链表按此结构对各节点的访问需从链表的头找起,后续节点的地址由当前节点给出。不管在表中访问哪一个节点,都须要从链表的头开始,顺序向后查找。链表的尾节点因为无后续节点,其指针域为空,写做为NULL。app

1.3 循环链表

循环链表是与单向链表同样,是一种链式的存储结构,所不一样的是,循环链表的最后一个结点的指针是指向该循环链表的第一个结点或者表头结点,从而构成一个环形的链。

循环链表的运算与单链表的运算基本一致。所不一样的有如下几点:

一、在创建一个循环链表时,必须使其最后一个结点的指针指向表头结点,而不是像单链表那样置为NULL。

二、在判断是否到表尾时,是判断该结点链域的值是不是表头结点,当链域的值等于表头指针时,说明已到表尾。而非象单链表那样判断链域的值是否为NULL。

1.4 双向链表

双向链表

双向链表实际上是单链表的改进,当咱们对单链表进行操做时,有时你要对某个结点的直接前驱进行操做时,又必须从表头开始查找。这是由单链表结点的结构所限制的。由于单链表每一个结点只有一个存储直接后继结点地址的链域,那么能不能定义一个既有存储直接后继结点地址的链域,又有存储直接前驱结点地址的链域的这样一个双链域结点结构呢?这就是双向链表。

在双向链表中,结点除含有数据域外,还有两个链域,一个存储直接后继结点地址,通常称之为右链域(当此“链接”为最后一个“链接”时,指向空值或者空列表);一个存储直接前驱结点地址,通常称之为左链域(当此“链接”为第一个“链接”时,指向空值或者空列表)。

二、redis队列

2.1 说明

Redis 列表是简单的字符串列表,按照插入顺序排序。你能够添加一个元素到列表的头部(左边)或者尾部(右边)

Redis 列表使用两种数据结构做为底层实现:双端列表(linkedlist)、压缩列表(ziplist)

经过配置文件中(list-max-ziplist-entries、list-max-ziplist-value)来选择是哪一种实现方式

在数据量比较少的时候,使用双端链表和压缩列表性能差别不大,可是使用压缩列表更能节约内存空间

redis 链表的实现源码 redis src/adlist.h

2.2 应用场景

消息队列,秒杀项目

秒杀项目:

提早将须要的商品码信息存入 Redis 队列,在抢购的时候每一个用户都从 Redis 队列中取商品码,因为 Redis 是单线程的,同时只能有一个商品码被取出,取到商品码的用户为购买成功,并且 Redis 性能比较高,能抗住较大的用户压力。

2.3 演示

如何经过 Redis 队列中防止并发状况下商品超卖的状况。

假设:

网站有三件商品须要卖,咱们将数据存入 Redis 队列中

一、 将三个商品码(1000一、1000二、10003)存入 Redis 队列中

# 存入商品
RPUSH commodity:queue 10001 10002 10003
复制代码

二、 存入之后,查询数据是否符合预期

# 查看所有元素
LRANGE commodity:queue 0 -1

# 查看队列的长度
LLEN commodity:queue
复制代码

三、 抢购开始,获取商品码,抢到商品码的用户则能够购买(因为 Redis 是单线程的,同一个商品码只能被取一次 )

# 出队
LPOP commodity:queue
复制代码

这里了解到 Redis 列表是怎么使用的,下面就用 Go 语言实现一个双向链表来实现这些功能。

三、Go双向链表

3.1 说明

这里只是用 Go 语言实现一个双向链表,实现:查询链表的长度、链表右端插入数据、左端取数据、取指定区间的节点等功能( 相似于 Redis 列表的中的 RPUSH、LRANGE、LPOP、LLEN功能 )。

3.2 实现

golang 双向链表

  • 节点定义

双向链表有两个指针,分别指向前一个节点和后一个节点

链表表头 prev 的指针为空,链表表尾 next 的指针为空

// 链表的一个节点
type ListNode struct {
    prev  *ListNode // 前一个节点
    next  *ListNode // 后一个节点
    value string    // 数据
}

// 建立一个节点
func NewListNode(value string) (listNode *ListNode) {
    listNode = &ListNode{
        value: value,
    }

    return
}

// 当前节点的前一个节点
func (n *ListNode) Prev() (prev *ListNode) {
    prev = n.prev

    return
}

// 当前节点的前一个节点
func (n *ListNode) Next() (next *ListNode) {
    next = n.next

    return
}

// 获取节点的值
func (n *ListNode) GetValue() (value string) {
    if n == nil {

        return
    }
    value = n.value

    return
}
复制代码
  • 定义一个链表

链表为了方便操做,定义一个结构体,能够直接从表头、表尾进行访问,定义了一个属性 len ,直接能够返回链表的长度,直接查询链表的长度就不用遍历时间复杂度从 O(n) 到 O(1)。

// 链表
type List struct {
    head *ListNode // 表头节点
    tail *ListNode // 表尾节点
    len  int       // 链表的长度
}


// 建立一个空链表
func NewList() (list *List) {
    list = &List{
    }
    return
}

// 返回链表头节点
func (l *List) Head() (head *ListNode) {
    head = l.head

    return
}

// 返回链表尾节点
func (l *List) Tail() (tail *ListNode) {
    tail = l.tail

    return
}

// 返回链表长度
func (l *List) Len() (len int) {
    len = l.len

    return
}
复制代码
  • 在链表的右边插入一个元素
// 在链表的右边插入一个元素
func (l *List) RPush(value string) {

    node := NewListNode(value)

    // 链表未空的时候
    if l.Len() == 0 {
        l.head = node
        l.tail = node
    } else {
        tail := l.tail
        tail.next = node
        node.prev = tail

        l.tail = node
    }

    l.len = l.len + 1

    return
}
复制代码
  • 从链表左边取出一个节点
// 从链表左边取出一个节点
func (l *List) LPop() (node *ListNode) {

    // 数据为空
    if l.len == 0 {

        return
    }

    node = l.head

    if node.next == nil {
        // 链表未空
        l.head = nil
        l.tail = nil
    } else {
        l.head = node.next
    }
    l.len = l.len - 1

    return
}
复制代码
  • 经过索引查找节点

经过索引查找节点,若是索引是负数则从表尾开始查找。

天然数和负数索引分别经过两种方式查找节点,找到指定索引或者是链表所有查找完则查找完成。

// 经过索引查找节点
// 查不到节点则返回空
func (l *List) Index(index int) (node *ListNode) {

    // 索引为负数则表尾开始查找
    if index < 0 {
        index = (-index) - 1
        node = l.tail
        for true {
            // 未找到
            if node == nil {

                return
            }

            // 查到数据
            if index == 0 {

                return
            }

            node = node.prev
            index--
        }
    } else {
        node = l.head
        for ; index > 0 && node != nil; index-- {
            node = node.next
        }
    }

    return
}
复制代码
  • 返回指定区间的元素
// 返回指定区间的元素
func (l *List) Range(start, stop int) (nodes []*ListNode) {
    nodes = make([]*ListNode, 0)

    // 转为天然数
    if start < 0 {
        start = l.len + start
        if start < 0 {
            start = 0
        }
    }

    if stop < 0 {
        stop = l.len + stop
        if stop < 0 {
            stop = 0
        }
    }

    // 区间个数
    rangeLen := stop - start + 1
    if rangeLen < 0 {

        return
    }

    startNode := l.Index(start)
    for i := 0; i < rangeLen; i++ {
        if startNode == nil {
            break
        }

        nodes = append(nodes, startNode)
        startNode = startNode.next
    }

    return
}
复制代码

四、总结

  • 到这里关于链表的使用已经结束,介绍链表是有哪些(单向链表,双向链表以及循环链表),也介绍了链表的应用场景(Redis 列表使用的是链表做为底层实现),最后用 Go 实现了双向链表,演示了链表在 Go 语言中是怎么使用的,你们能够在项目中更具实际的状况去使用。

五、参考文献

维基百科 链表

github redis

项目地址:go 实现队列

github.com/link1st/lin…

相关文章
相关标签/搜索