文章目录
前言
数组需要一块连续的内存空间来存储,对内存的要求很高,而链表通过指针将一组零散的内存块串联起来使用,其中内存块称为链表的结点,每个链表的结点除了存储数据,还需要记录链上的下一个结点的地址,记录该地址的指针叫作后继指针next,如图1所示。链表结构五花八门,三种最常见的链表:单链表、双向链表以及循环链表。
一、常用链表
1.单链表
图1所示即为单链表,第一个节点称头节点,最后一个节点称尾节点。尾节点特殊在于:指针不是指向下一个节点,而是指向一个空地址NULL。
2.双向链表
图2所示即为双向链表,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点。从图2中可以看出,双向链表需要额外的两个空间来存储后继节点和前驱结点。如果存储同样数量的数据,双向链表比单向链表占用更多的内存空间。
它支持双向遍历,可以支持时间复杂度为O(1)的情况下找到前驱结点,对于删除给定指针指向的结点来说,我们找到了要删除的结点,但是删除某个结点q之前需要知道其前驱结点,而单链表不能直接获得其前驱结点,所以为了找到前驱结点,我们还是要从头结点开始便利,知道p->next = q,说明p是q的前驱结点,对于这种情况,单链表删除操作需要O(n)的时间复杂度,而双向链表只需要O(1)的时间复杂度。
除了插入、删除操作有优势外,对于一个有序的链表,双向链表的按值查询效率也比单向链表高一些。因为,我们可以记录上次的查找位置p,每次查询时,根据要查找的值与p的大小关系,决定往前还是往后,平均只需查找一半的数据。
3.循环链表
图3所示即为循环链表(一种特殊的链表),与单链表的唯一区别就在与尾节点(指针指向头结点),优点是从链头到链位比较方便,当处理的数据具有环形结构特点时,特别适合采用循环链表。
二、链表VS数组
数组和链表,它们插入、删除以及随机访问操作的时间复杂度正好相反(链表的插入删除O(1),随机访问O(n))。但实际开发并不能局限于用复杂度分析来决定使用哪个数据结构,还要考虑实际使用过程的效率以及内存。
三、如何基于链表实现LRU缓存淘汰算法
1.常见的三种缓存淘汰策略
(1)先进先出策略(First In,First Out - FIFO)
(2)最少使用测类(Least Frequently Used - LFU)
(3)最近最少使用策略(Least Recently Used - LRU)
2.用链表实现LRU缓存淘汰法
维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新数据被访问时,从链表头开始顺序遍历链表。
(1)如果此数据之前已经缓存在链表中,遍历得到该数据对应的结点,将其从原来的位置删除,然后插入到链表的头部;
(2)如果此数据并未缓存在链表中,又可以分为两种情况:1)缓存未满,将此节点直接插入到链表的头部;2)缓存已满,删除尾节点,将新的数据结点插入链表的头部。
3.用数组实现LRU缓存淘汰策略
也是维护一个有序数组,越靠近头部(下标越小)的元素是越早之前访问的。当有一个新数据被访问时,从数组尾部开始遍历。
(1)数组未满时:若存在,则直接复制到尾部(不删除,避免每次访问删除,数组插入删除很浪费时间);若不存在,则直接新增到尾部。时间复杂度:O(n)
(2)数组已满时:删除第一个,新增到尾部。时间复杂度:O(1)
四、判断一个字符串是否是回文字符串
首先,回文字符串:正读和反读都一样的字符串。(刚开始学习,对快慢指针(找环、找中间结点等)的使用并不了解)使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进两步。在慢指针前进的过程中,同时修改起next指针,使得链表前半部分反序。最后比较中点两侧的链表是否相等。时间复杂度:O(n),空间复杂度O(1)(空间复杂度的计算是看额外的内存消耗,不是看链表本身需要多少存储空间)
总结
与数组一样,链表也支持数据查找、插入和删除操作。对于数据查找(随机访问):数组数据连续存储,根据首地址和下表,通过寻址公式能直接计算出对应的内存地址,而链表需要根据指针一个节点一个节点地一次便利,知道找到相应的结点;对于插入、删除操作:数组为了保持内存数据的连续性,需要做大量的数据搬移,所以时间复杂度是O(n),而链表的存储空间本身就不是连续的,不需要为了保持内存的连续性而搬移结点,所以链表的插入和删除很快。
记住空间换时间的设计思想。将循环链表和双向链表整合到一起就是双向循环链表。