LeetCode链表刷题笔记

笔记说明

说明:

该题目解答及搜集参考于:

其中,标有 *表示 LeetCode HOT 100 热题。

一、链表基础

// 单链表定义
struct ListNode
{
    int val;                                // 结点上存储的元素
    ListNode *next;                         // 指向下一结点的指针
    ListNode(int x):val(x),next(nullptr) {} // 构造函数:初始化列表
    ListNode(int x,ListNode* next):val(x),next(next) {}
};
​
// 构造结点的方式
// 方式一
ListNode* head = new ListNode(5);
// 方式二
ListNode* head = new ListNode();
head->val = 5;

1 移除链表元素-203

203. 移除链表元素

给你一个链表的头结点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的结点,并返回 新的头结点

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的结点数目在范围 [0, 104]

  • 1 <= Node.val <= 50

  • 0 <= val <= 50

1.1 普通解法

方法一:普通方式,需要分类讨论是不是头结点,因为处理方式不同。

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) 
    {
        // 1)若要删除的元素是头结点
        // 注意:这里是 while,不是 if
        // 如 head = [7,7,7,7] val = 7,操作之后的链表为 []
        while (head != nullptr && head->val == val)
        {
            ListNode * tmp = head;
            head = head->next;
            delete tmp; // C++ 需要做内存释放
        }
        // 2)若要删除的元素不是头结点
        ListNode* cur = head;       // 头结点不能移动,需要重新定义一个结点用于遍历
        // 注意:循环终止条件必须要加上 cur != nullptr,因为可能 cur = nullptr,而 cur->next 就操作空指针了
        // 从头结点开始遍历,应该先判断头结点是否为空,如果头结点为空的话,直接返回 head
        // 后面开始进行删除操作,如果要删除一个结点,需要找到它的前一个结点,所以每次判断是是 cur->next 是否等于 val
        while (cur != nullptr && cur->next != nullptr)
        {
            if (cur->next->val == val)
            {
                cur->next = cur->next->next;
            }
            else
            {
                cur = cur->next;
            }
        }
        return head;        // 返回头结点
    }
};

1.2 虚拟头结点

方法二:采用虚拟头结点,即在头结点前增加一个不存在的结点,这种方法的写法更加统一,更推荐。

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val)
    {
        // 创建虚拟头结点
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        // 或者直接一行实现
        // ListNode* dummyhead = new ListNode(0, head);
​
        ListNode* cur = dummyhead;  // 创建新的结点用于遍历,从虚拟头结点开始,依次向后
        while (cur->next != NULL)
        {
            if (cur->next->val == val)
            {
                cur->next = cur->next->next;
            }
            else
            {
                cur = cur->next;
            }
        }
        // 不能直接返回 head,因为可能原始头结点已经被删除
        // 所以应该返回虚拟头结点的下一个结点,这个结点才是此时真正的头结点
        return dummyhead->next; 
    }
};

2 设计链表-707

707. 设计链表

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的结点应该具备两个属性:valnextval 是当前结点的值,next 是指向下一个结点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个结点。假设链表中的所有结点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。

  • int get(int index) 获取链表中下标为 index 的结点的值。如果下标无效,则返回 -1

  • void addAtHead(int val) 将一个值为 val 的结点插入到链表中第一个元素之前。在插入完成后,新结点会成为链表的第一个结点。

  • void addAtTail(int val) 将一个值为 val 的结点追加到链表中作为链表的最后一个元素。

  • void addAtIndex(int index, int val) 将一个值为 val 的结点插入到链表中下标为 index 的结点之前。如果 index 等于链表的长度,那么该结点会被追加到链表的末尾。如果 index 比长度更大,该结点将 不会插入 到链表中。

  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的结点。

示例:

输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]
​
解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3

提示:

  • 0 <= index, val <= 1000

  • 请不要使用内置的 LinkedList 库。

  • 调用 getaddAtHeadaddAtTailaddAtIndexdeleteAtIndex 的次数不超过 2000

class MyLinkedList {
public:
    // 单链表定义
    struct ListNode
    {
        int val;                                // 结点上存储的元素
        ListNode* next;                         // 指向下一结点的指针
        ListNode(int x):val(x),next(nullptr){}     // 构造函数,初始化列表
    };
​
    MyLinkedList() {
        dummyhead = new ListNode(0);    // 构建虚拟头结点
        size = 0;                       // 链表元素个数为 0 
    }
    
    // 获取链表下标为 index 的结点的值
    int get(int index) {
        // 若下标小于 0 或者 超过链表长度,下标无效,返回 -1
        if (index < 0 || index >= size)
        {
            return -1;
        }
        else
        {
            ListNode* cur = dummyhead->next;
            while(index != 0)
            {
                cur = cur->next;
                index--;
            }
            return cur->val;
        }
    }
    
    // 将一个值为 val 的结点插入到链表的头结点位置
    void addAtHead(int val) {
        ListNode* newNode = new ListNode(val);       // 要插入的新结点     
        newNode->next = dummyhead->next;
        dummyhead->next = newNode;
        size++;     // 增加结点成功后,size 加一
    }
    
    // 将一个值为 val 的结点追加到链表的末尾
    void addAtTail(int val) {
        ListNode* newNode = new ListNode(val);
        // 首先要找到当前链表最后一个结点的位置
        ListNode* cur = dummyhead;
        while(cur->next != NULL)
        {
            cur = cur->next;
        }
        cur->next = newNode;
        size++;     // 增加结点成功后,size 加一
    }
    
    // 将一个值为 val 的结点插入到链表下标为 index 的结点之前
    void addAtIndex(int index, int val) {
        if (index > size)   // 若下标大于链表长度,则无法插入
            return;
        if (index < 0)      // 若下标小于 0 ,认为是在头结点前插入
            index = 0;
        ListNode* newNode = new ListNode(val);
        // 首先要找到链表下标为 index 的结点的前一个结点
        ListNode* cur = dummyhead;
        while(index--)
        {
            cur = cur->next;
        }
        // 遍历完后的 cur 刚好为 index 的前一个结点
        newNode->next = cur->next;
        cur->next = newNode;
        size++;     // 增加结点成功后,size 加一
    }
    
    // 删除下标为 index 的结点
    void deleteAtIndex(int index) {
        // 若下标小于 0 或者 超过链表长度,下标无效,返回
        if (index < 0 || index >= size)
            return;
        // 对于可删除情况
        ListNode* cur = dummyhead;
        while(index--)
        {
            cur = cur->next;
        }
        ListNode* tmp = cur->next;  // C++ 需要做内存释放
        cur->next = cur->next->next;
        delete tmp;     // 释放 tmp 指针原本所指向的内存
        tmp = nullptr;  // delete 后 tmp 的值为随机值,为避免其成为野指针,将其设置为空
        size--;         // 删除结点成功后,size 减一
    }
private:
    int size;               // 记录链表的元素个数
    ListNode* dummyhead;    // 定义虚拟头结点
};

3 反转链表

3.1 *反转整个链表-206

206. 反转链表

给你单链表的头结点 head ,请你反转链表,并返回反转后的链表。

示例 1:

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

示例 2:

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

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中结点的数目范围是 [0, 5000]

  • -5000 <= Node.val <= 5000

进阶:链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

3.1.1 双指针法
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 双指针法
        ListNode* tmp;
        ListNode* cur = head;
        ListNode* pre = nullptr;
        while(cur != nullptr)
        {
            // 此时结点位置分布:pre、cur、tmp
            tmp = cur->next;    // 先将 cur 结点后的指向保留,否则连接断开,将失去指向
            cur->next = pre;    // 更改指向,即从 pre ---> cur 变为 pre <--- cur
            
            // 更新 pre 和 cur 的位置
            // 将 pre 指针先向后移动,达到 cur 指针原来的位置,再移动 cur 指针
            // 顺序不能更换,若先移动了 cur,则 pre 找不到 cur 的位置了
            pre = cur;          
            cur = tmp;          
        }
        return pre; // 最后 pre 成为了头结点,而 cur 是 NULL
    }
};
3.1.2 递归法
class Solution {
public:
    ListNode* reverse(ListNode* pre, ListNode* cur)
    {
        if (cur == nullptr) // 注意,此处是当 cur 遍历到链表末尾时,停止递归
            return pre;
        ListNode* tmp = cur->next;
        cur->next = pre;
        return reverse(cur, temp);  // 当链表没有遍历完成时,一直调用递归,即反转链表元素
        // 注意,下一次迭代要更新结点位置
        // pre 往后挪一位,变成了 cur 所在的位置;
        // cur 往后挪一位,变成了 temp 所在的位置;
        // 所以迭代函数 reverse 中的参数 pre 变成 cur,cur 变成 temp 
    }
​
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        return reverse(pre, cur);
        // 或直接简写
        // return reverse(nullptr, head);
    }
};

3.2 反转前N个链表

思路:使用双指针法

ListNode* reverseN(ListNode* head, int n)
{
    // 对于特殊情况
    if (n == 1 || head == NULL)     // 若链表中只有一个数字或链表为空,则直接返回头结点
        return head;
    // 对于一般情况,使用双指针法
    ListNode* cur = head;
    ListNode* pre = NULL;
    ListNode* tmp;
    for (int i = 0; i < n && cur != NULL; i++)  // 更改链表指针指向
    {
        tmp = cur->next;
        cur->next = pre;
        pre = cur;
        cur = tmp;
    }
    // 循环结束后,cur 移动到不需要更改方向的链表的第一个结点
    head->next = cur;   // 将头结点与此结点相连,即可反转成功
    return pre;         // 此时 pre 为链表的头结点
}

3.3 反转部分链表-92

92. 反转链表 II

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表结点,返回 反转后的链表

示例 1:

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

示例 2:

输入:head = [5], left = 1, right = 1
输出:[5]

提示:

  • 链表中结点数目为 n

  • 1 <= n <= 500

  • -500 <= Node.val <= 500

  • 1 <= left <= right <= n

进阶: 你可以使用一趟扫描完成反转吗?

思路:穿针引线法,将需要反转的链表截断,然后将整个截断的链表全部反转后,再插入到原链表中。

class Solution {
public:
    void reverseList(ListNode* head)    // 反转整个链表
    {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur != nullptr)
        {
            ListNode* tmp = cur->next;  // 临时变量,存储 cur 后的结点
            cur->next = pre;            // 更改指向
            pre = cur;                  // 更新指针位置
            cur = tmp;
        }
    }
​
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* cur = dummyHead;
        for (int i = 0; i < left - 1 ; i++)
        {
            cur = cur->next;
        }
        ListNode* pre = cur;             // 找到需要反转链表的左边界的前一个结点
        ListNode* leftNode = cur->next;  // 找到反转链表的左边界
        for (int i = 0; i < right - left + 1; i++)
        {
            cur = cur->next;
        }
        ListNode* rightNode = cur;      // 找到需要反转链表的右边界
        cur = cur->next;                // 记录右边界的下一个结点
​
        // 1. 截断需要反转的链表
        pre->next = nullptr;
        rightNode->next = nullptr;
        // 2. 将截断的链表全部反转
        reverseList(leftNode);  
        // 3. 将反转后的链表,插入到原链表中
        // 注意:反转后的左结点变为了右结点,右结点变为了左结点,即:pre - 右结点......左结点 - cur
        pre->next = rightNode;
        leftNode->next = cur;
        return dummyHead->next;
    }
};

4 两两交换链表中的结点-24

24. 两两交换链表中的结点

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

示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

思路:每次交换前,指针指向要交换两个结点的前一个结点,然后交换完成,将该指针向后移动两个结点,使其又能指向要交换结点的前一个结点,重新开始交换新的两个结点

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* cur = dummyhead;
        // 1. 当结点数量为偶数个时,遍历结束的条件为 cur->next != NULL; 
        // 如 1->2->3->4->NULL
        // 要保证 cur 指向两个交换结点之前,第一次 cur 指向虚拟头结点,在 1 和 2 结点之前
        // 第二次 cur 指向 2,在 3 和 4 结点之前
        // 第三次 cur 指向 4,此时 cur->next = NULL,遍历结束
        // 2. 当结点数量为奇数个时,遍历结束条件为 cur->next->next != NULL;
        // 如 1->2->3->4->5->NULL
        // 前几步相同,当 cur 指向 4 结点时,后面只有一个结点,不能继续继续交换
        // 此时 cur->next->next = NULL,遍历结束
        
        // 注意:在写循环终止条件时,一定要先保证 cur->next != 0,再操作 cur->next->next,否则会保报错:操作非法指针
        // 注意:循环终止条件超级容易写错,要记住,移动 cur 要保证它指向的是要交换两个结点的前一个结点的位置
        while (cur->next != NULL && cur->next->next != NULL)
        {
            // 还没修改链表结构前,保留原链表 cur 之后的结点为临时结点
            ListNode* tmp = cur->next;
            ListNode* tmp1 = tmp->next->next;
​
            // 交换操作
            cur->next = tmp->next;
            tmp->next->next = tmp;
            tmp->next = tmp1;
​
            // 移动 cur 指针向后两位
            cur = cur->next->next;  // 等价于 cur = tmp;
        }
        return dummyhead->next;
    }
};

5 *删除链表中倒数第N个结点-19

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

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

示例 1:

输入: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

5.1 单指针法

先统计出链表中结点的个数,然后将遍历指针移动到要删除结点的前一结点,将其 next 指向其 next->next,即可完成删除结点操作。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyhead = new ListNode(0, head);
        ListNode* cur = dummyhead; // 用于遍历原始链表,统计链表中的结点个数
        int size = 0;
        while (cur->next != nullptr)
        {
            cur = cur->next;
            size++;
        }
        cur = dummyhead;
        for (int i = 0; i < size - n && cur != nullptr; i++)
        {
            cur = cur->next;
        }
        ListNode* tmp = cur->next;
        cur->next = cur->next->next;  // 删除结点操作
        delete tmp;                   // 释放内存
        tmp = nullptr;
        return dummyhead->next;
    }
};

5.2 快慢指针法

使用快慢指针,只需要遍历一次,就可以将指针指向要删除结点的前一个结点

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyhead = new ListNode(0, head);
        ListNode* fast = dummyhead;
        ListNode* slow = dummyhead;
        // 先让快指针走 n 步,为了让慢指针指向的是要删除结点的前一个结点,将快指针向后多移动一步
        while (n-- && fast != nullptr)
        {
            fast = fast->next;
        }
        fast = fast->next;
        
        // 同时移动快慢指针
        while (fast->next != nullptr)
        {
            fast = fast->next;
            slow = slow->next;
        }
        // 慢指针指向要删除结点的前一个结点,开始删除操作
        ListNode* tmp = slow->next;
        slow->next = slow->next->next;
        delete tmp;
        tmp = nullptr;
        return dummyhead->next;
    }
};

6 相交链表-160

160. 相交链表

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

图示两个链表在结点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始结点的值。如果不存在相交结点,这一值为 0

  • listA - 第一个链表

  • listB - 第二个链表

  • skipA - 在 listA 中(从头结点开始)跳到交叉结点的结点数

  • skipB - 在 listB 中(从头结点开始)跳到交叉结点的结点数

评测系统将根据这些输入创建链式数据结构,并将两个头结点 headAheadB 传递给你的程序。如果程序能够正确返回相交结点,那么你的解决方案将被 视作正确答案

示例 1:

输入: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 个结点。 — 请注意相交结点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的结点 (A 中第二个结点和 B 中第三个结点) 是不同的结点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的结点 (A 中第三个结点,B 中第四个结点) 在内存中指向相同的位置。

示例 2:

输入: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 个结点。

示例 3:

输入: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

  • 如果 listAlistB 没有交点,intersectVal0

  • 如果 listAlistB 有交点,intersectVal == listA[skipA] == listB[skipB]

进阶:你能否设计一个时间复杂度 O(m + n) 、仅用 O(1) 内存的解决方案?

思路:分别统计链表 A 和链表 B 的结点数,保证链表 A 的结点数更多,否则交换链表 A 和链表 B。先将链表 A 多余头部的结点遍历,保证两个链表的末尾段位置对齐,然后再依次遍历链表 A 和链表 B,若找到相同结点,即为相交结点返回,反之返回 null。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        int lenA = 0, lenB = 0;
        ListNode* curA = headA;         // 用于遍历链表 A
        ListNode* curB = headB;         // 用于遍历链表 B
        while (curA != nullptr)        // 求取链表 A 的长度
        {
            curA = curA->next;
            lenA++;
        }
        while (curB != nullptr)        // 求取链表 B 的长度
        {
            curB = curB->next;
            lenB++;
        }
        // 需要重新遍历,将遍历结点重置为头结点
        curA = headA;
        curB = headB;
        if (lenA < lenB)
        {
            swap(curA, curB);   // 让链表 A 变为结点更多的链表,lenA 为其长度
            swap(lenA, lenB);
        }
        int gap = lenA - lenB;  // 链表 A 和链表 B 相差的结点数
        while (gap--)
        {
            curA = curA->next;  // 让指针 curA 先把多余结点遍历,以保证后面的剩余结点与链表 B 相同,即末尾位置对齐
        }
        while (curA != nullptr && curB != nullptr)    // 遍历链表 A 和链表 B,遇到相同结点则返回,否则返回 nullptr
        {
            if (curA == curB)
            {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return nullptr;
    }
};

7 环形链表-141/142

142. 环形链表 II

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

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

不允许修改 链表。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 链表中结点的数目范围在范围 [0, 104]

  • -105 <= Node.val <= 105

  • pos 的值为 -1 或者链表中的一个有效索引

进阶:你是否可以使用 O(1) 空间解决此题?

思路:

相遇时慢指针走过的结点数为:x + y, 快指针走过的结点数:x + y + n (y + z),n 为快指针在圈内循环的圈数, (y + z)为 圈内结点数。

因为快指针每次走两个结点,慢指针每次走一个结点, 则 :

慢指针走过的结点数 * 2 = 快指针走过的结点数
列式如下:
(x + y) * 2 = x + y + n (y + z)
x + y = n (y + z)
x = n (y + z) - y
x = (n - 1) (y + z) + z
当 n = 1 时,x = z

注意:n 一定是大于等于 1 的,因为快指针至少要多走一圈才能相遇慢指针。

从头结点出发一个指针,从相遇结点也出发一个指针,这两个指针每次只走一个结点, 那么当这两个指针相遇的时候就是环形入口的结点

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        // 定义快慢指针,快指针每次移动两个结点,慢指针每次移动一个结点
        // 慢指针在环里走完一圈之内,一定会和快指针相遇
        // 从相遇结点开始到环入口结点的距离 = 从头结点到入口结点的距离
        
        // 注意:此处循环结束的条件不能仅仅只写 fast != nullptr,要加上 fast->next != nullptr
        // 因为 fast 指针每次移动两个结点,如果 fast->next = nullptr 时,那么 fast = fast->next->next 就操作了空指针
        while (fast != nullptr && fast->next != nullptr)
        {
            fast = fast->next->next;
            slow = slow->next;
            if (fast == slow)
            {
                ListNode* index1 = head;
                while (index1 != index2)    // 找到入口结点
                {
                    index1 = index1->next;  // 从头开始遍历
                    slow = slow->next;      // 从相遇位置开始遍历
                }
                return slow;
            }
        }
        return nullptr;
    }
};

8 *合并两个有序链表-21

21. 合并两个有序链表

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

示例 1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

输入:l1 = [], l2 = []
输出:[]

示例 3:

输入:l1 = [], l2 = [0]
输出:[0]

提示:

  • 两个链表的结点数目范围是 [0, 50]

  • -100 <= Node.val <= 100

  • l1l2 均按 非递减顺序 排列

思路:

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* dummyhead = new ListNode(0);
        ListNode* cur = dummyhead;
        ListNode* l1 = list1;
        ListNode* l2 = list2;
        // 对于两个链表依次遍历,直到有一个链表遍历完为止
        while (l1 != nullptr && l2 != nullptr)
        {
            // 将值较小的结点连接到 cur 之后
            if (l1->val < l2->val)
            {
                cur->next = l1;
                l1 = l1->next;
            }
            else
            {
                cur->next = l2;
                l2 = l2->next;
            }
            // 注意:将 list1 和 list2 遍历后,得到其较小值的结点,还需要将 cur 也向后遍历
            cur = cur->next;
        }
        if (l1 != nullptr)     // 若 l1 链表还有剩余结点,就把后面的结点接到 cur 之后(此时 l2 链表已经为空)
        {
            cur->next = l1;
        }
        if (l2 != nullptr)     // 若 l2 链表还有剩余结点,就把后面的结点接到 cur 之后(此时 l1 链表已经为空)
        {
            cur->next = l2;
        }
        return dummyhead->next; 
    }
};

9 *合并K个升序链表-23

23. 合并 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

思路:

class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        if (lists.empty())
            return nullptr;
        ListNode* dummyhead = new ListNode(0);
        ListNode* cur = dummyhead;
        // 优先级队列,默认最大堆,这里使用最小堆
        // 堆顶元素最小,a > b 的值返回 true,表示优先级较高的元素会排在堆顶
        // 注意:
        // 定义一个优先级对齐 pq
        // 语法:priority<参数1(类型), 参数2(原型),参数3(函数)> pq([](参数1, 参数2){return ....;});
        // pq 后面使用了 lambda 函数表达式
        priority_queue<ListNode*, vector<ListNode*>,function<bool(ListNode*, ListNode*)>> 
        pq([](ListNode* a, ListNode* b){return a->val > b->val;});
​
        // 将 K 个链表的头结点加入最小堆
        for (auto head : lists)
        {
            if (head != nullptr)
                pq.push(head);
        }
        // 注意:这里的循环一直在判断 pq 中是否为空,因为每次都将 k 个链表的头结点(即最小结点)放入队列中
        // 找到 k 个头结点中的最小值后,放入新链表中,若选中的头结点后面还有结点,则将它在新放入队列中,进行下次比较
        while (!pq.empty())
        {
            ListNode* minNode = pq.top();   // 获取最小结点
            pq.pop();                       // 获取出来后就在优先级队列中删除
            cur->next = minNode;
            if (minNode->next != nullptr)  // 相当于找到数值较小的结点后,需要将其指针向后移动一位
            {
                pq.push(minNode->next);     // 将该链表的下一个结点(当前头头结点)放入优先级队列中排序
            }
            cur = cur->next;                // cur 指向了新结点后,需要向后遍历
        }
        return dummyhead->next;
    }
};

10 链表的中间结点-876

876. 链表的中间结点

给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

示例 1:

输入:head = [1,2,3,4,5]
输出:[3,4,5]
解释:链表只有一个中间结点,值为 3 。

示例 2:

输入:head = [1,2,3,4,5,6]
输出:[4,5,6]
解释:该链表有两个中间结点,值分别为 3 和 4 ,返回第二个结点。

提示:

  • 链表的结点数范围是 [1, 100]

  • 1 <= Node.val <= 100

10.1 单指针法

先统计链表中的结点个数,再计算将头结点移动多少到达链表的中间结点

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* cur = head;
        int len = 0;    
        while (cur != nullptr)
        {
            cur = cur->next;    // 遍历链表
            len++;              // 统计链表中结点的个数
        }
        int count = len / 2;    // 需要将头结点移动的结点个数,偶数个移动的次数正好达到中间两结点的右结点
        while (count--)
        {
            head = head->next;
        }
        return head;
    }
};

10.2 快慢指针法

快指针每次移动两个结点,慢指针每次移动一个结点,这样当快指针移动到结点末尾时,慢指针刚好位于链表中间结点。

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        // 当结点数为奇数时,循环终止条件为 fast != NULL
        // 当结点数为偶数时,循环终止条件为 fast->next != NULL
        while (fast != nullptr && fast->next != nullptr)
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};

11 分隔链表-86

86. 分隔链表

给你一个链表的头结点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的结点都出现在 大于或等于 x 的结点之前。

你应当 保留 两个分区中每个结点的初始相对位置。

示例 1:

输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

示例 2:

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

提示:

  • 链表中结点的数目在范围 [0, 200]

  • -100 <= Node.val <= 100

  • -200 <= x <= 200

思路:先拆再合

合并链表时是合二为一,这里需要分解链表,可以将原链表分成两个小链表,一个存放元素大于 x 的结点,另一个存放大于等于 x 的结点,最后再把这两条链表连接在一起即可。

class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode* dummyhead1 = new ListNode(0);
        ListNode* dummyhead2 = new ListNode(0);
        ListNode* cur1 = dummyhead1;
        ListNode* cur2 = dummyhead2;
        ListNode* cur = head;
        while (cur != nullptr)
        {
            if (cur->val < x)
            {
                cur1->next = cur;
                cur1 = cur1->next;
            }
            else
            {
                cur2->next = cur;
                cur2 = cur2->next;
            }
            // 注意:要先断开,再更新位置
            // 不能直接移动遍历指针,而是应该先断开原链表的连接,这样才能将两个链表分开
            // 然后再将原链表遍历指针 cur 向后移动一个结点
            ListNode* tmp = cur->next;
            cur->next = nullptr;
            cur = tmp;
        }
        cur1->next = dummyhead2->next;
        return dummyhead1->next;
    }
};

12 删除链表中的结点-237

237. 删除链表中的结点

有一个单链表的 head,我们想删除它其中的一个结点 node。给你一个需要删除的结点 node 。你将 无法访问 第一个结点 head。链表的所有值都是 唯一的,并且保证给定的结点 node 不是链表中的最后一个结点。删除给定的结点。注意,删除结点并不是指从内存中删除它。这里的意思是:

  • 给定结点的值不应该存在于链表中。

  • 链表中的结点数应该减少 1。

  • node 前面的所有值顺序相同。

  • node 后面的所有值顺序相同。

自定义测试:

  • 对于输入,你应该提供整个链表 head 和要给出的结点 nodenode 不应该是链表的最后一个结点,而应该是链表中的一个实际结点。

  • 我们将构建链表,并将结点传递给你的函数。

  • 输出将是调用你函数后的整个链表。

示例 1:

输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个结点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9

示例 2:

输入:head = [4,5,1,9], node = 1
输出:[4,5,9]
解释:指定链表中值为 1 的第三个结点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9

提示:

  • 链表中结点的数目范围是 [2, 1000]

  • -1000 <= Node.val <= 1000

  • 链表中每个结点的值都是 唯一

  • 需要删除的结点 node链表中的结点 ,且 不是末尾结点

思路:

链表中结点值均不相同,且要求删除的结点并不是从内存中删除,而是让该结点的值不存在于链表中。

题目中传入函数的参数并不是头结点,因此无法定位到该结点的前一个结点。采用替身攻击的方法,将该结点的值变为它后一个结点的值,然后将后一个结点删除。

class Solution {
public:
    void deleteNode(ListNode* node) {
        node->val = node->next->val;        // 偷值
        ListNode* tmp = node->next;
        node->next = node->next->next;      // 删结点
        delete tmp;
        tmp = nullptr; 
    }
};

13 回文链表-234

234. 回文链表

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

示例 1:

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

示例 2:

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

提示:

  • 链表中结点数目在范围[1, 105]

  • 0 <= Node.val <= 9

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

13.1 数组+双指针

思路:复制链表的值到数组中,利用双指针法判断是否为回文。

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> nums;
        ListNode* cur = head;
        // 遍历链表,将所有的链表结点值存入数组中
        while (cur != nullptr)
        {
            nums.push_back(cur->val);
            cur = cur->next;
        }
        // 遍历数组,比较数组中头和尾的元素是否相等
        for (int i = 0, j = nums.size() - 1; i < j; i++, j--)
        {
            if (nums[i] != nums[j])
                return false;
        }
        return true;
    }
};

13.2 快慢指针

思路:

涉及题型:链表的中间结点-876、反转链表-206

步骤:找到前半段链表的尾结点(即链表中心结点)、反转后半段链表、比较前半段链表和反转后的后半段链表对应值是否相等(判断是否回文)。

class Solution {
public:
    ListNode* reverse(ListNode* head)   // 反转后半段的链表
    {
        ListNode* pre = nullptr;
        ListNode* cur = head;
        while (cur != nullptr)
        {
            ListNode* tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
    
    bool isPalindrome(ListNode* head) {
        if (head == NULL || head->next == NULL) // 若为空链表或链表中只有一个结点,返回为真
            return true;
        ListNode* fast = head;
        ListNode* slow = head;
        // 1. 找到中间结点,将链表分为前半段和后半段
        // 快指针每次移动两个结点,慢指针每次移动一个结点,当快指针走完的时候,慢指针刚好移动到中间结点
        // 此题与“查找链表中间结点-876”这道题有点区别,这一题是当有偶数个结点时,返回的是中间两个结点的后一个
        // 此题遇到偶数个结点数,如“1-2-2-1”,如何把前半段和后半段分开?中间两个结点是 2-2,应该找的是前面的 2
        // 这样就把链表分成了前半段“1-2”,和后半段“2-1”
        // 因此 fast 在遍历循环的时候,循环条件需要更改
        while (fast->next != NULL && fast->next->next != NULL)
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        // 此时 slow 表示的就是中间结点的位置
        // 2. 将后半段链表进行反转
        ListNode* nextHead = slow->next;    // slow 结点后为另一链表的起始头结点
        ListNode* pre = reverse(nextHead);
​
        // 3. 依次遍历前半段和后半段链表,看对应结点的值是否相等
        // 如果链表中有奇数个结点,如原链表为“1-2-3-2-1”,中间结点为 3
        // 前半段链表为“1-2-3”;后半段链表本来为“2-1”,经过反转之后得“1-2”
        // 只需要依次查看结点 1 和结点 2,即遍历后半段反转之后的结点,数值为 3 的结点不用管
        // 此时经过反转后的后半段的头结点变为 pre
        ListNode* cur = head;
        while (pre != NULL)
        {
            if (cur->val != pre->val) // 若值不相等,代表不是回文链表
            {
                return false;
            }
            // 否则,前半段与后半段值相等,依次遍历至后半段链表结束
            cur = cur->next;
            pre = pre->next;
        }
        return true;
        // 原题解析中是将链表恢复成原链表的状态后,再将结果返回的
    }
};

14 旋转链表-61

61. 旋转链表

给你一个链表的头结点 head ,旋转链表,将链表每个结点向右移动 k 个位置。

示例 1:

输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]

示例 2:

输入:head = [0,1,2], k = 4
输出:[2,0,1]

提示:

  • 链表中结点的数目在范围 [0, 500]

  • -100 <= Node.val <= 100

  • 0 <= k <= 2 * 109

14.1 成环分隔

思路:

如果 k 值是链表长度的整数倍,则移动前后链表相同;将单链表转成环形链表反复移动,找出移动后的尾结点。

假设 k 值是小于链表长度 len,则移动 k 次后,如何才能获取尾结点呢?现在的尾结点就是原链表中的倒数第 k 个,也就是第 n - k 个。

再将其后的结点断开,即可得到移动后的新链表。

class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        // 当移动次数为 0 或者 链表为空 或者 链表只有一个结点时,直接返回当前头结点
        if (k == 0 || head == NULL || head->next == NULL)
            return head;
        ListNode* cur = head;
        int len = 1;
        while (cur->next != NULL)
        {
            cur = cur->next;
            len++;
        }
        // 如果移动的次数能够被其长度整除,则链表头结点并没有改变
        // 若不能被整除,则要多移动的次数为其余数,要想获得尾结点,需要移动 len - k % len
        int add = len - k % len;
        if (add == len)
            return head;
        // 此时 cur 指针指向链表的最后一个结点,将尾结点指向头结点,构成环形链表
        cur->next = head;   // 变为环形链表
        // 从尾结点开始遍历,遍历 add 次,即找到移动后的链表尾结点,然后将其从后面断开,后面的结点为当前链表的头结点
        while (add--)
        {
            cur = cur->next;
        }
        ListNode* tmp = cur->next;
        cur->next = nullptr;
        return tmp; // 返回当前链表头结点
    }
};

14.2 分隔合并

思路:先遍历链表统计结点个数,若 len 能被 k 整除,则链表本身相当于没有移动,返回当前头结点;否则标记其余数为 n,找到倒数第 n 个点,将其前后断开,然后将后半段拿到前面,再连接即可得到移动后的链表。

class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if (k == 0 || head == nullptr || head->next == nullptr)
            return head;
        ListNode* fast = head;
        ListNode* slow = head;
        int len = 0;
        while (fast != nullptr)
        {
            fast = fast->next;
            len++;
        }
        fast = head;
        int n = k % len;
        if (n == 0)
        {
            return head;
        }
        while (n-- && fast != nullptr)
        {
            fast = fast->next;
        }
        while (fast->next != nullptr)
        {
            fast = fast->next;
            slow = slow->next;
        }
        ListNode* newhead = slow->next; // 新的头结点
        slow->next = nullptr;           // 将前后断开
        fast->next = head;              // 将后半段拿到前面
        return newhead;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值