咱们又常常听到列表 List
数据结构,其实这只是更宏观的统称,表示存放数据的队列。node
列表List
:存放数据,数据按顺序排列,能够依次入队和出队,有序号关系,能够取出某序号的数据。先进先出的队列 (queue)
和先进后出的栈(stack)
都是列表。你们也常常据说一种叫线性表
的数据结构,表示具备相同特性的数据元素的有限序列,实际上就是列表
的同义词。
咱们通常写算法进行数据计算,数据处理,都须要有个地方来存数据,咱们可使用封装好的数据结构List
:算法
列表的实现有顺序表示
或链式表示
。数据库
顺序表示:指的是用一组地址连续的存储单元
依次存储线性表的数据元素,称为线性表的顺序存储结构
。它以物理位置相邻
来表示线性表中数据元素间的逻辑关系,可随机存取表中任一元素。顺序表示的又叫顺序表
,也就是用数组来实现的列表。segmentfault
链式表示:指的是用一组任意的存储单元
存储线性表中的数据元素,称为线性表的链式存储结构
。它的存储单元能够是连续的,也能够是不连续的。在表示数据元素之间的逻辑关系时,除了存储其自己的信息以外,还需存储一个指示其直接后继的信息,也就是用链表来实现的列表。数组
咱们在前面已经实现过这两种表示的数据结构:先进先出的队列 (queue)
和先进后出的栈(stack)
。接下来咱们会来实现链表形式的双端列表,也叫双端队列,这个数据结构应用场景更普遍一点。在实际工程应用上,缓存数据库Redis
的列表List
基本类型就是用它来实现的。缓存
双端列表,也能够叫双端队列安全
咱们会用双向链表来实现这个数据结构:数据结构
// 双端列表,双端队列 type DoubleList struct { head *ListNode // 指向链表头部 tail *ListNode // 指向链表尾部 len int // 列表长度 lock sync.Mutex // 为了进行并发安全pop操做 } // 列表节点 type ListNode struct { pre *ListNode // 前驱节点 next *ListNode // 后驱节点 value string // 值 }
设计结构体DoubleList
指向队列头部head
和尾部tail
的指针字段,方便找到链表最前和最后的节点,而且链表节点之间是双向连接的,链表的第一个元素的前驱节点为nil
,最后一个元素的后驱节点也为nil
。如图:并发
咱们实现的双端列表和Golang
标准库container/list
中实现的不同,感兴趣的能够阅读标准库的实现。app
// 获取节点值 func (node *ListNode) GetValue() string { return node.value } // 获取节点前驱节点 func (node *ListNode) GetPre() *ListNode { return node.pre } // 获取节点后驱节点 func (node *ListNode) GetNext() *ListNode { return node.next } // 是否存在后驱节点 func (node *ListNode) HashNext() bool { return node.pre != nil } // 是否存在前驱节点 func (node *ListNode) HashPre() bool { return node.next != nil } // 是否为空节点 func (node *ListNode) IsNil() bool { return node == nil }
以上是对节点结构体ListNode
的操做,主要判断节点是否为空,有没有后驱和前驱节点,返回值等,时间复杂度都是O(1)
。
// 添加节点到链表头部的第N个元素以前,N=0表示新节点成为新的头部 func (list *DoubleList) AddNodeFromHead(n int, v string) { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过列表长度,必定找不到,panic if n > list.len { panic("index out") } // 先找出头部 node := list.head // 日后遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.next } // 新节点 newNode := new(ListNode) newNode.value = v // 若是定位到的节点为空,表示列表为空,将新节点设置为新头部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的节点,它的前驱 pre := node.pre // 若是定位到的节点前驱为nil,那么定位到的节点为链表头部,须要换头部 if pre.IsNil() { // 将新节点连接在老头部以前 newNode.next = node node.pre = newNode // 新节点成为头部 list.head = newNode } else { // 将新节点插入到定位到的节点以前 // 定位到的节点的前驱节点 pre 如今连接到新节点上 pre.next = newNode newNode.pre = pre // 定位到的节点的后驱节点 node.next 如今连接到新节点上 node.next.pre = newNode newNode.next = node.next } } // 列表长度+1 list.len = list.len + 1 }
首先加锁实现并发安全。而后判断索引是否超出列表长度:
// 索引超过列表长度,必定找不到,panic if n > list.len { panic("index out") }
若是n=0
表示新节点想成为新的链表头部,n=1
表示插入到链表头部数起第二个节点以前,新节点成为第二个节点,以此类推。
首先,找出头部:node := list.head
,而后日后面遍历,定位到索引指定的节点node
:
// 日后遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.next }
接着初始化新节点:newNode := new(ListNode)
。
定位到的节点有三种状况,咱们须要在该节点以前插入新节点:
判判定位到的节点node
是否为空,若是为空,代表列表没有元素,将新节点设置为新头部和新尾部。
不然找到定位到的节点的前驱节点:pre := node.pre
。
若是前驱节点为空:pre.IsNil()
,代表定位到的节点node
为头部,那么新节点要取代它,成为新的头部:
if pre.IsNil() { // 将新节点连接在老头部以前 newNode.next = node node.pre = newNode // 新节点成为头部 list.head = newNode }
新节点成为新的头部,须要将新节点的后驱设置为老头部:newNode.next = node
,老头部的前驱为新头部:node.pre = newNode
,而且新头部变化:list.head = newNode
。
若是定位到的节点的前驱节点不为空,代表定位到的节点node
不是头部节点,那么咱们只需将新节点连接到节点node
以前便可:
// 定位到的节点的前驱节点 pre 如今连接到新节点前 pre.next = newNode newNode.pre = pre // 定位到的节点连接到新节点以后 newNode.next = node node.pre = newNode
先将定位到的节点的前驱节点和新节点绑定,由于如今新节点插在前面了,把定位节点的前驱节点的后驱设置为新节点:pre.next = newNode
,新节点的前驱设置为定位节点的前驱节点:newNode.pre = pre
。
同时,定位到的节点如今要连接到新节点以后,因此新节点的后驱设置为:newNode.next = node
,定位到的节点的前驱设置为:node.pre = newNode
。
最后,链表长度加一。
大部分时间花在遍历位置上,若是n=0
,那么时间复杂度为O(1)
,不然为O(n)
。
// 添加节点到链表尾部的第N个元素以后,N=0表示新节点成为新的尾部 func (list *DoubleList) AddNodeFromTail(n int, v string) { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过列表长度,必定找不到,panic if n > list.len { panic("index out") } // 先找出尾部 node := list.tail // 往前遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 新节点 newNode := new(ListNode) newNode.value = v // 若是定位到的节点为空,表示列表为空,将新节点设置为新头部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的节点,它的后驱 next := node.next // 若是定位到的节点后驱为nil,那么定位到的节点为链表尾部,须要换尾部 if next.IsNil() { // 将新节点连接在老尾部以后 node.next = newNode newNode.pre = node // 新节点成为尾部 list.tail = newNode } else { // 将新节点插入到定位到的节点以后 // 新节点连接到定位到的节点以后 newNode.pre = node node.next = newNode // 定位到的节点的后驱节点连接在新节点以后 newNode.next = next next.pre = newNode } } // 列表长度+1 list.len = list.len + 1 }
操做和头部插入节点类似,自行分析。
// 从头部开始日后找,获取第N+1个位置的节点,索引从0开始。 func (list *DoubleList) IndexFromHead(n int) *ListNode { // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取头部节点 node := list.head // 日后遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.next } return node }
若是索引超出或等于列表长度,那么找不到节点,返回空。
不然从头部开始遍历,拿到节点。
时间复杂度为:O(n)
。
// 从尾部开始往前找,获取第N+1个位置的节点,索引从0开始。 func (list *DoubleList) IndexFromTail(n int) *ListNode { // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取尾部节点 node := list.tail // 往前遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.pre } return node }
操做和从头部获取节点同样,请自行分析。
// 从头部开始日后找,获取第N+1个位置的节点,并移除返回 func (list *DoubleList) PopFromHead(n int) *ListNode { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取头部 node := list.head // 日后遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.next } // 移除的节点的前驱和后驱 pre := node.pre next := node.next // 若是前驱和后驱都为nil,那么移除的节点为链表惟一节点 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是头部节点,那么下一个节点成为头节点 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部节点,那么上一个节点成为尾节点 list.tail = pre pre.next = nil } else { // 移除的是中间节点 pre.next = next next.pre = pre } // 节点减一 list.len = list.len - 1 return node }
首先加并发锁实现并发安全。先判断索引是否超出列表长度:n >= list.len
,若是超出直接返回空指针。
获取头部,而后遍历定位到第N+1
个位置的元素:node = node.next
。
定位到的并要移除的节点有三种状况发生:
查看要移除的节点的前驱和后驱:
// 移除的节点的前驱和后驱 pre := node.pre next := node.next
若是前驱和后驱都为空:pre.IsNil() && next.IsNil()
,那么要移除的节点是链表中惟一的节点,直接将列表头部和尾部置空便可。
若是前驱节点为空:pre.IsNil()
,表示移除的是头部节点,那么头部节点的下一个节点要成为新的头部:list.head = next
,而且这时新的头部前驱要设置为空:next.pre = nil
。
同理,若是后驱节点为空:next.IsNil()
,表示移除的是尾部节点,须要将尾部节点的前一个节点设置为新的尾部:list.tail = pre
,而且这时新的尾部后驱要设置为空:pre.next = nil
。
若是移除的节点处于两个节点之间,那么将这两个节点连接起来便可:
// 移除的是中间节点 pre.next = next next.pre = pre
最后,列表长度减一。
主要的耗时用在定位节点上,其余的操做都是链表连接,能够知道时间复杂度为:O(n)
。
// 从尾部开始往前找,获取第N+1个位置的节点,并移除返回 func (list *DoubleList) PopFromTail(n int) *ListNode { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取尾部 node := list.tail // 往前遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 移除的节点的前驱和后驱 pre := node.pre next := node.next // 若是前驱和后驱都为nil,那么移除的节点为链表惟一节点 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是头部节点,那么下一个节点成为头节点 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部节点,那么上一个节点成为尾节点 list.tail = pre pre.next = nil } else { // 移除的是中间节点 pre.next = next next.pre = pre } // 节点减一 list.len = list.len - 1 return node }
操做和从头部移除节点类似,请自行分析。
package main import ( "fmt" "sync" ) // 双端列表,双端队列 type DoubleList struct { head *ListNode // 指向链表头部 tail *ListNode // 指向链表尾部 len int // 列表长度 lock sync.Mutex // 为了进行并发安全pop操做 } // 列表节点 type ListNode struct { pre *ListNode // 前驱节点 next *ListNode // 后驱节点 value string // 值 } // 获取节点值 func (node *ListNode) GetValue() string { return node.value } // 获取节点前驱节点 func (node *ListNode) GetPre() *ListNode { return node.pre } // 获取节点后驱节点 func (node *ListNode) GetNext() *ListNode { return node.next } // 是否存在后驱节点 func (node *ListNode) HashNext() bool { return node.pre != nil } // 是否存在前驱节点 func (node *ListNode) HashPre() bool { return node.next != nil } // 是否为空节点 func (node *ListNode) IsNil() bool { return node == nil } // 返回列表长度 func (list *DoubleList) Len() int { return list.len } // 添加节点到链表头部的第N个元素以前,N=0表示新节点成为新的头部 func (list *DoubleList) AddNodeFromHead(n int, v string) { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过列表长度,必定找不到,panic if n > list.len { panic("index out") } // 先找出头部 node := list.head // 日后遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.next } // 新节点 newNode := new(ListNode) newNode.value = v // 若是定位到的节点为空,表示列表为空,将新节点设置为新头部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的节点,它的前驱 pre := node.pre // 若是定位到的节点前驱为nil,那么定位到的节点为链表头部,须要换头部 if pre.IsNil() { // 将新节点连接在老头部以前 newNode.next = node node.pre = newNode // 新节点成为头部 list.head = newNode } else { // 将新节点插入到定位到的节点以前 // 定位到的节点的前驱节点 pre 如今连接到新节点前 pre.next = newNode newNode.pre = pre // 定位到的节点连接到新节点以后 newNode.next = node node.pre = newNode } } // 列表长度+1 list.len = list.len + 1 } // 添加节点到链表尾部的第N个元素以后,N=0表示新节点成为新的尾部 func (list *DoubleList) AddNodeFromTail(n int, v string) { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过列表长度,必定找不到,panic if n > list.len { panic("index out") } // 先找出尾部 node := list.tail // 往前遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 新节点 newNode := new(ListNode) newNode.value = v // 若是定位到的节点为空,表示列表为空,将新节点设置为新头部和新尾部 if node.IsNil() { list.head = newNode list.tail = newNode } else { // 定位到的节点,它的后驱 next := node.next // 若是定位到的节点后驱为nil,那么定位到的节点为链表尾部,须要换尾部 if next.IsNil() { // 将新节点连接在老尾部以后 node.next = newNode newNode.pre = node // 新节点成为尾部 list.tail = newNode } else { // 将新节点插入到定位到的节点以后 // 新节点连接到定位到的节点以后 newNode.pre = node node.next = newNode // 定位到的节点的后驱节点连接在新节点以后 newNode.next = next next.pre = newNode } } // 列表长度+1 list.len = list.len + 1 } // 返回列表链表头结点 func (list *DoubleList) First() *ListNode { return list.head } // 返回列表链表尾结点 func (list *DoubleList) Last() *ListNode { return list.tail } // 从头部开始日后找,获取第N+1个位置的节点,索引从0开始。 func (list *DoubleList) IndexFromHead(n int) *ListNode { // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取头部节点 node := list.head // 日后遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.next } return node } // 从尾部开始往前找,获取第N+1个位置的节点,索引从0开始。 func (list *DoubleList) IndexFromTail(n int) *ListNode { // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取尾部节点 node := list.tail // 往前遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.pre } return node } // 从头部开始日后找,获取第N+1个位置的节点,并移除返回 func (list *DoubleList) PopFromHead(n int) *ListNode { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取头部 node := list.head // 日后遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.next } // 移除的节点的前驱和后驱 pre := node.pre next := node.next // 若是前驱和后驱都为nil,那么移除的节点为链表惟一节点 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是头部节点,那么下一个节点成为头节点 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部节点,那么上一个节点成为尾节点 list.tail = pre pre.next = nil } else { // 移除的是中间节点 pre.next = next next.pre = pre } // 节点减一 list.len = list.len - 1 return node } // 从尾部开始往前找,获取第N+1个位置的节点,并移除返回 func (list *DoubleList) PopFromTail(n int) *ListNode { // 加并发锁 list.lock.Lock() defer list.lock.Unlock() // 索引超过或等于列表长度,必定找不到,返回空指针 if n >= list.len { return nil } // 获取尾部 node := list.tail // 往前遍历拿到第 N+1 个位置的元素 for i := 1; i <= n; i++ { node = node.pre } // 移除的节点的前驱和后驱 pre := node.pre next := node.next // 若是前驱和后驱都为nil,那么移除的节点为链表惟一节点 if pre.IsNil() && next.IsNil() { list.head = nil list.tail = nil } else if pre.IsNil() { // 表示移除的是头部节点,那么下一个节点成为头节点 list.head = next next.pre = nil } else if next.IsNil() { // 表示移除的是尾部节点,那么上一个节点成为尾节点 list.tail = pre pre.next = nil } else { // 移除的是中间节点 pre.next = next next.pre = pre } // 节点减一 list.len = list.len - 1 return node } func main() { list := new(DoubleList) // 在列表头部插入新元素 list.AddNodeFromHead(0, "I") list.AddNodeFromHead(0, "love") list.AddNodeFromHead(0, "you") // 在列表尾部插入新元素 list.AddNodeFromTail(0, "may") list.AddNodeFromTail(0, "happy") // 正常遍历,比较慢 for i := 0; i < list.Len(); i++ { // 从头部开始索引 node := list.IndexFromHead(i) // 节点为空不可能,由于list.Len()使得索引不会越界 if !node.IsNil() { fmt.Println(node.GetValue()) } } fmt.Println("----------") // 正常遍历,特别快 // 先取出第一个元素 first := list.First() for !first.IsNil() { // 若是非空就一直遍历 fmt.Println(first.GetValue()) // 接着下一个节点 first = first.GetNext() } fmt.Println("----------") // 元素一个个 POP 出来 for { node := list.PopFromHead(0) if node.IsNil() { // 没有元素了,直接返回 break } fmt.Println(node.GetValue()) } fmt.Println("----------") fmt.Println("len", list.Len()) }
输出:
you love I may happy ---------- you love I may happy ---------- you love I may happy ---------- len 0
首先,先从列表头部插入三个新元素,而后从尾部插入两个新元素,而后用三种方式进行遍历,两种只是查看元素,一种是遍历移除元素。
我是陈星星,欢迎阅读我亲自写的 数据结构和算法(Golang实现),文章首发于 阅读更友好的GitBook。