3种链表结构:
单向链
一个结点不仅存储数据还有指向下个结点的指针。尾结点指向NULL。
双向链
一个结点不仅存储数据还有指向下个结点的指针和指向前面结点的指针。尾结点指向前面的结点。
循环链
一个结点不仅存储数据还有指向下个结点的指针。尾结点指向第一个结点。
特点:插入和删除的时候,由于只需要记录前后的数据,所以时间复杂度为O(1)。查询的时候,需要遍历所有数据,所有时间复杂度为O(n)。
空间换时间设计理念:
双向链由于需要多记录前面的结点,多了一个指针,增大内存的消耗,但是查询时如果正好数据在前面的那个结点,那么这个时候查询复杂度为O(1),相当于拿空间换时间。
数组和链表区别:
数组:1,删除插入慢,时间复杂度为O(n)。查询快,时间复杂度为O(1)。
2,和缓存的关系,由于缓存通常速度比较,空间小,数组有缓存行的概念,时连续的空间,可以缓存附近的数据。
3,扩容问题,数组扩容需要先申请内存,再复制,消耗资源
链表:1,和数组相反,删除插入快,时间复杂度为O(1)。查询慢,时间复杂度为O(n)。
2,和缓存的关系,由于数据不是存在连续的内存地址,不存在缓存行概念。链表的结点不仅包含数据还包含指针,占用空间大小相当于数组的2倍。
3,链表不存在扩容问题,有容量就行。
除此之外,如果你的代码对内存的使用非常苛刻,那数组就更适合你。因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除操作,还会导致频繁的内存申请和释放,容易造成内存碎片,如果是 Java 语言,就有可能会导致频繁的 GC(Garbage Collection,垃圾回收)。
3种缓存淘汰策略:
1,先进先出策略 FIFO(First In,First Out)、
2,最少使用策略 LFU(Least Frequently Used)、
3,最近最少使用策略 LRU(Least Recently Used)
如何利用链表实现(LRU)缓存淘汰机制?
当缓存满了的时候按照淘汰机制删除数据。
1,维护一个有序的单向链,越早存入的数据放在尾部
2,当缓存被访问的时候,如果链表中存在则,删除数据,然后插入到首部。
3,如果链表中不存在,缓存没满,插入到首部
4,缓存满了,删除尾部的数据,新数据插入首部
由于每次插入数据都需要遍历所有数据,时间复杂度为O(n)
几个写链表代码技巧:
1:理解指针或引用的含义
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
2:警惕指针丢失和内存泄漏
不知道你有没有这样的感觉,写链表代码的时候,指针指来指去,一会儿就不知道指到哪里了。所以,我们在写的时候,一定注意不要弄丢了指针。
3:哨兵模式
对链表操作的时候,通常需要判断第一个和最后一个,和中间数据的逻辑操作。
如果在链表中加入一个数据,保证所有的操作都一样,不需要判断首尾数据。那么加入数据操作成为哨兵模式。
4:重点留意边界条件处理:
1、如果链表为空时,代码是否能正常工作?
2、如果链表只包含一个结点时,代码是否能正常工作?
3、如果链表只包含两个结点时,代码是否能正常工作?
4、代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
5:举例画图,辅助思考:
对于稍微复杂的链表操作,比如前面我们提到的单链表反转,指针一会儿指这,一会儿指那,一会儿就被绕晕了。总感觉脑容量不够,想不清楚。所以这个时候就要使用大招了,举例法和画图法。
你可以找一个具体的例子,把它画在纸上,释放一些脑容量,留更多的给逻辑思考,这样就会感觉到思路清晰很多。比如往单链表中插入一个数据这样一个操作,我一般都是把各种情况都举一个例子,画出插入前和插入后的链表变化
6:多写多练,没有捷径
所以,我精选了 5 个常见的链表操作。你只要把这几个操作都能写熟练,不熟就多写几遍,我保证你之后再也不会害怕写链表代码。
1、单链表反转
2、链表中环的检测
3、两个有序的链表合并
4、删除链表倒数第 n 个结点
5、求链表的中间结点