链式铠甲——合体
一、题目描述
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
示例 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 均按 非递减顺序 排列
看完了本题的描述,你是否觉得这个题目在哪里做到过。是的,对于这道题目,我们曾经有做过类似的力扣习题,而且我也做了题解说明,就是力扣88 - 合并两个有序数组,没看过的同学可以先去看看这个,对于数组的合成比链表简单一些
二、思路分析
好,看完题目的描述,我们来分析一下如何去求解这道题目
- 思路很简单,合并链表,那就是要将两个链表合为一个,这里没有说最后归并到第一个还是第二个,所以我们需要重新定义一个链表,然后进行尾插结点的操作
- 一同遍历这两个链表,比较所遍历到的结点,看看那个结点的值小,将小的值链接到新的链表中,然后直到有一个链表遍历到了结尾,就结束两个链表的同时遍历,跳出循环
- 接着可能有一个链表还没有遍历完,只需要将没遍历完的那个链表继续链在新链表最后即可,因为题目本身说明了给出的链表就是有序的,所以不需要担心
- 还有一个的话就是带头结点的,对于这种思路的代码写起来简单一些,因为不需要考虑一开始尾插结点的时候尾指针是否为空,只需要将结点做一个尾插即可,具体的分析我们到下一模块讲
三、代码详解
然后我们通过这段代码来给大家分析一下
way1【不带头结点】
- 首先是一些初始化
ListNode* tail, *newhead;
tail = newhead = NULL;
- 下面是循环遍历的逻辑,可以看出来,代码量非常得多其实就是在判断是否是第一个尾插的结点
while(list1 && list2)
{
if(list1->val < list2->val)
{
if(tail == NULL)
{
tail = newhead = list1;
}
else
{
tail->next = list1;
tail = tail->next;
}
list1 = list1->next;
}
else
{
if(tail == NULL)
{
tail = newhead = list2;
}
else
{
tail->next = list2;
tail = tail->next;
}
list2 = list2->next;
}
}
- 首先是不带头结点这一块,刚开始做一个初始化
- 然后看看1 = 1,随便拿哪个都可以,我们拿【list2】,然后将其后移,此时【tail】无需动
- 然后拿【list1】,list1继续后移,tail后移
- 同理
- 同理
- 然后此时可以看到,两个链表都只剩下最后最后一个结点,我们继续看
- 可以看到,当这个【list1】遍历完了之后,我们此时应该跳出循环,将【list2】的剩余结点全部链接到【list3】中
- 使用的是下面这段代码逻辑
if(list1)
tail->next = list1;
if(list2)
tail->next = list2;
- 此时也就结束了我们的有序链表合并
- 最后将这个【List3】指向首个结点的指针返回即可
return newhead;
- 但是。。。出来的确实这个,这是为什么呢?
- 此时你就要仔细分析力扣给出来报错,也就是两个链表:一个为空,一个只有一个0的值。这个时候再回过去看你的代码其实就可以发现,因为有一个链表初始的时候就是空的,所以不会进入上面那段大的循环逻辑,因此会直接进到下面这个最后的尾插,但是可以看到力扣给出的错误定位提示,因为我们在一开始的时候将这个【tail】和【newhead】都设置为NULL了,所以在上面没有修改tail的时候进到这里对tail使用【tail->next】其实就是一个解引用的操作,对空指针进行解引用其实是一个很错误的写法,因此程序给我们报出了错误,那应该怎么去修改呢?
- 你可以在后面这段if判断的逻辑里修改,当然也可以在程序一开头就做一个判读,就像下面这样
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
- 可以看到,顺利AC🎈,抬走,下一个
way2【带头结点】
- 说完不太头结点的方法,接下去我们来说说带头结点的情况,这种情况的代码可比不太头结点来得简洁多了,因为不需要考虑第一次尾插的结点是否为头结点
- 首先先来看一下内部的循环逻辑该如何修改。可以看到,当这个结点值的大小判断完后就直接进行了一个尾插,然后就是结点指针的移动,完全不需要另外进行一个判断,代码就看起来很简练
while(list1 && list2)
{
if(list1->val < list2->val)
{
tail->next = list1;
tail = tail->next;
list1 = list1->next;
}
else
{
tail->next = list2;
tail = tail->next;
list2 = list2->next;
}
}
- 有关如何进行插入就如下图所示,我就不做分步讲解了,和不带头结点类似,若是比较到哪个结点小,直接尾插在List3之后即可
- 有一点比较重要的我讲一下,就是当最后需要返回头结点指针的时候,返回的不是List3,而是【List3->next】,那有同学问,这是为什么呢?我们现在看一看结点指针的初始化
- 这里新的头结点指针我使用的是newhead,这个没关系,大家可以自己命名,可以看到一开始就为【tail】和【newhead】这两个结构体指针进行了一个初始化操作,也就是在堆区中为其申请出一块空间,但是并没有将它们的【data域】和【next域】进行一个初始化赋值,因为这个是带头结点的,因此不能赋值成NULL,只有不太头的可以这么多,于是这两个域就会变成随机值和随机地址值,不信的话我带你们去DeBug调试看看
ListNode* tail, *newhead;
tail = newhead = (ListNode *)malloc(sizeof(ListNode));
newhead->next = NULL;
- 可以看到,当这个两个指针被初始化后是一个随机值,所以后续的结点其实是尾插在这个头结点之后的,因此提交之后就会变成下面这样,这其实就出现了问题,其实我们应该要返回的是当前头结点的下一个结点,才是正确的返回结果
- 因此我们可以总结出,对于带头结点的单链表,返回的当前newhead->next,但是这里为了使程序更加完整,我们应该在返回后将这个头结点释放掉,这样就可以将这块申请的内存地址还回去了,程序也显得比较完善
- 我们也可以总结出来对于带头结点的单链表这个头结点其实只是起到一个辅助尾插的作用,链表真正的结构还是从头结点的下一结点开始
ListNode* nextNode = newhead->next;
free(newhead); //将新的头结点销毁,返回其下一个结点
return newhead; //返回原来头结点的下一结点
好,在看完了两种方法之后,相信你对带头结点和不带头结点的单链表应该有了一个很好的认识,下去也要自己多加练习才能融会贯通
四、整体代码展示【需要自取】
给出两种方法的代码
方法一:不带哨兵位【无头结点】
/**
* 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) {
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
ListNode* tail, *newhead;
tail = newhead = NULL;
while(list1 && list2)
{
if(list1->val < list2->val)
{
if(tail == NULL)
{
tail = newhead = list1;
}
else
{
tail->next = list1;
tail = tail->next;
}
list1 = list1->next;
}
else
{
if(tail == NULL)
{
tail = newhead = list2;
}
else
{
tail->next = list2;
tail = tail->next;
}
list2 = list2->next;
}
}
if(list1)
tail->next = list1;
if(list2)
tail->next = list2;
return newhead;
}
};
方法二:带哨兵位【有头结点】
/**
* 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) {
//无需判断其中一个链表是否为空,因为tail不可能为空
ListNode* tail, *newhead;
tail = newhead = (ListNode *)malloc(sizeof(ListNode));
newhead->next = NULL;
while(list1 && list2)
{
if(list1->val < list2->val)
{
tail->next = list1;
tail = tail->next;
list1 = list1->next;
}
else
{
tail->next = list2;
tail = tail->next;
list2 = list2->next;
}
}
if(list1)
tail->next = list1;
if(list2)
tail->next = list2;
ListNode* nextNode = newhead->next;
free(newhead); //将新的头结点销毁,返回其下一个结点
return nextNode; //返回原来头结点的下一结点
}
};
五、总结与提炼
- 好,我们来总结一下本文所学习的知识,在本文中,我们通过一道力扣题【合并两个有序链表】,讲解了对于单链表不带头结点和带头结点的不同做法:对于不带头结点,需要在第一次尾插的时候判断一下尾结点指针是否为空,在后续再进行一个尾插;对于带头结点的单链表,我们在进行结点尾插的时候直接进行尾插即可,但是在最后返回整个链表的头时,不能返回我们开辟出来的头结点指针,而是要返回其next的结点,才是链表真正的开始部分
- 对于链表带头和不带头大家一定多加练习,在力扣上大多都是不太头的,你要做到的是灵活地修改,将不带头的可以轻松转换为带头的
以上就是本文所要描述的所有内容,感谢您对本文的观看,如有疑问请于评论区留言或者私信我都可以🍀
本文含有隐藏内容,请 开通VIP 后查看