algorithm-3:链表

35 阅读4分钟

问题:实现一个LRU缓存淘汰算法

链表

单向链表

内存存储结构(单向链表)

  1. 内存空间非连续,每个元素节点,除实际数据之外,额外添加一个指针部分,用于指向下一个节点的地址,最后一个节点的指针,指向为null,整个链表靠头指针,指向第一个节点,从而能够“顺藤摸瓜” 遍历到所有节点

image.png image.png

插入、删除复杂度

链表的插入、删除,并不需要大量迁移数据,删除时:只需要断掉要删除节点和前后的连接,然后将它的前后连接,建立起连接即可,插入时:也只需要在要插入的地方建立起和前、后节点的连接即可!都是O(1)

image.png

注意这里仅仅讨论的是,插入操作、删除操作本身,如果考虑到寻找到要插入、删除的位置,那复杂度还是O(n)

随机访问的不便

由于内存存储特性,无法像数组一样,直接计算第k个元素的地址,然后直接访问链表访问第k个元素,就需要从第一个节点遍历,直到找到位置k,O(n)复杂度

其他形式链表

双向链表

image.png

特点:

  1. 有前后2个指针,分别指向prev前一个和next后一个节点!

带来的好处(删除操作)

  1. 可以双向遍历(显然)
  2. 该特性使得插入、删除操作本身,比单向链表更高效(虽然单向的插入、删除,已经是O(1)了)

考虑以下2种业务场景:

  1. 删除链表中,值等于某个值的,节点
  2. 删除给定指针的,节点

针对场景1

  1. 无论单向、还是双向链表,都需要从头遍历,每次比较,直到找到相等的值,然后执行删除操作!
  2. 查找复杂度为O(n),删除为O(1),单向、双向没啥区别!

针对场景2:(有意思了)

  1. 前提:我直接就拿到了currentNode,要删除节点的的指针;
  2. 如果是单向链表:我可以“顺藤摸瓜”,从该节点直到最后一个节点,但是!!删除操作,是需要前一个节点的指针重新指向后一个节点的,这里,我只能通过从头遍历,直到prevNode.next = currentNode,来找到prevNode,然后才能进行删除操作,遍历时间复杂度为O(n)
  3. 如果是双向链表:currentNode,就有个前指针prev,指向了前一个节点,那么直接就开删了,时间复杂度为O(1)

带来的好处(插入操作)

同理情况如果是要在某指定节点的前插入节点,双向也比单向链表要快

总结:双向链表更常用(在内存相对不紧张的情况下,用空间换时间)

有序-双向链表

一个有序的-双向链表,当按值查询时,会比同样有序的-单向链表要平均,少查询一半的数据,为什么?

因为:单向链表,比较值进行查询只能从头开始,直到尾部,为O(n)双向链表,可以借助一个指针,用来保存上次查到值的节点位置p,下次查询时,先和p比较,根据比较的大小结果,可以选择向后查,还是向前查(利用有序特性)虽然也是O(n)但是数量上平均就只需要查询一半!

循环链表

在单向链表基础上,尾部节点的指针,不是指向null,而是指向第一个节点,适用于:环形结构的数据

image.png

双向循环链表

image.png

数组vs链表(优缺点)

数组优缺点:

  1. 支持随机访问(按下标),为O(1)
  2. 可以利用cpu的预读机制,把连续数组内存块读入cpu缓存,链表不可以

cpu有预读机制,一次读一块内存,所以有机会把数组的一部分一起读到缓存中,加速访问,但链表由于是分散在内存中的,就享受不到这待遇了

  1. 插入、删除,需要移动数据,以保内存连续,为O(n)
  2. 如何优化第二点:删除时,先标记删除,累积到一定程序再一起移动数据
  3. 申请数组就需要指定大小,如果申请过大,系统没有足够大的连续内存给它,就会报错out of memory!
  4. 数组本身不支持动态扩容

链表优缺点

  1. 支持动态扩容
  2. 占用更多内存
  3. 不支持随机访问,(按下标)是需要遍历的O(n)
  4. 插入、删除本身,是O(1)的,但在这之前的查询定位节点的操作是O(n)(前提:是按照给定值定位)
  5. 如果是给定节点指针:
    1. 单向链表的插入、删除,需要先找到前一个节点,该过程需要O(n)
    2. 双向链表的插入、删除,不需要找前一个节点,直接就有,该过程只需要O(1)了

解决:实现LRU缓存淘汰算法(链表实现)

练习:判断单链表存储的字符串是否是回文字符串