笔记说明
说明:
该题目解答及搜集参考于:
其中,标有
*
表示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
给你一个链表的头结点
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
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的结点应该具备两个属性:
val
和next
。val
是当前结点的值,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 库。
调用
get
、addAtHead
、addAtTail
、addAtIndex
和deleteAtIndex
的次数不超过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
给你单链表的头结点
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
给你单链表的头指针
head
和两个整数left
和right
,其中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
给你一个链表,两两交换其中相邻的结点,并返回交换后链表的头结点。你必须在不修改结点内部的值的情况下完成本题(即,只能进行结点交换)。
示例 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
给你一个链表,删除链表的倒数第
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
给你两个单链表的头结点
headA
和headB
,请你找出并返回两个单链表相交的起始结点。如果两个链表不存在相交结点,返回null
。图示两个链表在结点
c1
开始相交:题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal
- 相交的起始结点的值。如果不存在相交结点,这一值为0
listA
- 第一个链表
listB
- 第二个链表
skipA
- 在listA
中(从头结点开始)跳到交叉结点的结点数
skipB
- 在listB
中(从头结点开始)跳到交叉结点的结点数评测系统将根据这些输入创建链式数据结构,并将两个头结点
headA
和headB
传递给你的程序。如果程序能够正确返回相交结点,那么你的解决方案将被 视作正确答案 。示例 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
如果
listA
和listB
没有交点,intersectVal
为0
如果
listA
和listB
有交点,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
给定一个链表的头结点
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
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。
示例 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
l1
和l2
均按 非递减顺序 排列
思路:
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
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 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
给你单链表的头结点
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
给你一个链表的头结点
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
有一个单链表的
head
,我们想删除它其中的一个结点node
。给你一个需要删除的结点node
。你将 无法访问 第一个结点head
。链表的所有值都是 唯一的,并且保证给定的结点node
不是链表中的最后一个结点。删除给定的结点。注意,删除结点并不是指从内存中删除它。这里的意思是:
给定结点的值不应该存在于链表中。
链表中的结点数应该减少 1。
node
前面的所有值顺序相同。
node
后面的所有值顺序相同。自定义测试:
对于输入,你应该提供整个链表
head
和要给出的结点node
。node
不应该是链表的最后一个结点,而应该是链表中的一个实际结点。我们将构建链表,并将结点传递给你的函数。
输出将是调用你函数后的整个链表。
示例 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
给你一个单链表的头结点
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
给你一个链表的头结点
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;
}
};