应用:LRU缓存淘汰算法
链表也是一种线性表。
链表的内存结构是不连续的内存空间,将一组不连续的内存空间串联起来,从而进行数据存储的数据结构。
链表中每个内存块儿被称为节点Node, 节点除了存储数据外,还需要记录链表上指向下一个节点的地址,即后继指针next.
链表特点
1.插入、删除数据效率O(1)级别(只需要改指针).
随机访问效率O(n)级别。(需要从链表头到链尾进行遍历)
2.和数组比,内存空间消耗更大,因为每个存储数据的节点需要额外的空间存储后继指针。
常用的链表:单链表,循环链表,双链表
1.单链表:
(1)每个节点只包含一个指针,即后继指针。
(2)有两个特殊的节点,首节点和尾节点。用首节点的地址表示链表的地址,尾节点的后继指针为NULL.
(3)性能特点:插入、删除时间复杂度 O(1),查找时间复杂度O(n).
2.循环链表
(1)除了尾节点的后继指针指向首节点外,均与单链表一致。
(2)适用于存储有循环特点的数据,如约瑟夫问题。
3.双向链表
(1)节点除了存储数据外,还有两个指针分别指向前一个节点地址和后一个节点地址。
(2)首节点的前驱指针和尾节点的后继指针均指向NULL.
(3) 和单链表比,存储相同的数据,需要更多的存储空间。插入、删除操作比单链表效率高。
数组和链表对比
1.数组缺点
(1)若申请内存空间很大,比如100M,但内存空间没有100M的连续空间,则申请失败,即使内存可用空间大于100M.
(2)大小固定,若存储空间不足,则需要进行扩容,一旦扩容就需要数据复制,这是非常耗时的。
2.链表缺点
(1)内存空间消耗更大,因为需要额外的空间存储指针信息。
(2)对链表频繁的插入和删除操作,会导致频繁的内存申请和释放,容易造成内存碎片。
cpu缓存机制
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。
CPU在从内存读取数据的时候,会先把读取到的数据加载到CPU的缓存中。而CPU每次从内存读取数据并不是只读取那个特定要访问的地址,而是读取一个数据块(这个大小我不太确定。。)并保存到CPU缓存中,然后下次访问内存数据的时候就会先从CPU缓存开始查找,如果找到就不需要再从内存中取。这样就实现了比内存访问速度更快的机制,也就是CPU缓存存在的意义:为了弥补内存访问速度过慢与CPU执行速度快之间的差异而引入。
对于数组来说,存储空间是连续的,所以在加载某个下标的时候可以把以后的几个下标元素也加载到CPU缓存这样执行速度会快于存储空间不连续的链表存储。
链表写作技巧
1.理解指针或引用的含义
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
p->next=q。这行代码是说,p 结点中的 next 指针存储了 q 结点的内存地址。
p->next=p->next->next。这行代码表示,p结点的 next 指针存储了 p 结点的下下一个结点的内存地址。
变量是存储值的,每一个变量都有地址。
指针的值是一个变量的地址。
使用指针,可以在无需知道变量名的情况下,间接读取或更新变量的值。
go语言的方式:
声明变量 var x int 表达式&x(x的地址)获取一个指向整型变量的指针,它的类型是整型指针(*int)。
如果指针的值叫做p,我们说p指向x, 或p包含x的地址。
p指向的变量写成*p, 表达式*p表示获取变量的值,一个整型。
*p代表一个变量,可以出现在赋值操作符左边,用于更新变量的值。
x := 1
p := &x //p是一个指向变量x地址的指针
fmt.Println(*p) //*p,获取指针p指向的变量的值,即x的值
*p = 2 //即x = 2, 通过指针p改变变量x的值
fmt.Println(x) //输出2
结构体的成员或数组的元素都是变量。
指针类型的零值是nil,测试 p != nil,结果true,说明p指向一个变量。
两个指针当前仅当指向同一个变量或者两者都是nil,才相等。
2.警惕指针丢失和内存泄露
如在链表中插入新节点的操作顺序
x->next = p->next
p->next = x
3.利用哨兵简化实现难度
4.重点留意边界条件处理
如果链表为空时,代码是否能正常工作?
如果链表只包含一个结点时,代码是否能正常工作?
如果链表只包含两个结点时,代码是否能正常工作?
代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
5.学会画图
画流程图,把逻辑画在图上
6.多写多练
单链表反转
链表中环的检测
两个有序的链表合并
删除链表倒数第 n 个结点
求链表的中间结点