定义
链表通常跟数组对比来讲,最大不同从底层的存储结构上:数组需要连续的内存空间,对内存的要求比较高。而链表恰恰相反,它并不需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来。
链表种类
单链表
头节点
尾节点,尾节点的next指针指向null。
双向链表
每个节点相比单链表都多存储了一个前驱指针,比较耗费内存。但是比单链表更便捷的插入和删除操作(单项链表插入和删除需要遍历两次链表,一次找到对应的值,第二次找到前驱指针,然后才能做插入或者删除操作。而双向链表,每个节点保持的前驱节点指针,直接就能找到前驱节点,更加高效),利用了空间换时间的设计思想。
循环链表
尾节点的next指针指向头节点。
双向循环链表
涉及设计思想
空间换时间,如缓存设计
时间换空间
特点
- 链表跟数组一样,支持查找,插入和删除。但是由于其存储结构不像数组那样要求连续内存,本身就是零散存储,所以插入和删除不需要维护连续性,非常快速,时间复杂度为O(1)。
- 数组内存连续存储,借助CPU缓存机制,预读数组中的数据,所以访问更加高效。而链表分散存储,对cpu缓存机制并不友好。
- 数组动态扩容比较耗时,而链表本身没有大小限制,天然支持动态扩容。
- 数组仅存储数组区域,链表需要多存储指向指针,所以耗费的存储空间会翻倍。链表比数组更加多占用存储空间。对于空间很有要求的场景,适合选用数组。
缺点
不支持随机访问K个元素。因为地址不是连续存储,不像数组那样可以通过寻址公式(假设二维数组维度为m*n,则a[i][j]_adress = base_adress+(i*n+j)*type_size)直接定位到指定内存地址的值,需要通过首结点依次遍历,直到找到相应的节点。所以随机访问并不高效,时间复杂度O(n)。
时间复杂度
链表 | 数组 | |
插入,删除 | O(1) | O(n) |
随机访问 | O(n) | O(1) |
应用
LRU缓存淘汰策略
维护一个有序单链表,越靠近队尾位置,是越早被访问的数据。当有访问一个数据时,先遍历链表:
- 此链表存在链表中时,直接删除此节点,并把此节点移动到队首。
- 当此链表不存在此节点时:
- 此链表缓存未满:直接将此数组插入到链表队首。
- 此链表缓存已满:直接删除掉队尾结点,把新数据插入到队首结点。
单链表实现
package main
import (
"fmt"
)
type LinkNode struct {
next *LinkNode
value interface{}
}
type LinkedList struct {
head *LinkNode
length uint
}
func NewLinkNode(v interface{}) *LinkNode {
return &LinkNode{
next: nil,
value: v,
}
}
func (linkNode *LinkNode) GetNext() *LinkNode {
return linkNode.next
}
func (linkNode *LinkNode) GetValue() interface{} {
return linkNode.value
}
func NewLinkedList() *LinkedList {
return &LinkedList{
head: NewLinkNode(0),
length: 0,
}
}
//在某个节点后面插入节点
func (this *LinkedList) InsertAfter(p *LinkNode, v interface{}) bool {
if p == nil {
return false
}
newNode