链表 (List)
问题向导:
- 问题1:如何实现LRU缓存淘汰算法?
- 思路:
- 维护一个有序的单链表,越靠近链表尾部的结点是越早之前访问的,当有一个新的数据被访问时,从链表头开始顺序遍历链表。
- 实现:
- 如果在此数据之前已经被缓存在链表中了,遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
- 如果此数据没有在缓存链表中,可以分两种情况考虑:
- 如果此时缓存未满,则将该结点直接插入到链表的头部。
- 如果此时缓存已满,则删除链表的尾结点,将新的数据结点插入到链表的头部。
- 优化实现:
- 引入散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到O(1).
- 其他实现:
- 使用数组实现LRU缓存淘汰策略
扩展1:
- 缓存利用的是空间换时间的设计思想
- 对于执行较慢的程序,可以通过消耗更多的内存(空间换时间)来进行优化。
- 对于消耗过多内存的程序,可以通过消耗更多时间(时间换空间)来降低内存的消耗。 扩展2:
- 常见缓存淘汰策略
- FIFO(First In, First Out):先进先出策略
- LFU(Least Frequently Used): 最少使用策略
- LRU(Least Recently Used): 最少使用策略
链表概述:
概念:
- 链表是通过指针将一组零散的内存快串联在一起的一种数据结构。
- 内存块通常被称为链表的结点,每个链表的结点除了存储数据之外,还需要记录下一个节点的地址的指针(通常称为后继指针next)
对比:
- 数组:需要一块连续的内存空间来存储,对内存要求比较高
- 链表:不需要一块连续的内存空间,它通过指针将一组零散的内存块串联使用。
特点:
- 支持数据的查找、插入、删除操作
- 相对与数组,链表更适合插入和删除操作、查找的效率则没有数组高。
- 插入删除
- 数组O(n)
- 链表O(1)
- 随机访问
- 数组O(1)
- 链表O(n)
分类:
- 单链表
- 循环链表
- 双向链表
- 双 向循环链表
链表详解:
单链表:
- 特点:
- 只有一个方向
- 结点只有一个后继指针next指向后的结点
- 头结点记录链表的基地址
- 尾结点指针指向一个空地址NULL
- 常用操作的时间复杂度:
- 查找: O(n)
- 插入 : O(1)
- 删除: O(1)
循环链表:
- 特点:
- 是一种特殊的单链表
- 和单链表唯一的区别在我尾结点,尾结点指针指向连链表的头结点
- 和单链表相比,优点是从链尾到链头比较方便,当要处理的数据具有环形结构特点时,就适合采用循环链表(单链表实现相对比较繁琐)。比如:约瑟夫问题。
双向链表:
- 特点
- 支持两个方向
- 每个结点不止有一个后继指针next指向后面的结点,还有前驱指针pre指向前面的结点
- 相较于单链表,会占用更多 的内存。
- 支持双向遍历,这样也带来了 双向链表操作的灵活性。提高的数据插入和删除的效率。
- 操作
- 删除的情况分析:
- 删除结点中“值等于某个给定值”的结点;
- 需要从链表头部依次遍历,知道找到值等于给定值的结点,O(n),然后执行删除操作O(1),所以时间复杂度为O(n)
- 删除给定指针指向的结点;
- 此情况需要知道该结点的前驱结点,而单链表不支持获取前驱结点,故需要从头开始遍历O(n),直到到 p->next=q,说明 p 是 q 的前驱结点。与之对比,使用双向链表操作比较高效,时间复杂度为O(1)。
- 删除结点中“值等于某个给定值”的结点;
- 删除的情况分析:
双向循环链表
- 整合了循环链表和双向链表的特点形成了一种链表
- 首节点的前驱指针指向尾结点,尾结点的后驱指针指向首节点。
链表代码编写技巧指南
技巧一:理解指针或引用的含义
- 指针的理解:
- 将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
- eg:
- p->next=q。这行代码是说,p结点中的next指针存储了q结点的内存地址。
- p->next=p->next->next。这行代码表示,p结点的next指针存储了p结点的下下一个结点的内存地址。
技巧二:警惕指针丢失和内存泄露
- eg: 单链表的插入操作
p->next = x; // 将 p 的 next 指针指向 x 结点;
x->next = p->next; // 将 x 的结点的 next 指针指向 b 结点;
- 分析:
- p->next 指针在完成第一步操作之后,已经不再指向结点 b 了,而 是指向结点 x。第 2 行代码相当于将 x 赋值给 x->next,自己指向自己。因此,整个链表也就 断成了两半,从结点 b 往后的所有结点都无法访问到了
- 正确做法:
- 先将结点 x 的 next 指针指向结点 b,再把结点 a 的 next 指针指向结点 x,这样才不会丢失指针,导致内存泄漏。所以,对于刚刚的插入代码,我们只需要把第 1 行和第 2 行代码的顺序颠倒一下就可以了。