Post

LeetCode刷题学习(三) -- 链表篇

LeetCode刷题学习(三) -- 链表篇

LeetCode刷题学习记录,链表篇。

1. 基础

链表定义:

1
2
3
4
5
6
7
8
9
// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

// 示例,初始化一个链表,构造函数便于初始化时对节点赋值
ListNode* head = new ListNode(5);

2. 203.移除链表元素

203. Remove Linked List Elements

2.1. 思路和解法

技巧:定义虚拟头节点,避免头节点单独处理。

另外有几个注意点:

  • 虚拟头节点记得释放空间,不要直接return dummyHead->next
  • C++里要删除的节点,一般也释放其空间
  • 注意循环中判断currNode->next,而不是用 currNode != nullptr,并对currNode迭代
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
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 定义一个虚拟节点,指向链表头
        ListNode *dummyHead = new ListNode();
        dummyHead->next = head;
        ListNode *currNode = dummyHead;
        // 注意循环中判断next,而不是用 currNode != nullptr,并对currNode迭代
        while (currNode->next != nullptr) {
            if (currNode->next->val == val) {
                // 下一个节点和val相等,则移除下个节点
                ListNode *tmp_node = currNode->next;
                currNode->next = currNode->next->next;
                delete tmp_node;
            } else {
                // 没有匹配到相等节点时迭代下一个,上面匹配到时next已经指向下一个了
                currNode = currNode->next;
            }
        }

        // dummyHead 是临时申请的空间,需要释放掉
        ListNode *node = dummyHead->next;
        delete dummyHead;

        // 返回的节点指针,是原来链表中已有的节点
        return node;
    }
};

2.2. Rust解法

Rust中如果要实现链表的话比较复杂,可作了解:手把手带你实现链表

上面的题目链接(203. Remove Linked List Elements)中,已经给出了Rust中的链表定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct ListNode {
  pub val: i32,
  // Option枚举类型,Some(T)和None;返回Some(T)时,是堆上分配的Box智能指针
  pub next: Option<Box<ListNode>>
}

impl ListNode {
  // 内联属性
  #[inline]
  // 这里的new只是创建了一个新的ListNode实例,不是指分配在堆内存上
  fn new(val: i32) -> Self {
    ListNode {
      next: None,
      val
    }
  }
}

学习参考链接中的题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl Solution {
    pub fn remove_elements(head: Option<Box<ListNode>>, val: i32) -> Option<Box<ListNode>> {
        // 使用智能指针
        let mut dummyHead = Box::new(ListNode::new(0));
        dummyHead.next = head;
        let mut cur = dummyHead.as_mut();
        // 使用take()替换std::mem::replace(&mut node.next, None)达到相同的效果,并且更普遍易读
        while let Some(nxt) = cur.next.take() {
            if nxt.val == val {
                cur.next = nxt.next;
            } else {
                cur.next = Some(nxt);
                cur = cur.next.as_mut().unwrap();
            }
        }
        dummyHead.next
    }
}

3. 707.设计链表

707. Design Linked List

3.1. 思路和解法

注意题目要求中的几个小点和边界:

  • 0 <= index, val <= 1000 范围都是正整数,代码中可以不判断< 0的边界
    • 若在实际开发中,建议还是加上显式校验,弱约束依赖调用方的靠谱程度
  • int get(int index),如果索引无效则返回-1
  • void addAtIndex(int index, int val),在index节点前插入节点,如果index等于链表长度,则在最后插入,否则不插入

插入和删除都需要先找到前一个节点,所以从虚拟头节点开始会更方便;而get查找,从第一个节点开始即可,更容易理解。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class MyLinkedList {
public:
    // 定义链表结构
    struct LinkedNode {
        int val;
        LinkedNode *next;
        LinkedNode(int val):val(val), next(nullptr){}
    };

    MyLinkedList() {
        size = 0;
        dummyHead = new LinkedNode(0);
    }
    
    int get(int index) {
        // 题目的约束中限制了`>=0`,实际还是显式判断下不能`<0`
        if (index > size - 1 || index < 0) {
            return -1;
        }

        // 从第一个节点(index下标为0)开始遍历
        LinkedNode *cur = dummyHead->next;

        // for (int i = 0; i < index; i++) {
        //     cur = cur->next;
        // }
        // while循环更简洁一点,不用临时变量
        while (index--) {
            cur = cur->next;
        }
        return cur->val;
    }
    
    void addAtHead(int val) {
        LinkedNode *node = new LinkedNode(val);
        node->next = dummyHead->next;
        dummyHead->next = node;
        size++;
    }
    
    void addAtTail(int val) {
        LinkedNode *cur = dummyHead;
        // 遍历到最后节点
        while (cur->next != nullptr) {
            cur = cur->next;
        }
        LinkedNode *node = new LinkedNode(val);
        cur->next = node;
        size++;
    }
    
    // 插入到index前面
    void addAtIndex(int index, int val) {
        // 写边界时,可代入一个特殊值,如size=1时,须满足index<=1,两者相等时插入到队尾
        if (index > size) {
            return;
        }
        // 虽然题目有边界,index<0的边界还是显式处理下
        if (index < 0) {
            index = 0;
        }

        // 遍历到index前一个节点,需要从虚拟头开始
        LinkedNode *cur = dummyHead;
        while (index--) {
            cur = cur->next;
        }
        LinkedNode *node = new LinkedNode(val);
        node->next = cur->next;
        cur->next = node;
        size++;
    }
    
    void deleteAtIndex(int index) {
        // 虽然题目边界index>=0,实际编程中还是硬性判断一下
        if (index > size - 1) {
            return;
        }
        // 遍历到index前一个节点
        LinkedNode *cur = dummyHead;
        while (index--) {
            cur = cur->next;
        }
        LinkedNode *node = cur->next;
        cur->next = cur->next->next;
        delete node;
        size--;
    }

private:
    // 链表长度,获取指定索引时需要校验
    int size;
    // 虚拟头节点,实际next为链表
    LinkedNode *dummyHead;
};

4. 206.反转链表

206. Reverse Linked List

4.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
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 不需要虚拟头节点
        ListNode *prev = nullptr;
        ListNode *cur = head;
        ListNode *tmp = nullptr;
        while (cur != nullptr) {
            // 先记录下个节点,因为cur下面会改变,导致其next指向也变化
            ListNode *tmp = cur->next;
            cur->next = prev;
            prev = cur;
            cur = tmp;
        }
        // 最后cur是nullptr,链表头应该为prev
        return prev;
    }
};

5. 24.两两交换链表中的节点

24. Swap Nodes in Pairs

5.1. 思路和解法

题目中要求两两交换,节点对应值不可修改,即节点交换而不是值交换。

注意:只交换节点不够,需要更新新节点的next的指向

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
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 由于头节点也涉及交换,还是定义一个虚拟头便于处理
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode *cur = dummyHead;
        // cur初始值和退出条件是关键
        while (cur->next != nullptr && cur->next->next != nullptr) {
            // 记录下用于步进
            // ListNode* tmp = cur->next;
            // 错误做法:交换两节点 dummyHead -> a -> b ->,先让a->next指向b的下一个节点,再b->next指向a
            // cur->next->next = cur->next->next->next;
            // tmp->next->next = tmp;

            // 上述光交换两节点不够
            ListNode *tmp = cur->next;
            // 再定义一个临时节点,记录 dummyHead -> a -> b -> 中的下一个节点
            ListNode *tmp1 = cur->next->next->next;
            // 交换两节点,并修改其next指向
            cur->next = cur->next->next;
            cur->next->next = tmp;
            cur->next->next->next = tmp1;

            // 步进cur
            cur = cur->next->next;
        }
        cur = dummyHead->next;
        delete dummyHead;

        return cur;
    }
};

画一下示意图非常有必要:

示意图

6. 19.删除链表的倒数第N个节点

有时插件网络不好,还是切中文站,题目以英文显示。

19. Remove Nth Node From End of List

6.1. 思路和解法

双指针法。

题目中sz是链表长度,1 <= sz <= 30,并限定了 1 <= n <= sz,即倒数取值不会超过链表长度。

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
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* result;

        // 头节点可能被移除,定义一个虚拟头
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;
        // 双指针法
        ListNode *slow = dummyHead;
        ListNode *fast = dummyHead;
        
        // 题目中边界限定了 n 不会超过链表长度,可以不单独考虑n没结束但fast提前到nullptr
        // 先让快指针走n+1步,这里n+1步是核心,正好让slow在待删的前一个节点
        while (n-- > 0 && fast != nullptr) {
            fast = fast->next;
        }
        fast = fast->next;

        // 一起步进,fast到底则slow->next就是要移除的节点
        while (fast != nullptr) {
            slow = slow->next;
            fast = fast->next;
        }
        ListNode* tmp = slow->next;
        slow->next = slow->next->next;
        delete tmp;
        result = dummyHead->next;
        delete dummyHead;

        return result;
    }
};

题目最好先理清楚,注意边界。跟着示例画一下图示过程,便于加深理解:

图示

7. 160.链表相交

160. Intersection of Two Linked Lists

7.1. 思路和解法

某个节点指针相等时才表示相交,即该节点及之后,两个链表的所有节点都相同。

解法:先计算2个链表长度,让长的链表先移动长度差值个节点,而后同时步进两个链表,直到出现有相等节点指针就表示相交

示意图:

示意图

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
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        int lenA = 0;
        int lenB = 0;
        ListNode *tmpA = headA;
        ListNode *tmpB = headB;

        // 计算链表长度
        while (tmpA != nullptr) {
            tmpA = tmpA->next;
            lenA++;
        }
        while (tmpB != nullptr) {
            tmpB = tmpB->next;
            lenB++;
        }
        tmpA = headA;
        tmpB = headB;

        // 长链表先移动到两者长度相等的节点
        int sub = lenA >= lenB ? (lenA-lenB) : (lenB-lenA);
        if (lenA >= lenB) {
            while (sub--) {
                tmpA = tmpA->next;
            }
        } else {
            while (sub--) {
                tmpB = tmpB->next;
            }
        }

        // 比较是否相交
        while (tmpA != nullptr && tmpB != nullptr) {
            if (tmpA == tmpB) {
                // 相交
                return tmpA;
            }
            tmpA = tmpA->next;
            tmpB = tmpB->next;
        }

        return nullptr;
    }
};

8. 142.环形链表II

142. Linked List Cycle II

8.1. 思路和解法

快慢指针法,快指针追上慢指针时的交点距离(可能快指针走n圈才追上),对应推导说明可参考:代码随想录 – 环形链表II

此处直接用结论:

  • 快指针每次移动2个节点。快指针是一个节点一个节点靠近慢指针的,因此一定会相遇。(注意:快慢指针的相遇点并一定是环形入口节点
  • 环形入口:从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点,那么当这两个指针相遇的时候就是环形入口的节点

示意图:

示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        // 快慢指针法
        ListNode *slow = head;
        ListNode *fast= head;
        while (fast != nullptr && fast->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
            // 快慢节点相遇,但不是环形入口
            if (slow == fast) {
                // 直接用结论:从 链表起点 和 相遇点 各自按一步移动,相遇点即环形入口
                ListNode *tmp1 = head;
                ListNode *tmp2= slow;
                while (tmp1 != tmp2) {
                    tmp1 = tmp1->next;
                    tmp2 = tmp2->next;
                }
                return tmp1;
            }
        }
        return nullptr;
    }
};

9. 小结

画图示理解十分重要,示意图可参考:excalidraw图示

10. 参考

1、代码随想录 – 链表篇

2、LeetCode中文站

3、LeetCode英文站

This post is licensed under CC BY 4.0 by the author.