单链表的解题套路

01 合并两个有序链表

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

  1. 示例 1
  2. 输入:l1 = [1,2,4], l2 = [1,3,4]
  3. 输出:[1,1,2,3,4,4]
  4. 示例 2
  5. 输入:l1 = [], l2 = []
  6. 输出:[]
  7. 示例 3
  8. 输入:l1 = [], l2 = [0]
  9. 输出:[0]
  10. 提示:
  11. 两个链表的节点数目范围是 [0, 50]
  12. -100 <= Node.val <= 100
  13. l1 l2 均按 非递减顺序 排列
  14. 来源:力扣(LeetCode
  15. 链接:https://leetcode-cn.com/problems/merge-two-sorted-lists
  16. 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

使用 while 循环每次比较 l1->vall2->val 的大小,将较小的节点接在结果链表后。

1.1.7 单链表的解题套路 - 图1

代码中还使用了虚拟头节点,有了虚拟头节点这个占位符,可以避免处理空指针的情况。

ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
    if (list2 == nullptr)
        return list1;
    if (list1 == nullptr)
        return list2;
    ListNode* dummay = new ListNode(-101);
    ListNode* cur = dummay;
    while (list1 != nullptr && list2 != nullptr) {
        if (list1->val < list2->val) {
            cur->next = list1;
            list1 = list1->next;
        } else {
            cur->next = list2;
            list2 = list2->next;
        }
        cur = cur->next;
    }
    cur->next = (list1 == nullptr) ? list2 : list1;
    return dummay->next;
}

02 合并K个升序链表

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
示例 2:

输入:lists = []
输出:[]
示例 3:

输入:lists = [[]]
输出:[]


提示:

k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i] 按 升序 排列
lists[i].length 的总和不超过 10^4

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/merge-k-sorted-lists
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

01 方法一:顺序合并

定义一个空的节点,每次和数组中的链表进行合并,每次都是两条链表合并。可以使用上一题的代码。

ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
    if (list1 == nullptr)
        return list2;
    if (list2 == nullptr)
        return list1;
    ListNode* dummay = new ListNode(-1);
    ListNode* cur = dummary;
    while (list1 != nullptr && list2 != nullptr) {
        if (list1->val < list2->val) {
            cur->next = list1;
            list1 = list1->next;
        } else {
            cur->next = list2->next;
            list2 = list2->next;
        }
        cur = cur->next;
    }
    return dummay->next;
}

ListNode* mergeKLists(vector<ListNode*>& lists) {
    if (lists.empty()) {
        return nullptr;
    }
    if (lists.size() == 1)
        return lists[0];
    ListNode* ans = nullptr;
    for (auto list : lists)
        ans = mergeTwoLists(ans, list);
    return ans;
}

02 方法二:分治合并

  • k 个链表配对并将同一对中的链表合并;
  • 第一轮合并后,k 个链表被合并成了 k / 2 个链表,平均长度为 2n / k,然后是 k / 4个链表,k / 8 个链表等;
  • 重复这一过程,直到得到最终的有序数组。

1.1.7 单链表的解题套路 - 图2

代码如下:

ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
    if (list1 == nullptr)
        return list2;
    if (list2 == nullptr)
        return list1;
    ListNode* dummay = new ListNode(-1);
    ListNode* cur = dummary;
    while (list1 != nullptr && list2 != nullptr) {
        if (list1->val < list2->val) {
            cur->next = list1;
            list1 = list1->next;
        } else {
            cur->next = list2->next;
            list2 = list2->next;
        }
        cur = cur->next;
    }
    return dummay->next;
}

ListNode* merge(vector<ListNode*>& lists, int left, int right) {
    if (left == right)
        return lists[left];
    if (left > right)
        return nullptr;
    int mid = (left + right) >> 1;
    return mergeTwoLists(merge(lists, left, mid), merge(lists, mid + 1, r));
}

ListNode* mergeKLists(vector<ListNode*>& lists) {
    return merge(lists, 0, lists.size() -1);
}

03 方法三:优先级队列

将链表节点放入一个小根堆,就可以每次获得 k 个 节点中的最小节点。维护当前每个链表没有被合并的元素最前面一个,每次从中选取值最小的一个,合并到答案中即可。

struct cmp {
    bool operator()(ListNode* list1, ListNode* list2) {
        return list1->val > list2->val;
    }
};
ListNode* mergeKList(vector<ListNode*>& lists) {
    if (list.empty())
        return nullptr;
    ListNode* dummay = new ListNode(-1);
    ListNode* cur = dummay;
    // 优先队列,最小堆
    priority_queue<ListNode*, vector<ListNode*>, cmp> pq;
    // 将 k 个链表的头节点加入最小堆
    for (auto list : lists) {
        if (list)
            pq.push(list);
    }
    while (!pq.empty()) {
        // 获取最小节点,放入结果链表中
        ListNode* tmp = pq.top();
        pq.pop();
        cur->next = tmp;
        // 如果该条链表没有到终点,将下一个链表放入最小堆
        if (tmp->next)
            pq.push(tmp->next);
        cur = cur->next;
    }

}

03 返回链表的倒数第 K 个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

使用双指针,先让快指针向前移动 k 个位置。然后快慢指针一起向前移动,当 fast == nullptr,返回 slow 即可。

ListNode* getKthFromEnd(ListNode* head, int k) {
    ListNode* fast = head;
    for (int i = 0; i < k && fast != nullptr; ++i) {
        fast = fast->next;
    }
    ListNode* slow = head;
    while (fast != nullptr) {
        fast = fast->next;
        slow = slow->next;
    }
    return slow;
}

04 删除链表的倒数第 N 个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:

输入:head = [1], n = 1
输出:[]
示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

链表中结点的数目为 sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

可以使用上一题思路,但是这题需要删除倒数第 k 个节点,所以需要找到它的前一个节点,此外,定义一个虚拟头节点,可以统一删除思路。比如链表共有 5 个基点,题目让你删除倒数第 5 个,按照逻辑,需要找到倒数第 6 个,但是第一个节点前面灭有节点了这就会出错。

ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode* dummay = new ListNode(-1);
    dummay->next = head;
    ListNode* fast = dummay;
    fast = fast->next;
    for (int i = 0; i < n && fast != nullptr; ++i) {
        fast = fast->next;
    }
    ListNode* slow = dummay;
    while (fast != nullptr) {
        slow = slow->next;
        fast = fast->next;
    }
    ListNode* tmp = slow->next;
    slow->next = tmp->next;
    delete tmp;
    return dummay->next;
}

05 单链表的中点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。


提示:

给定链表的结点数介于 1 和 100 之间。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/middle-of-the-linked-list
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

使用快慢指针,慢指针每次向前移动一位,快指针每次向前移动二位,当快指针移动到链表末尾,慢指针所指的位置就是链表的中点。如果链表长度为偶数,就会有两个中点,它会指向第二个中点节点。

ListNode* middleNode(ListNode* head) {
    ListNode* fast = head;
    ListNode* slow = head;
    while (fast != nullptr && fast->next != nullptr) {
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}

06 环形链表-判断链表中是否有环

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

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

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。


提示:

链表中节点的数目范围是 [0, 104]
-105 <= Node.val <= 105
pos 为 -1 或者链表中的一个 有效索引 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/linked-list-cycle
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

还是使用快慢指针。慢指针每次移动一步,快指针每次移动两步。如果快指针最终遇到空指针,说明链表中没有环;如果快慢指针相遇,则快指针至少超过了慢指针一圈,说明链表有环。

bool hasCycle(ListNode* head) {
    if (head == nullptr)
        return false;
    ListNode* fast, *slow;
    fast = slow = head;
    while (fast != nullptr && fast->next != nullptr) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast)
            return true;
    }
    return false;
}

07 环形链表-链表的开始入环的第一个节点

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

链表中节点的数目范围在范围 [0, 104] 内
-105 <= Node.val <= 105
pos 的值为 -1 或者链表中的一个有效索引

先判断该链表是否有环。如果有环,让链表头指针和慢指针每次向前移动一位,如果两者相等,则该位置就是链表入环的第一个节点。

ListNode* detectCycle(ListNode* head) {
    if (head == nullptr) {
        return nullptr;
    }
    ListNode* fast, *slow;
    fast = slow = head;
    while (fast != nullptr && fast->next != nullptr) {
        fast = fast->next->next;
        slow = slow->next;
        if (slow == fast) {
            while (head != slow) {
                head = head->next;
                slow = slow->next;
            }
            return head;
        }
    }
    return nullptr;
}

08 链表相交

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

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。


提示:

listA 中节点数目为 m
listB 中节点数目为 n
1 <= m, n <= 3 * 104
1 <= Node.val <= 105
0 <= skipA <= m
0 <= skipB <= n
如果 listA 和 listB 没有交点,intersectVal 为 0
如果 listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

01 方法一:哈希集合

使用哈希集合存储链表节点。

首先遍历链表 headA,并将每个节点存放在哈希集合。然后遍历 headB ,对于遍历到的每个节点,判断该节点是否在哈希集合中:

  • 如果当前节点不在哈希集合中,则继续遍历下一个节点;
  • 如果当前节点哈希集合中,则后面的节点都在哈希集合中,返回该节点

如果链表 headB 中的节点都不在哈希集合中,则两个链表不相交,返回 nullptr

ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
    unordered_set<ListNode*> visited;
    ListNode* temp = headA;
    while (temp != nullptr) {
        visited.insert(temp);
        temp = temp->next;
    }
    temp = headB;
    while (temp != nullptr) {
        if (visited.count(temp) > 0)
            return temp;
        temp = temp->next;
    }
    return nullptr;
}

02 方法二:双指针

计算两个链表的长度,让长的链表先走 gap = lenA - lenB。这样可以使得两条链表按末尾对其的方式向前前进。然后让两个指针向后移动。如果 listA == listB。直接返回 listA or listB,如果到链表结尾,返回 nullptr 即可。

ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
    ListNode* list1 = headA;
        ListNode* list2 = headB;
        int lenA = 0, lenB = 0;
        while (list1 != nullptr) {
            lenA++;
            list1 = list1->next;
        }
        while (list2 != nullptr) {
            lenB++;
            list2 = list2->next;
        }

        list1 = headA;
        list2 = headB;
        if (lenA < lenB) {
            swap(list1, list2);
            swap(lenA, lenB);
        }

        int gap = lenA - lenB;
        while (gap--)
            list1 = list1->next;
        while (list1 != nullptr && list2 != nullptr) {
            if (list1 == list2) {
                return list1;
            } else {
                list1 = list1->next;
                list2 = list2->next;
            }
        }
        return nullptr;
}