#### 2、链表 链表不需要一块连续的内存空间,它通过指针将一组零散的内存块串联起来使用。其中每个内存块叫做链表的结点,记录下个结点地址的指针叫做后继指针。 #### 2.1 单链表 链表的插入和删除是非常快速的,因为不需要做数据搬移,但是链表想要随机访问第k个元素就没有数组那么高效了,因为链表的数据不是连续的,没办法将首地址和下标带入寻址公式直接计算出对应的内存地址,而是需要根据指针一个结点一个结点的一次遍历,直到找到相应的结点,需要O(n)的时间复杂度。 #### 2.2 循环链表 在单链表基础上,尾结点的指针指向链表的头结点 #### 2.3 双向链表 每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点。 * 删除和访问操作 当删除结点中“值等于某个给定值”的结点,无论单链表还是双向链表,为了查找到匹配的结点,都需要从头指针一个一个一次遍历,然后将其删除。尽管删除操作时间复杂度是O(1),但是遍历查找的时间复杂度是O(n)。 当删除给定指针指向的结点,我们已经找到了要删除的结点,需要找到他的前驱结点,单链表不支持获取前驱结点,所以,为了找到前驱结点,还是要从头开始获取前驱结点。但是对于双向链表来说,有前驱指针,所以只需要O(1)的时间复杂度就可以。 除此之外,对于一个有序链表,双向链表在按值查询时,可以记录上次查找的位置,每次查询的时候,根据查找的值判断大小,然后决定往前找还是往后找,平均只需要查找一半的数据。 * 空间换时间设计思想 在实际的开发中,尽管双向链表比较耗费内存,也比单链表应用更加广泛,比如java中的linkedHashMap,这里体现一种用空间换时间的思想,当我们内存空间充足的时候,如果我们更加追求代码的执行速度,就可以选用空间复杂度相对较高,但时间复杂度相对较低的算法或者数据结构。反过来也类似。 #### 2.4 数组和链表的缺点 * 数组的缺点是大小固定,且需要占用整块连续的内存空间,如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致out of memory,如果声明的过小,又容易产生动态扩容发现数据搬移。 * 链表的缺点是需要耗费额外的存储空间去存储指针,而且对链表的频繁插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片,如果是Java语言,就有可能频繁的GC。 #### 2.5 如何实现一个LRU缓存淘汰算法? 链表使用的经典场景,LRU(least recently userd),最少最近使用策略。 我的思路是这样的,维护一个按时间排序的有序单链表,越靠近尾部的结点是最早被访问的,当有一个新的数据被访问时,分以下两种情况: * 如果数据已经在链表中,先遍历找到这个数据对应的结点,将原来数据删除,然后把数据插入到链表的头部。 * 如果数据不在链表中,要判断链表是否已经满了,如果未满,将数据插入到链表头部,如果已满,将链表尾结点删除,再将新的数据插入链表头部。
时间复杂度分析的话,不管缓存满不满,我们都需要遍历一遍链表,所以是O(n)的时间复杂度。实际上可以引入散列表来记录每个数据的位置,将缓存访问的时间复杂度降低到O(1),在后续散列表会详细说明。