一、数组和链表的优缺点
链表和数组作为算法中的两个基本数据结构,在程序设计过程中经常用到。尽管两种结构都可以用来存储一系列的数据,但又各有各的特点。
数组的优势,在于可以方便的遍历查找需要的数据。在查询数组指定位置(如查询数组中的第4个数据)的操作中,只需要进行1次操作即可,时间复杂度为O(1)。但是,这种时间上的便利性,是因为数组在内存中占用了连续的空间,在进行类似的查找或者遍历时,本质是指针在内存中的定向偏移。然而,当需要对数组成员进行添加和删除的操作时,数组内完成这类操作的时间复杂度则变成了O(n)。
链表的特性,使其在某些操作上比数组更加高效。例如当进行插入和删除操作时,链表操作的时间复杂度仅为O(1)。另外,因为链表在内存中不是连续存储的,所以可以充分利用内存中的碎片空间。
如果我们申请一个 100MB 大小的数组,当内存中没有连续的、足够大的存储空间时,即便内存的剩余总可用空间大于 100MB,仍然会申请失败。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组串联起来使用,所以如果我们申请的是 100MB 大小的链表,根本不会有问题。
底层内存模型分别为:
二、链表类型
最常见的链表类型分别为:单链表、双向链表和循环链表。
1. 单链表
链表通过指针将一组零散的内存块串联在一起。其中,我们把内存块称为链表的“结点"。为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址,称为后继指针next。
在进行数组的插入、删除操作时,为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是 O(n)。而在链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的。所以,在链表中插入和删除一个数据是非常快速的。
但是,有利就有弊。链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。
2. 循环链表
循环链表是一种特殊的单链表。
循环链表跟单链表唯一的区别就在尾结点。我们知道,单链表的尾结点指针指向空地址,表示这就是最后的结点了。而循环链表的尾结点指针是指向链表的头结点。从我画的循环链表图中,你应该可以看出来,它像一个环一样首尾相连,所以叫作“循环”链表。
和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。比如著名的约瑟夫问题。尽管用单链表也可以实现,但是用循环链表实现的话,代码就会简洁很多。
3. 双向链表
单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。而双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
三、Go语言单链表实现
package linklist
import (
"errors"
"fmt"
)
type ListNode struct {
data interface{}
next *ListNode
}
type LinkList struct {
head *ListNode
length uint
}
func NewLinkList() *LinkList {
return &LinkList{
head: &ListNode{0, nil},
length: 0,
}
}
func NewLinkNode(value interface{})*ListNode{
return &ListNode{
data:value,
next:nil,
}
}
func (this *ListNode) GetNext() *ListNode {
return this.next
}
func (this *ListNode) GetValue() interface{} {
return this.data
}
func (this *LinkList) Insert(i uint, node *ListNode) error {
if i < 0 || node == nil || i > this.length{
return errors.New("index out of range or node is nil")
}
preItem := (*this).head
for j := uint(0); j < i; j++ {
preItem = preItem.next
}
node.next = preItem.next
preItem.next = node
this.length++
return nil
}
func (this *LinkList) Delete(node *ListNode)error{
if nil == node{
return errors.New("node is nil")
}
pre := this.head
cur := this.head.next
for nil != cur{
if cur.data == node.data {
break
}
pre = cur
cur = cur.next
}
if nil == cur{
return errors.New("not find node")
}
pre.next = cur.next
node = nil
this.length--
return nil
}
func (this LinkList) FindByIndex(index uint)(*ListNode,error){
if index < 0 || index >= this.length {
return nil,errors.New("out of range")
}
preItem := this.head.next
for i := uint(0); i < index; i++ {
preItem = preItem.next
}
return preItem,nil
}
func (this LinkList)Print(){
pre := this.head.next
for nil != pre{
fmt.Printf("%v\n",pre.GetValue())
pre = pre.next
}
}