链表复习(二)

工作好辛苦!!!!

  1. 数组 链表性能比拼
  2. 不能局限于时间复杂度,在实际开发中,不能仅用复杂度分析来决定使用哪个数据结构来存储数据
  3. 数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读
  4. 数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。
  5. 这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,我觉得这也是它与数组最大的区别。
  6. 你可能会说,我们 Java 中的 ArrayList 容器,也可以支持动态扩容啊?
  7. 当我们往支持动态扩容的数组中插入一个数据时,如果数组中没有空闲空间了,就会申请一个更大的空间,将数据拷贝过去,而数据拷贝的操作是非常耗时的。
  8. 举一个稍微极端的例子。
  9. 如果我们用 ArrayList 存储了了 1GB 大小的数据,这个时候已经没有空闲空间了,当我们再插入数据的时候,ArrayList 会申请一个 1.5GB 大小的存储空间,并且把原来那 1GB 的数据拷贝到新申请的空间上。
  10. 是不是就很耗时?除此之外,如果你的代码对内存的使用非常苛刻,那数组就更适合你。
  11. 因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。
  12. 而且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片
  13. 如果是 Java 语言,就有可能会导致频繁的 GC(垃圾回收)。
  14. 如何基于链表实现 LRU 缓存淘汰算法?
  15. 我的思路是这样的:我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。
  16. 1. 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
  17. 2. 如果此数据没有在缓存链表中,又可以分为两种情况:
  18. 如果此时缓存未满,则将此结点直接插入到链表的头部;如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
  19. 这样我们就用链表实现了一个 LRU 缓存,是不是很简单?现在我们来看下 m 缓存访问的时间复杂度是多少。
  20. 因为不管缓存有没有满,我们都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。
  21. 实际上,我们可以继续优化这个实现思路,比如引入散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到 O(1)。
  22. 不过链表要比数组稍微复杂,从普通的单链表衍生出来好几种链表结构,比如双向链表、循环链表、双向循环链表。
  23. 和数组相比,链表更适合插入、删除操作频繁的场景,查询的时间复杂度较高。
  24. 不过,在具体软件开发中,要对数组和链表的各种性能进行对比,综合来选择使用两者中的哪一个。

你可能感兴趣的:(数据结构与算法学习,数据结构与算法学习,数据结构与算法学习)