力扣链表部分题目总结---基本完结~

链表

前言:

链表部分之前刷力扣也做过一些,基本做不出来,都是看题解,希望这次训练可以在链表的题目上有些进步叭;)

1.相交链表

  • 描述:给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

解法一:

  • 思路:两个链表相交,采用双指针的方法,开始分别指向两个链表头,同步向后走,一旦走完一个链表就换另一个链表头继续走,那么两个指针会在相交位置刚好重逢(如果相交的话),否则就返回null。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null) return null;
ListNode pA = headA;
ListNode pB = headB;
while(pA != pB){
pA = pA == null? headB : pA.next;
pB = pB == null? headA : pB.next;
}
return pA;
}
}
  • 非常巧妙的双指针,简洁有力的解决问题。简写的条件判断自己写的话用的比较少,可以学习一下。

解法二:

  • 思路:用哈希表存下一个链表的所有节点,再遍历另一个链表去找相交点,由于哈希表里存的是地址,所以是可行的。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> visited = new HashSet<>();
ListNode tmp = headA;
while(tmp != null){
visited.add(tmp);
tmp = tmp.next;
}
tmp = headB;
while(tmp != null){
if(visited.contains(tmp)){
return tmp;
}
tmp = tmp.next;
}
return null;
}
}
  • 总结:思路并不难,就是对Set这些数据结构要足够熟悉才行。

2.反转链表

  • 描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

解法一:

  • 思路:采用迭代的方式,设置3个变量prev,cur,next,从链表头开始逐步反转每一条next指针
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
}
  • 总结:最直白的思路,但是还是要对链表的基础足够熟悉才行。

解法二:

  • 思路:采取递归,一直往后,那么假设后半段链表已经反转过来,形成类似O->O->O->O<-O<-O,那么中间这个的前一个head的next.next修改为自己,再head.next=null,即可变为O->O->O<-O<-O<-O,以此类推。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
  • 总结:递归更加抽象,也更加有意思,对于递归的处理有了更深的理解,一般都是倒推,后面的已经完成,怎么处理当前的状况。

3.回文链表:

  • 描述:给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

解法一:

  • 思路:用一个数组取出所有节点的val,然后双指针判断是否回文
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public boolean isPalindrome(ListNode head) {
List<Integer> vals = new ArrayList<>();
ListNode cur = head;
while(cur != null){
vals.add(cur.val);
cur = cur.next;
}
int l = 0, r = vals.size() - 1;
while(l < r){
if(!vals.get(l).equals(vals.get(r))){
return false;
}
l++;
r--;
}
return true;
}
}
  • 总结:最直接的思路,熟练使用List数据结构,包括add,get,equals方法。

解法二:

  • 思路:利用递归会变相的反向遍历链表这个特点,设置一个从head开始的全局变量进行比较验证。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
private ListNode frontPointer;
public boolean recursivelyCheck(ListNode cur){
if(cur == null){
return true;
}
if(!recursivelyCheck(cur.next)){
return false;
}
if(cur.val == frontPointer.val){
frontPointer = frontPointer.next;
return true;
}
return false;
}
public boolean isPalindrome(ListNode head) {
frontPointer = head;
return recursivelyCheck(head);
}
}
  • 总结:实用性不强,因为在许多语言中,堆栈帧的开销很大(如 Python),并且最大的运行时堆栈深度为 1000(可以增加,但是有可能导致底层解释程序内存出错)。为每个节点创建堆栈帧极大的限制了算法能够处理的最大链表大小。但是思路很有意思。

解法三:

  • 思路:为了实现O(1)的空间复杂度,先利用快慢指针找到链表的中间位置,返回后半部分链表,比较两个链表,再恢复链表。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
public ListNode endOfFisrtHalf(ListNode head){
ListNode slow = head;
ListNode fast = head;
while(fast.next != null && fast.next.next != null){ //考虑只有一个节点或两个节点的特殊情况,最终slow必须是属于前半部分的最后一个节点
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
public boolean isPalindrome(ListNode head) {
if(head == null) return true;
ListNode firstHalfEnd = endOfFisrtHalf(head);
ListNode secondHalfStart = reverseList(firstHalfEnd.next);
ListNode p1 = head;
ListNode p2 = secondHalfStart;
boolean res = true;
while(res && p2 != null){
if(p1.val != p2.val){
res = false;
}
p1 = p1.next;
p2 = p2.next;
}
firstHalfEnd.next = reverseList(secondHalfStart);
return res;
}
}
  • 总结:和之前的反转链表连起来了,很美妙的做法,以及通过快慢指针找中间节点的方法,感觉也很常用。

4.环形链表

  • 描述:给你一个链表的头节点 head ,判断链表中是否有环。

    如果链表中存在环 ,则返回 true 。 否则,返回 false

解法一:

  • 思路:利用快慢指针,慢指针一次走一步,快指针一次走两步,如果有环,那么快指针一定比慢指针先进入,继而落后于慢指针,由于此时快指针与慢指针的距离每次减少1环可以看作是无限的直线,所以一定会相遇
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while(slow != fast){
if(fast == null || fast.next == null) return false;
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
  • 总结:神奇的快慢指针,与环结合很适合用来做相遇,很棒的思路~

5.环形链表||

  • 描述:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

解法一:

  • 思路:回归最简单直接的方法,Hash记录所有访问过的点,一旦再次访问到即可直接返回。

  • 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Solution {
public ListNode detectCycle(ListNode head) {
Set<ListNode> visited = new HashSet<>();
ListNode pos = head;
while(pos != null){
if(visited.contains(pos)){
return pos;
}else{
visited.add(pos);
pos = pos.next;
}
}
return null;
}
}

解法二:

思路
  • 思路:还是基于快慢指针,并且通过数学计算发现相遇后,从头节点到入环点的距离等于相遇点到入环点的距离。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null) return null;
ListNode slow = head, fast = head;
while(fast != null){
slow = slow.next;
if(fast.next == null) return null;
fast = fast.next.next;
if(slow == fast){
ListNode ptr = head;
while(ptr != slow){
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
}
  • 总结:和直接判断环的题目还是不太一样,首先就是快慢指针的起点都是head,然后导致while判断也发生变化,需要视情况而定。

6.合并两个有序链表

  • 描述:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

解法一:

  • 思路:采用递归的思想,先判断两个头部的值,较小的保留,剩余的两个链表继续合并,直到其中一个链表为空。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null) return list2;
if(list2 == null) return list1;
if(list1.val < list2.val){
list1.next = mergeTwoLists(list1.next, list2);
return list1;
}else{
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
}
  • 总结:两种情况再分别递归,实现两个链表中的较小值的逐一合并,很巧妙。

7.两数相加

  • 描述:给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

解法一:

  • 思路:创建一个新链,逐个去取两个链表上的数相加,直到两个链表均为空,若有一个为空,直接赋值为0,最后注意还要判断一下进位。

  • 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null, tail = null;
int carry = 0;
while(l1 != null || l2!= null){
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 != null ? l2.val : 0;
int sum = n1 + n2 + carry;
if(head == null){
head = tail = new ListNode(sum % 10);
}else{
tail.next = new ListNode(sum % 10);
tail = tail.next;
}
carry = sum /10;
if(l1 != null){
l1 = l1.next;
}
if(l2 != null){
l2 = l2.next;
}
}
if(carry > 0){
tail.next = new ListNode(carry);
}
return head;
}
}
  • 总结:思路很清晰,主要是写代码要多多注意细节问题~

8.排序链表

  • 描述:给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

解法一:

  • 思路:自顶向下,采用归并递归的方法,不断细分链表,直到不能再细分然后进行合并两个有序链表。时间复杂度为O(NlogN),空间复杂度为O(NlogN),因为用到了栈的递归空间。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail){
if(head == null) return head; //当前链表没有节点或只有一个节点是返回条件
if(head.next == tail){
head.next = null;
return head;
}
ListNode slow = head, fast = head;
while(fast != tail){
slow = slow.next;
fast = fast.next;
if(fast != tail){
fast = fast.next;
}
}
ListNode mid = slow;
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid, tail);
ListNode sorted = merge(list1, list2);
return sorted;
}
public ListNode merge(ListNode head1, ListNode head2){
ListNode dummyHead = new ListNode(0);
ListNode tmp1 = head1, tmp2 = head2;
ListNode temp = dummyHead;
while(tmp1 != null && tmp2 != null){
if(tmp1.val <= tmp2.val){
temp.next = tmp1;
tmp1 = tmp1.next;
}else{
temp.next = tmp2;
tmp2 = tmp2.next;
}
temp = temp.next;
}
if(tmp1 != null){
temp.next = tmp1;
}else if(tmp2 != null){
temp.next = tmp2;
}
return dummyHead.next;
}
}
  • 总结:关键点就是归并实现最终的排序,一些细节问题需要注意,比如需要断链然后再重新合并(tail这个节点很关键),merge方法还要更加熟练。

解法二:

  • 思路:自底向上,先把链表分为subLen为1的子链表,两两合并,subLen再乘2重复操作,直到大于等于链表长度。涉及到两个子链表的拆分(cur)然后再合并(prev),细节要很清楚才行。时间复杂度为O(NlogN),空间复杂度为O(1).
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Solution {
public ListNode sortList(ListNode head) {
if(head == null) return null;
ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
int len = 0;
ListNode node = head;
while(node != null){
len++;
node = node.next;
}
for(int subLen = 1; subLen < len; subLen <<= 1){
ListNode prev = dummyHead;
ListNode cur = dummyHead.next;
while(cur != null){
ListNode head_1 = cur;
for(int i = 1; i < subLen && cur != null && cur.next != null; i++){
cur = cur.next;
}
ListNode head_2 = cur.next;
cur.next = null;
cur = head_2;
for(int i = 1; i < subLen && cur != null && cur.next != null; i++){
cur = cur.next;
}
ListNode next = null;
if(cur != null){
next = cur.next;
cur.next = null;
}
ListNode merged = merge(head_1, head_2);
prev.next = merged;
while(prev.next != null){
prev = prev.next;
}
cur = next;
}
}
return dummyHead.next;
}
public ListNode merge(ListNode head1, ListNode head2){
ListNode dummyHead = new ListNode(0);
ListNode tmp1 = head1, tmp2 = head2;
ListNode temp = dummyHead;
while(tmp1 != null && tmp2 != null){
if(tmp1.val <= tmp2.val){
temp.next = tmp1;
tmp1 = tmp1.next;
}else{
temp.next = tmp2;
tmp2 = tmp2.next;
}
temp = temp.next;
}
if(tmp1 != null){
temp.next = tmp1;
}else if(tmp2 != null){
temp.next = tmp2;
}
return dummyHead.next;
}
}
  • 总结:这是一道很棒的题目,深入熟悉了两种做法,特别是两种优秀的底层思路,归并和自底向上。自底向上的思路说起来简单,但是操作起来常常有很多细节需要反复推敲,熟能生巧叭~

9.两两交换链表中的节点

  • 描述:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)

解法一:

  • 思路:本质就是交换链表中的两个节点,然后采取递归的方式不断往后走。
  • 代码:
1
2
3
4
5
6
7
8
9
10
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newHead = head.next;
head.next = newHead.next;
newHead.next = head;
head.next = swapPairs(head.next);
return newHead;
}
}
  • 总结:自己写出来的一道题目,很棒,刚开始swapPairs里写成了head.next.next,然后根据输出debug解决了问题,很不错~

10.随机链表的复制

  • 描述:给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。 深拷贝应该正好由 n全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点

解法一:

  • 思路:由于random指针可能指向后面的未创建的节点,所以采用回溯+哈希表,直接往下递归,把对应的节点返回来即可。时间和空间复杂度都是O(N)。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
Map<Node, Node> cacheNode = new HashMap<>();
public Node copyRandomList(Node head) {
if(head == null) return null;
if(!cacheNode.containsKey(head)){
Node newNode = new Node(head.val);
cacheNode.put(head, newNode);
newNode.next = copyRandomList(head.next);
newNode.random = copyRandomList(head.random);
}
return cacheNode.get(head);
}
}
  • 总结:原来递归还可以用于回溯,其实好像递归的本质就有点回溯的意味,那么这道题刚好应用了这一本质,再结合哈希避免了节点的重复复制。

解法二:

  • 思路:采取迭代的方法,先把每个节点拆分为两个一样的节点,前面一个节点作为原节点,后面一个节点作为复制节点,遍历把后面节点的random指针指向对应的复制节点,最后再拆分链表。时间复杂度为O(N),空间复杂度为O(1)。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
for(Node node = head; node != null; node = node.next.next){
Node newNode = new Node(node.val);
newNode.next = node.next;
node.next = newNode;
}
for(Node node = head; node != null; node = node.next.next){
Node newNode = node.next;
newNode.random = (node.random != null) ? node.random.next : null;
}
Node newHead = head.next;
for(Node node = head; node != null; node = node.next){
Node newNode = node.next;
node.next = node.next.next;
newNode.next = (newNode.next != null) ? newNode.next.next : null;
}
return newHead;
}
}
  • 总结:由于链表的特性采取复制节点的想法,那么next指针的复制就很自然,random指针的复制也很方便,很巧妙的方法~

解法三:

  • 思路:直接先忽略random指针复制出链表的结构,然后再针对random指针进行复制即可。其中利用map键值对来维护节点和它的复制节点的关系。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
Node cur = head;
Map<Node, Node> map = new HashMap<>();
while(cur != null){
map.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
while(cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
  • 总结:超级简单易懂,巧妙地采取了Map来维护,非常巧妙!

11.LRU缓存

  • 描述:请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

    实现 LRUCache 类:

    • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
    • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
    • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

    函数 getput 必须以 O(1) 的平均时间复杂度运行。

解法一:

  • 思路:采用map来实现key和node的对应,采用双向链表来维护节点间的结构,保证了删除尾节点后在O(1)时间内找到尾结点的前一个作为新的尾结点。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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<>();
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;
}
moveToHead(node);
return node.value;
}

public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if(node == null){
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{
node.value = value;
moveToHead(node);
}
}
private void moveToHead(DLinkedNode node){
removeNode(node);
addToHead(node);
}
private void removeNode(DLinkedNode node){
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void addToHead(DLinkedNode node){
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
private DLinkedNode removeTail(){
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
  • 总结:对于链表来说很好的一道综合题,考察了各种链表操作。然后双向链表以及head,tail节点的使用也很精妙,值得反复去写的一道题。

12.K个一组翻转链表

  • 描述: 给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

    k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

    你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换

解法一:

  • 思路: 对每k个节点进行翻转链表,由于是翻转部分链表,那么链表的两侧的相邻的节点都要注意,会跟之前的直接翻转链表有点区别,cur节点在后面就相当于已经翻转好的节点的集合,具体指向的是最后一个节点。
  • 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(-1, head), prev = dummy;
while(true){
ListNode last = prev;
for(int i = 0; i < k; i++){
last = last.next;
if(last == null) return dummy.next;
}
ListNode cur = prev.next, next;
for(int i = 0; i < k - 1; i++){
next = cur.next;
cur.next = next.next;
next.next = prev.next;
prev.next = next;
}
prev = cur;
}
}
}
  • 总结: 翻转部分的链表与直接翻转整个链表还是有一些区别的,但是写起来起承转合的很优美哇~

力扣链表部分题目总结---基本完结~
http://example.com/2024/02/02/力扣算法题/力扣链表部分题目总结/
作者
jhxxxxx
发布于
2024年2月2日
许可协议