什么是链表
链表是指将一组零散的内存块
串联在一起,我们把内存块称之为结点
,每个链表的结点除了存储数据外还要记录链上的上一个或下一个结点的地址。我们把记录下个结点地址的叫做后继指针next
,把上个结点称为前驱指针prev
。
相比数组,链表是一种稍微复杂一点儿的数据结构。
链表的分类
链表的种类非常多,最常见的有三种链表结构,分表为:单向链表、双向链表和循环链表
单链表
顾名思义就是指链表上只有一个方向,结点只有一个后继结点next
指向后面的结点,但链表的尾结点指向一个空地址NULL
,表示这是链表上的最后一个结点。
循环链表
和单链表相比,循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表。
双向链表
顾名思义,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。
所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
双向循环链表
链表的时间复杂度分析
查找、插入、删除的时间复杂度分表为
单链表:查找O(n), 插入/删除O(1)
循环链表:查找O(n), 插入/删除O(1)
双向链表:查找O(n), 插入/删除O(1)
例如:删除操作
单链表和双向链表的删除的时间复杂度实际上是不一样的,在实际的软件开发中,从链表中删除一个数据无外乎这两种情况:
- 删除结点中“值等于某个给定值”的结点;
- 删除给定指针指向的结点。
第一种情况:不管是单链表还是双向链表,都需要从头结点开始查找到元素才能进行删除,查找的时间复杂度为O(n),删除的时间复杂度为O(1),所以删除的总的时间复杂度为O(n)。
第二种情况:知道了要删除的结点,但是删除后需要知道前驱结点
,并要使前驱指针
指向删除结点的 后继结点
,但是单向链表只有后继指针next,无法直接知道前驱结点,所以需要从头结点开始遍历链表,直到p->next=q,说明q的前驱结点是p。
但是对于双向链表就非常有优势了,因为双向链表中的结点已经保存了前驱结点的指针,不需要像单向链表那样从头结点遍历。综上,单链表的时间复杂度是O(n),双向链表的时间复杂度是O(1)。
空间换时间or时间换空间
当内存空间充足的时候,如果我们更加追求代码的执行速度,我们就可以选择空间复杂度相对较高、但时间复杂度相对很低的算法或者数据结构。相反,如果内存比较紧缺,比如代码跑在手机或者单片机上,这个时候,就要反过来用时间换空间的设计思路。
缓存即就是空间换时间的典型例子。
与数组的区别
从底层的存储结构来看,数组需要一块连续的内存空间来存储,若内存中没有连续的或足够的存储空间时会申请失败。
而链表则只要有足够的存储空间即可,即不会有内存连续性的限制,即使是零散的内存块也不会有任何问题。
链表 VS 数组性能大比拼
常见的淘汰策略
经典的链表应用场景: LRU 缓存淘汰算法
缓存是提高数据读取性能的技术,例如CPU缓存,数据库缓存,浏览器缓存等。
常见的策略有三种:
FIFO(First In,First Out):先进先出策略
LFU(Least Frequently Used):最少使用策略
LRU(Least Recently Used):最近最少使用策略
如何基于链表实现LRU缓存淘汰算法?
维持一个有序的单链表,越靠近尾结点即越早访问的。当有一个数据被访问后,先遍历一遍链表
- 如果元素已经在链表中了,那删除这个结点,并将这个元素插入到头结点的位置
- 如果元素不在链表中,判断链表是否已满
如果达到最大数,则删除尾结点的元素,并将元素插入在头结点
如果未达到最大数,则直接插入到头结点
这个算法的时间复杂度为O(n)
优化
使用HashMap和链表来实现,链表用于存储我们缓存的节点,Map存储所有的节点,当需要淘汰一个节点的时候,只需要删除链表的尾部节点同时删除Map中的数据。
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 当链表满的时候,将链表尾部的数据丢弃
有了链表为什么还需要Map?空间换时间的思想。
-
在get缓存的时候从Map中获取的时候基本上时间复杂度控制在O(1),如果从链表中一次遍历的话时间复杂度是O(n)
-
访问一个已经存在的节点时候,需要将这个节点移动到header节点后,这个时候需要在链表中删除这个节点,并重新在header后面新增一个节点。这个时候先去Map中获取这个节点,删除节点关系,避免了从链表中遍历,将时间复杂度从O(n)减少为O(1)
如何基于数组实现LRU缓存淘汰算法?
维持一个固定大小的数组,越早访问则会在数组的尾部,每次有新元素,则先遍历一遍
- 如果数组中没有找到相同的元素
如果数组没满,则将新元素放到头部
满了,则将尾部元素移除,将新元素放到头部 - 如果找到了相同的元素,则将原来的移除掉,然后并放到头部
参考:https://blog.youkuaiyun.com/qq_20003891/article/details/89374863
快速排序算法(挖坑法+左右指针法+快慢指针法):https://blog.youkuaiyun.com/mabiao6822/article/details/82631690