什么是缓存
缓存是一种提高数据读取性能的技术,在硬件和软件中都有很多应用。比如常见的CPU缓存、数据库缓存、浏览器缓存
缓存淘汰机制:缓存的大小是有限制的,当缓存满了后哪些数据需要被清除 哪里需要被保留,这些都需要缓存淘汰策略来决定。一般分为 先进先出策略 FIFO、最少使用策略 LFU、最近最少使用 LRU
链表是什么
链表(Linked List)是一种稍微复杂一点的 数据结构。通过 指针 把零散的内存串联起来,其中内存块一般称为 结点。为了把内存串联起来,每个结点存储数据外还需要存储下一个结点的地址。
它和数组的区别是,数组需要一组 连续的内存空间 来存储,对内存要求比较高。比如我们需要一块100M的内存空间,假如内存还有足够剩余空间但是没有连续的100M内存空间了,这个时候就会申请失败。
而链表和数组相反,它不需要一块连续的内存空间,它通过指针将一组 零散的内存空间 串联起来,最后拼起来内存空间总和足够即可。
单链表
单链表是一种最简单的链表 结构如下图
其中有2个结点比较特殊,分别是第一个结点和最后一个结点。一般习惯把第一个结点叫做 头结点,最后一个结点叫做尾结点
头结点用来记录链表的基地址,尾结点比较特殊,他的指针不是指向下一个结点,而是指向一个 空地址NULL,表示这是链表的最后一个结点。
和数组一样,链表也支持数据的查找、插入和删除。对于链表的 插入和删除 操作速度是很快的。因为链表本身的存储空间就不是连续的,所以在插入和删除后并不需要搬移结点。只需要改变他前一个结点的指针指向后一个结点即可,所以链表在操作 插入和删除的时候时间复杂度为 O(1)。
对于链表的随机访问K元素就会比数组差很多,因为它在内存中不是连续的,无法通过固定的公式计算出内存地址,只能通过结点一个一个的遍历寻找,时间复杂度为O(1)。
循环链表
循环链表和单链表基本一样,唯一的不同在于循环链表的尾结点指向的是头结点。当处理的数据结构具有环形的特点时采用循环链表结构比较适合。
双向链表
双向链表如上图,它需要额外两个空间来存储 前一个结点 和 后一个结点,所以他会比一般的链表多占用空间。好处就是支持双向遍历
对于寻找前一个结点这种需求 复杂度为O(1)
用 删除操作 举例来说明双向链表的优势
删除链表中的一个数据有2中情况
- 给定值,删除等于该值的结点
- 删除给定指针的结点
这里要说明下其实删除本身操作很简单,只是删除一个结点,然后把前一个结点指向被删除结点的后一个结点即可,时间复杂度为O(1)。但是删除操作前需要找到该结点,也就是要遍历链表,对于遍历链表的时间复杂度都是一样的,都是O(n)。根据时间复杂度分析法,删除操作的时间复杂度变为了O(n),这是对应上面第一种给定值 删除的情况
对于第二种情况给定指针删除结点的需求,双向链表的优势就体现出来了。首先我们给定一个指针,要求删除它,这个操作很简单直接删除,但是删除还有一步操作就是把被删除结点的前一个结点指向被删除结点的后一个结点,这里假设被删除结点为B,B结点前一个结点是A,B结点后一个结点是C,删除B结点的操作是
1.删除B结点
2.更改A结点指向C结点
对于一般的链表因为不知道A结点是谁,所以只能遍历链表寻找到某个结点的下一个结点是B结点才能确定A结点是哪个,然后做删除的第二步操作。所以单向链表的删除操作时间复杂度还是O(n)。双向链表因为本身就存储了前一个结点的指针,直接一步获取到A结点,然后做删除的第二步操作,所以双向两边的删除操作时间复杂度就为O(1)。
双向链表除了添加、删除块以外,对于给定值的查询也要比单向链表块。因为每次查询我们可以记录每次查找的位置P,每次查询根据要查找的值和P的大小比较,决定是哪一半继续查询,所以平均只需要查找一半的数据。
尽管双向链表比较耗内存,但是很多好多的实现还是使用双向链表,这是典型的 空间换时间。对于执行较慢的程序,可以通过空间换时间消耗更多的内存来优化,而消耗过多内存的程序,可以通过时间换空间的思想来降低内存消耗,具体还需要视情况而定。
链表 VS 数组
链表和数组是2种不同的内存组织方式。他们的 插入、删除、随机访问操作的时间复杂度是完全相反的
操作 | 数组 | 链表 |
---|---|---|
插入、删除 | O(n) | O(1) |
随机访问 | O(1) | O(n) |
数组的优势体现在它的内存空间是连续的,可以借助CPU的缓存机制,预读数组中的数据 随机访问效率较高。它的缺点是大小固定,需要整块连续空间,如果没有足够连续内存空间可能会导致内存不足的情况。还有一种情况如果内存过小,不够用的时候需要在申请一个更大的内存把原数组的内容复制进去比较费时(举个例子 假如数组1个G不够用了,在申请一个1.5G内存,然后把1G数据复制进去…)
链表的优势体现在它没有大小限制,支持动态扩容,不足的地方就是对于随机访问效率很低,还有对于频繁的插入和删除可能会产生内存碎片。
判断链表是否有环 并找到入口点
这是一个很经典的面试问题,首先确定链表是否是闭环也就是是否是环形链表,然后找到链表的入口点
思路:
1.遍历链表,一个每次走一步,一个每次走二步,如果走的快的追上了走的慢了就说明链表存在闭环 也就是有环,追上的点是相交点
2.如果有环,从链表的起点到环入口点的距离 = 上一步的相交点到环入口点的距离
3.然后遍历链表 一个从起点走 一个从相交点走,每次都走一步,相等的时候就是环入口点
|
|
PHP模拟链表使用LRU策略
|
|
PHP模拟链表实现回文串校验
假设数据是存储在链表中的 实现回文串 形如 1 2 3 3 2 1
或者 1 2 1
这种回文串校验
|
|
PHP模拟链表实现翻转链表
|
|