链表刷题总结
138. 随机链表的复制
- 拷贝问题,此类问题图拷贝问题(133题)
- 解题思路
- 遍历两次
- 第一次遍历,拷贝next,并且拷贝old和new到一个map中,映射关系
- 第二次遍历,拷贝random,通过oldRandom找到newrandom,然后设置到new中
92. 反转链表 II
思路通链表的操作问题,通常都不难,技巧就是一定要先想清楚思路,并且在草稿纸上画图,然后编码。
技巧:
- 使用虚拟头节点解决left=1的问题,这样可以找到prev
解题思路:
- 穿针引线:使用curr,next,pre三个节点
- 1、curr的下一个节点指向next的下一个节点
- 2、next的下一个节点,指向curr节点啊
- 3、pre的next节点指向next节点
- 反转局部,在将prev的next,指向翻转后的头节点,翻反转后的尾节点,指向right的下一个节点即可
141. 环形链表
解题思路:使用快慢指针的方法来实现
注意边界条件:
1、head==null
2、head.size == 1
3、head.size == 2
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null && slow != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
}
206. 反转链表
列表的操作总结:
1、ListNode node = xx,暂存
2、curr.next = xx,修改制向
3、prev = curr,移动节点,或者prev暂存curr
4、curr = next,移动节点
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;// 暂存后续节点
curr.next = prev;// 修改引用指向
prev = curr;// 暂存当前节点
curr = next;// 访问下一个节点
}
return prev;
}
}
19. 删除链表的倒数第 N 个结点
三个思路:
1、先计算size,再移动指针,然后修改指向
2、借助栈,来找到元素,然后移动指针
3、使用双指针的方式来计算,先移动n为firstNode,然后second从dummy开始移动,直到first=null的时候,那么就找到了要删除的前一个元素
82. 删除排序链表中的重复元素 II
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode();
dummy.next = head;
ListNode left = dummy.next;
ListNode prev = dummy;
while (left != null && left.next != null) {
ListNode next = left.next;
if ( next.val != left.val) {
if (prev.next == left) {
prev = left;
left = next;
} else {
prev.next = next;
left = next;
}
} else {
left = next;
}
}
if (prev.next != left) {
prev.next = null;
}
return dummy.next;
}
}
解题思路:双指针
1、判断相邻元素是否相等,如果相等,那么移动left指针
2、如果不相等,判断,prev和left是否相等
- 如果相等,那么prev和left一起移动
- 如果不等,那么删除元素,prev.next = next
3、每次,不管什么条件,都移动left指针
4、最后,判断prev.next是否等于left,如果不等,那么说明后面的元素重复,那么prev.next置为null即可
61. 旋转链表
解题思路:
1、找到size
2、求余,找到准确要走几步路
3、找到头尾,以及分开的时候的头尾
4、断开,重连
5、边界情况处理:k=0, head.size == 1, k%size == 0,都直接return即可
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if (head == null) {
return null;
}
if (k == 0 || head.next == null){
return head;
}
ListNode first = head;
ListNode last = head;
int size = 0;
while(last != null) {
size++;
if (last.next == null) {
break;
}
last = last.next;
}
int moveSize = k % size;
if (moveSize == 0) {
return head;
}
ListNode prev = first;
for(int i = 0; i < size - moveSize; i++) {
prev = first;
first = first.next;
}
prev.next = null;
last.next = head;
return first;
}
}
86. 分隔链表
class Solution {
public ListNode partition(ListNode head, int x) {
if (head == null) {
return head;
}
ListNode dummyNode1 = new ListNode();
ListNode prev = dummyNode1;
ListNode dummyNode2 = new ListNode();
ListNode second = dummyNode2;
ListNode pointer = head;
while (pointer != null) {
if (pointer.val < x) {
prev.next = pointer;
prev = prev.next;
} else {
second.next = pointer;
second = second.next;
}
pointer = pointer.next;
}
prev.next = dummyNode2.next;
return dummyNode1.next;
}
}
下面这段代码真正的问题是:
把链表“拆”成两段以后,第二段(≥x 段)的尾节点 second 仍然指向原链表中它后面的某个节点,于是两段一接起来就形成了一个环,后面所有节点都被无限循环访问,程序可能超时、输出错误甚至死循环。
举个例子:
原链表 1 → 4 → 3 → 2 → 5 → 2,x = 3
循环结束后 second 停在最后一个 2,它的 next 仍然指向 5(原链表里的下一个节点)。
当你执行 prev.next = dummyNode2.next 把两段拼起来时,链表变成
1 → 2 → 2 → 4 → 3 → 5 → 2 → 4 → 3 → 5 → 2 → …
永远走不到 null,于是“不奏效”。
改正旧直觉,建立新直觉
-
旧直觉:“只要把节点按值拆成两段,再把第二段的头接到第一段的尾就完事了。”
-
问题根源:链表节点不是复制出来的新对象,它们原来的
next指针还在。拆链时必须把“当前段”的尾节点的next显式断开,否则它会带着原来后面的整条子链一起留下来,造成环。 -
新直觉(以后写拆链代码时默念):
“每拆完一段,立刻把尾巴的next置为null。”
也就是second.next = null; // 关键一句放在拼接之前即可。
修正后的代码(只动一行)
class Solution {
public ListNode partition(ListNode head, int x) {
if (head == null) return head;
ListNode dummy1 = new ListNode(), tail1 = dummy1;
ListNode dummy2 = new ListNode(), tail2 = dummy2;
for (ListNode p = head; p != null; p = p.next) {
if (p.val < x) {
tail1.next = p;
tail1 = tail1.next;
} else {
tail2.next = p;
tail2 = tail2.next;
}
}
tail2.next = null; // 断开第二段尾巴,防止成环
tail1.next = dummy2.next;
return dummy1.next;
}
}
以后凡是“把一条链拆成多条再拼接”的场景,第一时间把每条链的尾节点 next 设成 null,就能彻底避免这一类隐蔽的成环 bug。
146. LRU 缓存
解题思路:
1、使用哈希表,来实现O(1)级别的get和put
2、使用一个双向链表来实现LRU,维护头节点和尾节点,实现基本操作比如addToHead, removeNode, moveToHead, removeTail;
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}

浙公网安备 33010602011771号