提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
总结⼀下单链表的基本技巧
一、合并两个有序链表
你输⼊两个有序链表,请你把他俩合并成⼀个新的有序链表;
例题:
上代码(通过代码分析过程):
/**
* 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* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy=new ListNode(-1),*p=dummy;//虚拟头结点 p 指向 dummy 所指向的对象,也就是虚拟头结点。这样,在之后的操作中,可以通过 p 来访问或者修改虚拟头结点的值或者指向
ListNode* p1=list1,*p2=list2;//定义了两个指针变量 p1 和 p2,分别指向两个输入的链表 list1 和 list2 的头部节点。这是为了在之后的代码中对两个链表进行遍历合并操作。
while(p1!=nullptr&&p2!=nullptr){//当p1和p2指向的链表中都有值时分别对两个链表中的每一个值进行比较
if(p1->val>p2->val){//比较p1和p2两个指针
p->next=p2;//将值较小的节点接到p指针
p2=p2->next;//p2往下顺移
}
else{
p->next=p1;
p1=p1->next;}
p = p->next;//更新p指向的对象
}
if(p1==nullptr){
p->next=p2;
}
if(p2==nullptr){
p->next=p1;
}
return dummy->next;// 将合并后的链表头节点返回
}
};
图解:
我们的while循环每次比较p1和p2的大小,将较小的节点接到结果链表上
二、单链表分解
在合并两个有序链表时让你合二为一,而这里需要分解让你把原链表一分为二。具体来说,我们可以把原链表分成两个小链表,一个链表中元素大小都小于x,另一个链表中元素都大于等于x,最后把这两条链表接到一起,就得到了结果。
例题:
上代码:
/**
* 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* partition(ListNode* head, int x) {
ListNode* dummy1=new ListNode(-1);
ListNode* dummy2=new ListNode(-1);
ListNode *p1=dummy1;
ListNode *p2=dummy2;
ListNode *p=head;
while(p!=nullptr){
if(p->val>=x){
p2->next=p;
p2=p2->next;
}
else{
p1->next=p;
p1=p1->next;
}
ListNode* temp=p->next;
p->next=nullptr;
p=temp;
}
p1->next=dummy2->next;
return dummy1->next;
}
};
当我们要根据给定值 x
对链表进行分割时,我们需要创建两个新的链表,一个链表存储小于 x
的节点,另一个链表存储大于等于 x
的节点。在这个过程中,我们需要保持节点的相对顺序不变。
-
创建虚拟节点:我们首先创建两个虚拟节点
dummy1
和dummy2
。这些虚拟节点的目的是为了简化代码逻辑,并且在最后的连接过程中能够更方便地处理边界情况。 -
初始化指针:我们初始化四个指针,
p1
和p2
分别指向虚拟节点dummy1
和dummy2
,用于将小于x
和大于等于x
的节点连接到对应的链表上。p
指向原始链表的头节点,用于遍历原始链表。temp
是一个临时指针,用于保存下一个要处理的节点。 -
遍历原始链表:我们使用一个
while
循环来遍历原始链表head
。在循环中,我们逐个处理原始链表的节点。 -
节点分割:对于当前节点
p
,如果其值大于等于x
,我们将其连接到dummy2
链表上,并更新p2
指针。如果其值小于x
,我们将其连接到dummy1
链表上,并更新p1
指针。 -
移动指针:在处理完当前节点后,我们需要将
p
移向下一个节点。为了避免形成环,我们需要先保存下一个节点的指针,然后将当前节点的next
指针置空,最后将p
更新为下一个节点。 -
连接分割后的链表:当遍历完成后,我们需要将
dummy1
链表的尾部连接到dummy2
链表的头部,形成最终的分割链表。 -
返回结果:最后,我们返回
dummy1
的下一个节点,即分割后的链表的头节点。
这段代码比较容易有疑问
ListNode* temp = p->next; // 保存当前节点 p 的下一个节点指针
p->next = nullptr; // 将当前节点 p 的 next 指针置为空,断开与下一个节点的连接
p = temp; // 将 p 更新为下一个节点,继续遍历链表
逐行解释:
ListNode* temp = p->next;:这一行代码将当前节点 p 的下一个节点的指针保存在临时变量 temp 中。这是为了在断开当前节点与下一个节点的连接之前先保存下一个节点的指针,以便在断开连接后能够继续遍历链表。
p->next = nullptr;:这一行代码将当前节点 p 的 next 指针设置为 nullptr,即将当前节点与下一个节点断开连接。这是为了避免在将当前节点连接到新链表时形成环。
p = temp;:这一行代码将 p 更新为下一个节点的指针,以便继续遍历链表。通过将 p 更新为 temp,我们实际上将 p 移动到了下一个节点,从而继续了链表的遍历过程。
再详细点:
当我们遍历链表时,我们通常需要在处理当前节点之后继续处理下一个节点。在这个过程中,我们需要小心处理节点之间的连接关系,以免丢失节点或形成环。
让我们以具体的例子来解释这段代码:
假设我们有一个链表 1 -> 4 -> 3 -> 2 -> 5
,并且 x = 3
。
- 初始时,
p
指向头节点1
。 - 第一次循环:
p->val
是小于3
的,所以我们将1
连接到dummy1
链表后面,同时更新p1
指针为1
。- 然后,我们进入下一个循环。现在,
p
指向4
。
- 第二次循环:
p->val
是大于等于3
的,所以我们将4
连接到dummy2
链表后面,同时更新p2
指针为4
。- 然后,我们进入下一个循环。现在,
p
指向3
。
- 第三次循环:
p->val
是大于等于3
的,所以我们将3
连接到dummy2
链表后面,同时更新p2
指针为3
。- 然后,我们进入下一个循环。现在,
p
指向2
。
- 第四次循环:
p->val
是小于3
的,所以我们将2
连接到dummy1
链表后面,同时更新p1
指针为2
。- 然后,我们进入下一个循环。现在,
p
指向5
。
- 第五次循环:
p->val
是大于等于3
的,所以我们将5
连接到dummy2
链表后面,同时更新p2
指针为5
。- 然后,我们进入下一个循环。现在,
p
指向nullptr
,循环结束。
在处理完每个节点后,我们需要将 p
移向下一个节点。为了做到这一点,我们首先保存当前节点 p
的下一个节点指针 temp
。然后,我们将当前节点的 next
指针置为 nullptr
,将其与下一个节点断开连接,以避免形成环。最后,我们将 p
更新为 temp
,将其移向下一个节点,继续遍历链表。
这样,我们可以安全地在遍历链表时修改节点的连接关系,而不会丢失节点或形成环。
三、合并k个有序链表
合并k个有序链表的逻辑类似于合并两个有序链表,关键在于如何快速得到k个节点中的最小节点,接到结果链表上;
这种时候就要用到优先级队列(二叉堆)这种数据结构,把链表节点放入一个最小堆,就可以每次获得k个节点中的最小节点。
题目:
上代码:
/**
* 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* mergeKLists(vector<ListNode*>& lists) {
int size=lists.size();
ListNode* dummy=new ListNode(-1);
ListNode* p=dummy;
priority_queue<ListNode*,vector<ListNode*>,compare>pq;
for(auto head:lists){
if(head) pq.push(head);
}
while(!pq.empty()){
auto node=pq.top();
pq.pop();
p->next=node;
if(node->next){
pq.push(node->next);
}
p=p->next;
}
return dummy->next;
}
private:
struct compare{
bool operator()(ListNode*a,ListNode*b){
return a->val>b->val;
};
};
};
这段代码实现了一个合并 k 个有序链表的算法。更详细地解释代码的逻辑:
-
初始化:
- 创建一个虚拟头结点
dummy
,它的值为 -1。 - 创建一个指针
p
,指向虚拟头结点。它用于构建结果链表。
- 创建一个虚拟头结点
-
创建优先队列:
- 创建一个优先队列
pq
,它是一个最小堆(最小优先级队列)。 - 使用自定义的比较函数
compare
来定义节点的比较规则。在这里,节点的值小的优先级更高。
- 创建一个优先队列
-
加入初始节点:
- 遍历
lists
中的每个链表头结点,并将非空的头结点加入到优先队列中。
- 遍历
-
合并链表:
- 进入循环,直到优先队列为空。
- 在每次循环中,从优先队列中取出最小的节点
node
,即值最小的节点。 - 将节点
node
接到结果链表的末尾,并将指针p
向后移动。 - 如果节点
node
的下一个节点不为空,则将其下一个节点加入到优先队列中,以确保优先队列中始终是 k 个链表的头结点中最小的。 - 重复上述步骤,直到优先队列为空。
-
返回结果:
- 返回虚拟头结点
dummy
的下一个节点,即合并后的有序链表的头结点。
- 返回虚拟头结点
通过使用优先队列,每次都可以从 k 个链表的头部中选择值最小的节点,从而保证了合并后的链表仍然是有序的。这种算法的时间复杂度为 O(Nlogk),其中 N 是所有链表中节点的总数,k 是链表的数量。
其中在这个自定义的比较函数 compare
中,operator()
函数定义了对两个 ListNode*
指针进行比较的方式。在这里,它使用了节点的值 val
进行比较,具体逻辑如下:
- 如果
a->val
的值比b->val
大,则返回true
。 - 如果
a->val
的值比b->val
小,则返回false
。
由于这是在一个优先队列中使用的比较函数,而优先队列是一个最小堆(最小优先级队列),因此在这个比较函数中,如果返回 true
,表示 a
的优先级更低(值更大),在优先队列中的位置更靠后;如果返回 false
,表示 a
的优先级更高(值更小),在优先队列中的位置更靠前。
因此,这个比较函数的逻辑是,节点的值小的优先级更高。
二叉堆详解实现优先级队列: