链表的定义
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。----摘自百度百科
链表的底层
可见它并不是一组连续的内存,他是靠指针将零散的内存串联起来,形成的一个完整的数据结构。
链表的一些重要概念
- 结点
- 链表中的某一个元素我们称之为结点
- 后继指针
- 如上图所示,链表中的元素不光包含数据,还应该包含一个指向下一个元素的指针,这个指针就叫做后继指针。
- 头结点
- 头结点为链表的第一个结点
- 只有通过头结点我们才可以遍历完整的链表(单向链表的情况下)
- 尾结点
- 最后一个结点称为尾结点
- 尾结点的后继指针指向一个空地址NULL
- 单向链表
- 整个链表只有一个遍历方向
- 每一个结点包含数据本身(指针),和一个后继指针
- 双向链表
- 可以通过两个方向遍历
- 每一个结点包含数据本身(指针),一个后继指针和前置指针
- 循环链表
- 尾结点指向头结点的链表
链表的特点
- 无法直接随机访问(依据下标访问)其中的任意数据
- 随机访问数据非常低效
- 高效的插入和删除
关于第一点和第二点,因为链表结构如上图所示,并不是一组连续的内存空间,因此不能通过直接的内存寻址公式计算出来,只能通过头结点依次遍历来实现访问特定元素,因此它的随机访问的时间复杂度为O(n)。
但是链表的插入和删除,因为链表中的结点都是通过指针来指示下一个元素或者上一个元素,因此插入和删除只需要简单的将插入目标的前一个结点的指针指向新的元素,新元素后继指针指向之前的下一元素即可。因此时间复杂度为O(1)。
链表的实现—伪代码篇
依据之前所说,梳理一下实现一个双向链表所需的关键内容。
-
链表这个数据结构本身,应该包含结点
-
而每一个结点,应该包含:
- 数据本身(无论是指向数据的指针或者是一个实际的值类型)
- 后继指针(指向下一个元素)
- 前置指针(指向上一个元素)
class List {
Node root //根结点,起始结点
}
class Node {
*Node next //后继指针
*Node prev //前置指针
Value Value //这里的value代表任意类型
}
以上为一段伪代码,接下来为List扩充插入和删除操作:
class List {
Node root //根结点,起始结点
//将new插入到at之后
function insert(Node new,Node at) {
//at是最后一个结点
if(at.next == null) {
at.next = &new //当前元素的下一个元素变为new,当前元素前一个元素不变
new.next = null //new元素下一个元素变为原下一个元素
new.prev = &at //new元素前一个元素变为当前元素
}else{
*Node oldNext = at.next //原下一个元素
at.next = &new //当前元素的下一个元素变为new,当前元素前一个元素不变
new.next = oldNext //new元素下一个元素变为原下一个元素
new.prev = &at //new元素前一个元素变为当前元素
oldNext.prev = &new //原下一个元素的前一个元素变为new,原下一个元素下一个元素不变
}
}
//删除指定结点
function delete(Node del) {
//del为最后一个结点
if(del.next == null) {
*Node oldPrev = del.prev //原前一个元素
oldPrev.next = null //原前一个元素的下一个元素变为原下一个元素
}elseif(del.prev == null){ //del为第一个结点
*Node oldNext = del.next //原下一个元素
oldNext.prev = null //原下一个元素的前一个元素变为原前一个元素
}else{
*Node oldNext = del.next //原下一个元素
*Node oldPrev = del.prev //原前一个元素
oldPrev.next = oldNext //原前一个元素的下一个元素变为原下一个元素
oldNext.prev = oldPrev //原下一个元素的前一个元素变为原前一个元素
}
del.next = null //置空指针,防止内存泄露
del.prev = null
}
}
上面就是链表的最基本操作。
这里需要注意的是**需要对于边界值进行特殊处理,否则如果你在null上操作指针肯定会引起FATAL错误。**至于其他比如插入xxx之前等等额外方法实现和这些没有什么太大的区别。
下面我们来看看GO,关于链表的实现。
链表的实现—GO源码篇
go语言的List在list包中。它的基本实现是我们之前的伪代码一致。
// Element is an element of a linked list.
type Element struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element
// The list to which this element belongs.
list *List
// The value stored with this element.
Value interface{}
}
// Next returns the next list element or nil.
func (e *Element) Next() *Element {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// Prev returns the previous list element or nil.
func (e *Element) Prev() *Element {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
以上是它的结点结构与方法,可以看到它一样包含了前置、后继指针,任意类型的Value,和属于哪一个list的List指针。Next和Prev只是将原本直接访问做了简单的封装,加入了一些判断逻辑。
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
root Element // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *List) Init() *List {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// New returns an initialized list.
func New() *List { return new(List).Init() }
// insert inserts e after at, increments l.len, and returns e.
func (l *List) insert(e, at *Element) *Element {
n := at.next
at.next = e
e.prev = at
e.next = n
n.prev = e
e.list = l
l.len++
return e
}
// remove removes e from its list, decrements l.len, and returns e.
func (l *List) remove(e *Element) *Element {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
return e
}
以上是它的List结构以及对应的新增和删除方法,其余方法大家可以自行查看源码。和我们的伪代码差不多,他的List结构也是包含了根结点与一个链表总长度。因为链表只能通过遍历获取长度,因此预设一个长度值将会节省很多求长度的操作。然后New()方法是go语言特有的构造函数方式,可以看到初始化的时候,会初始化一个根结点指针都指向自己的节点与长度为0的链表。
这里发现go语言并没有进行边界的特殊处理,这是因为go初始化链表的时候,将头结点的指针均指向了自己,并且不包含任何数据,也就是说链表的第一个结点只是用来做边界定义。这种链表叫做带头链表。这种定义方式可以简化我们的处理逻辑。
欢迎大家关注我的个人博客:http://blog.geek-scorpion.com/