作者:热爱编程的小y
专栏:C语言数据结构
座右铭:能击败你的只能是明天的你
OJ.1
题目
206. 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2] 输出:[2,1]
示例 3:
输入:head = [] 输出:[]
解析
先画图分析一波
如图所示,要想实现链表的反转,我们需要让第一个节点与NULL链接,第二个节点与第一个节点链接,第三个节点与第二个节点链接……以此类推。根据以上步骤,我们需要一个参数来表示当前节点,还需要一个参数标记前一个节点的位置用于链接,链接完成之后还需要将节点后移,因此还需要一个参数标记后一个节点的位置。由此不难设出三个指针变量n1,n2,n3。n2指向三者的中间节点,它负责重新链接,因此n2的初始位置应该在第一个节点处,那么n3初始位置就在第二个节点处,n1的位置没有节点,因此它的初始值为NULL。
当n2链接完成之后,也就是 n2->next = n1 之后,n1来到n2的位置;也就是 n1 = n2 ,n2来到n3的位置,也就是 n2 = n3;最后n3来到下一个节点处,n3 = n3->next。
如此往复几次之后n3肯定会来到一个NULL的位置,此时循环并没有停止,因为n2刚来到最后一个节点处,最后一个节点还未完成重新链接,因此只有当n2来到NULL的位置时才停止循环。
但这时候就有个问题,n3 = NULL 时没有停止的话,n3->next 还需要继续赋值给n3,但此时n3->next 已经不存在了,程序就会出现问题,因此需要加上一个判断条件,当n3为NULL时,直接返回值。
最后由于我们设了三个指针变量,因此链表至少含有两个节点,那么就需要考虑0节点和1节点的情况
由此可得最终代码如下:
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL)
return NULL;
if(head->next == NULL)
return head;
struct ListNode* n1 = NULL;
struct ListNode* n2 = head;
struct ListNode* n3 = head->next;
while(n2 != NULL)
{
n2->next = n1;
n1 = n2;
if(n3 == NULL)
return n2;//n3为空直接返回值
n2 = n3;
n3 = n3->next;
}
return n2;
}
OJ.2
题目
21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] 示例 2:
输入:l1 = [], l2 = [] 输出:[] 示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
如图所示,合并两个链表。有两种思路,一种是在链表1或者链表2内部插入另外一个链表,但这就涉及了在链表中间插入,要找到前一节点以及后一节点,还要考虑头节点和尾节点的情况,因此代码实现起来十分复杂,动辄八九十甚至上百行代码。既然如此,那我们何不另寻一种更简单高效的方法呢。
第二种思路就是额外创立一个新链表,因为链表和数组不同,链表的额外开辟成本很低,不需要考虑空间、长度,只需要一个新的节点即可。
方法1
我们定义指针d1,d2分别用于标记链表list1,list2的节点的位置。之后定义一个空指针head表示新链表的头节点位置,存放链表list1,list2的首节点的较小值。定义一个空指针tail表示新链表尾节点的位置,初始值与head相同。
若d1较小,则d1指向下一个节点,若d2较小,则d2指向下一个节点,之后tail指向的节点链接d1、d2的较小值,tail再指向下一个节点。如此往复直到d1,d2其一出现NULL值停止,再把没有遍历完的链表接到新链表的后面,即可完成合并。
由此可得代码如下:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
struct ListNode* d1 = list1;
struct ListNode* d2 = list2;
struct ListNode* head = NULL;
struct ListNode* tail = NULL;
while(d1 && d2)
{
if(d1->val >= d2->val)
{
if(head == NULL)
{
head = tail = d2;//初始化头尾指针
}
else
{
tail->next = d2;
tail = tail->next;//tail指向下一节点
}
d2 = d2->next;//d2指向下一节点
}
else
{
if(head == NULL)
{
head = tail = d1;//初始化头尾指针
}
else
{
tail->next = d1;
tail = tail->next;//tail指向下一节点
}
d1 = d1->next;//d1指向下一节点
}
}
if(d1)
{
tail->next = d1;
}
else if(d2)
{
tail->next = d2;
}
return head;
}
方法2
方法1声明head,tail指针之后对于不同情况需要对head,tail赋不同的值,那么有没有一种不需要赋值的方法呢,有,那就是哨兵位的头节点。
哨兵位的头节点就是额外开辟的一个值NULL的新节点guard,d1,d2判断完之后直接与之链接,就不存在赋值操作。理论上来说哨兵位头节点只需要重新定义一个结构体即可,但实际上需要用malloc函数为它开辟空间,并在使用完毕之后释放掉它。
我们在开辟空间的时候,让tail与guard指向同一块空间,这样做既保存了头节点的位置还能确保后续完成链接,只需要让tail指针向后指向即可。
由此可得代码如下:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* d1 = list1;
struct ListNode* d2 = list2;
struct ListNode* guard = NULL;
struct ListNode* tail = NULL;
guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode));//guard与tail指向同一块空间
tail->next = NULL;//初始化
while(d1 && d2)
{
if(d1->val >= d2->val)
{
tail->next = d2;
tail = tail->next;
d2 = d2->next;
}
else
{
tail->next = d1;
tail = tail->next;
d1 = d1->next;
}
}
if(d1)
{
tail->next = d1;
}
else if(d2)
{
tail->next = d2;
}
struct ListNode* head = guard->next;//保存头节点位置
free(guard);
guard = NULL;//释放内存,赋值为空
return head;
}