问题:实现一个LRU缓存淘汰算法
链表
单向链表
内存存储结构(单向链表)
- 内存空间非连续,每个元素节点,除实际数据之外,额外添加一个指针部分,用于指向下一个节点的地址,最后一个节点的指针,指向为null,整个链表靠头指针,指向第一个节点,从而能够“顺藤摸瓜” 遍历到所有节点
插入、删除复杂度
链表的插入、删除,并不需要大量迁移数据,删除时:只需要断掉要删除节点和前后的连接,然后将它的前后连接,建立起连接即可,插入时:也只需要在要插入的地方建立起和前、后节点的连接即可!都是O(1)
注意这里仅仅讨论的是,插入操作、删除操作本身,如果考虑到寻找到要插入、删除的位置,那复杂度还是O(n)
随机访问的不便
由于内存存储特性,无法像数组一样,直接计算第k个元素的地址,然后直接访问链表访问第k个元素,就需要从第一个节点遍历,直到找到位置k,O(n)复杂度
其他形式链表
双向链表
特点:
- 有前后2个指针,分别指向prev前一个和next后一个节点!
带来的好处(删除操作)
- 可以双向遍历(显然)
- 该特性使得插入、删除操作本身,比单向链表更高效(虽然单向的插入、删除,已经是O(1)了)
考虑以下2种业务场景:
- 删除链表中,值等于某个值的,节点
- 删除给定指针的,节点
针对场景1
- 无论单向、还是双向链表,都需要从头遍历,每次比较,直到找到相等的值,然后执行删除操作!
- 查找复杂度为O(n),删除为O(1),单向、双向没啥区别!
针对场景2:(有意思了)
- 前提:我直接就拿到了currentNode,要删除节点的的指针;
- 如果是单向链表:我可以“顺藤摸瓜”,从该节点直到最后一个节点,但是!!删除操作,是需要前一个节点的指针重新指向后一个节点的,这里,我只能通过从头遍历,直到prevNode.next = currentNode,来找到prevNode,然后才能进行删除操作,遍历时间复杂度为O(n)
- 如果是双向链表:currentNode,就有个前指针prev,指向了前一个节点,那么直接就开删了,时间复杂度为O(1)
带来的好处(插入操作)
同理情况如果是要在某指定节点的前插入节点,双向也比单向链表要快
总结:双向链表更常用(在内存相对不紧张的情况下,用空间换时间)
有序-双向链表
一个有序的-双向链表,当按值查询时,会比同样有序的-单向链表要平均,少查询一半的数据,为什么?
因为:单向链表,比较值进行查询只能从头开始,直到尾部,为O(n)双向链表,可以借助一个指针,用来保存上次查到值的节点位置p,下次查询时,先和p比较,根据比较的大小结果,可以选择向后查,还是向前查(利用有序特性)虽然也是O(n)但是数量上平均就只需要查询一半!
循环链表
在单向链表基础上,尾部节点的指针,不是指向null,而是指向第一个节点,适用于:环形结构的数据
双向循环链表
数组vs链表(优缺点)
数组优缺点:
- 支持随机访问(按下标),为O(1)
- 可以利用cpu的预读机制,把连续数组内存块读入cpu缓存,链表不可以
cpu有预读机制,一次读一块内存,所以有机会把数组的一部分一起读到缓存中,加速访问,但链表由于是分散在内存中的,就享受不到这待遇了
- 插入、删除,需要移动数据,以保内存连续,为O(n)
- 如何优化第二点:删除时,先标记删除,累积到一定程序再一起移动数据
- 申请数组就需要指定大小,如果申请过大,系统没有足够大的连续内存给它,就会报错out of memory!
- 数组本身不支持动态扩容
链表优缺点
- 支持动态扩容
- 占用更多内存
- 不支持随机访问,(按下标)是需要遍历的O(n)
- 插入、删除本身,是O(1)的,但在这之前的查询定位节点的操作是O(n)(前提:是按照给定值定位)
- 如果是给定节点指针:
- 单向链表的插入、删除,需要先找到前一个节点,该过程需要O(n)
- 双向链表的插入、删除,不需要找前一个节点,直接就有,该过程只需要O(1)了