链表可以说是最常见的数据结构之一。看似简单但是真正应用起来有很强的灵活性,现在将链表问题归纳一下,从链表的常见变式到具体应用,争取对链表问题一网打尽!
单链表
相比于数组插入删除时O(n)的时间复杂度,链表的时间复杂度只要O(1)
但是链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度
尾部插入
1 | private void insertToTail(int value){ |
索引插入
1 | private void insert(int value,int index){ |
通过值删除节点
注意头节点的特殊处理
1 | private void deleteByNode(int data){ |
通过值查找
1 | private Node findByValue(int data){ |
双链表
从结构上来看,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。
比如,删除操作的情况存在两种情况:
- 删除结点中“值等于某个给定值”的结点
- 删除给定指针指向的结点。
第一种情况链表的时间复杂度都是O(n),第二种情况双链表无需查找,O(1)时间复杂度删除节点
LinkedHashMap用的就是双向链表
插入操作
以尾部插入举例
1 | private void insertToTail(int data){ |
删除操作
删除头部
1 | private DuoNode deleteHead(){ |
删除尾部
1 | private DuoNode deleteTail() { |
查找
1 | private DuoNode findIndex(int index) { |
循环链表
循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表
私有属性
1 | public class CircleLinkedList { |
插入操作
1 | private void insertHead(int data){ |
应用
LRU缓存淘汰算法
思路:维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。
- 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
- 如果此数据没有在缓存链表中,又可以分为两种情况:
- 如果此时缓存未满,则将此结点直接插入到链表的头部;
- 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部。
构造方法
比起普通链表多维护一个容量
1 | public class LRUByLinkedList{ |
插入操作
1 | private void insert(int data){ |
查找缓存
1 | private Node findNode(int data) { |
单链表反转
递归法
注意开始边界条件
当递归的最后节点的时候将最后第二个节点的next节点指向最后第二个节点,然后最后一个节点成为了头节点
1 | /** |
非递归法
要点:1. 必须维护前驱节点 2. 反转将后继指针指向前驱节点
1 | /** |
环的检测
1 | //使用快慢指针法。追及问题,只要慢指针追上快指针,说明存在环路 |
两个有序链表的合并
1 | public static Node mergeSortList(Node la,Node lb){ |
可以通过维护一个哨兵让代码更加简洁
1 | /** |
递归法
1 | /** |
删除链表的倒数n个节点
快指针比慢指针前k个节点,然后快指针遍历到尾部的时候慢指针刚刚好到删除节点,注意保存删除节点的上一个节点
1 | private void deleteLastKth(Node list,int index){ |
求链表的中间节点
快慢指针法
1 | private static Node fndMiddleNode(Node list ){ |
判断是否回文串
思路:
使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进两步。在慢指针前进的过程中,同时修改其 next 指针,使得链表前半部分反序。最后比较中点两侧的链表是否相等。
1 | //使用的 |