昨天写了做了几个list相关的leetcode的练习,今天再记录一个和list有关的题目,顺便练习一下递归操作。
目录
1、题目描述如下:
在介绍代码之前先来看一下leetcode中的单链表是怎么定义的。
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
单链表的定义方式如上述结构体所示。其主要定义了节点(node)的值val,以及该节点处(指向下一个位置)的指针next。代码第三行表示,可以采用初始化列表的方式来初始化一个链表,其形式如下:
ListNode dummy = ListNode(0);
利用这种方式进行初始化的时候,可以指定val的值,并且自动将next指向NULL。(在第一次做这个题的时候就因为这个返回值一直都是NULL,因为dummy是个空节点,其后指针也是NULL。我定义一个ListNode的指针,其指向了dummy.next,返回值也是dummy.next,,所以一直是空。)在做题的时候,还遇到了->操作符和.操作符。现先在总结其用法再对该题目进行讲解。
1.1 c++ -> 和. 操作符
ListNode dummy = ListNode(0);
/* 此处不能 ListNode *l3 = dummy.next;
* 因为在初始化dummy的时候,dummy.next指向的是空指针,
* 这样return的时候就只能得到空 */
ListNode *l3 = &dummy;
第一句,声明了一个ListNode的对象 dummy,并用初始化列表的方式对其进行初始化,然后又定义了一个ListNode类型的指针l3,指向了dummy的首地址。 dummy和l3都可以用来操纵val的值,但是两者操纵的方式不一样。
dummy.next 合法,l3.next不合法。
dummy是定义的一个结构体,对其成员变量的访问采用 . 操作符来进行,而l3是定义的一个指针,其对成员变量的访问需要采用 ->操作符来进行。网上有人总结说:
箭头(->):左边必须为指针;
点号(.):左边必须为实体。
网上有同学摘自c++ primer中的讲解如下:https://blog.csdn.net/fulima007/article/details/6327067
1.2 算法思路(新建list,不递归)
这种方法的思路比价简单,比较第一个list中的元素和第二个list中元素的大小,新建的list每次指向比较小的节点即可。
注意:1、题目中要求不能采用新建节点的形式。
2、指针的操作,需要自行检查空指针的情况!(一般的编译器,空指针会报错,越界不能)
代码如下:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode dummy = ListNode(0);
//此处不能 ListNode *l3 = dummy.next; 因为在初始化dummy的时候,dummy.next指向的是空指针,这样return的时候就只能得到空
ListNode *l3 = &dummy;
while(l2 && l1) {
if(l1->val < l2->val) {
//l3->val = l1->val;
l3 ->next = l1;
l1 = l1->next;
}
else {
//l3->val = l2->val;
l3 -> next = l2;
l2 = l2->next;
}
l3 = l3->next;
}
if(l1) {
l3->next = l1;
}
if(l2) {
l3->next = l2;
}
return dummy.next;
}
};
2、递归简介
递归最简单的理解就是自己调用自己。有一篇简单介绍递归的博文,写的比较好,很适合初学者。
https://blog.csdn.net/hustyangju/article/details/22758637
递归是一个比较常用的工具,但是因为递归要进行大量的堆栈操作,使用递归一般会进行优化,以后有时间专门出一个尾递归相关的专题,顺便提高一下自己对递归的理解程度。
本题目也可以用递归来实现,虽然在实现的时候递归操作不是最快的(递归8ms,不递归4ms),但是因为本题是对链表进行操作,所以递归的时间复杂度也是O(n)。因为链表不需要对递归后的结果进行重新排列。
2.1 递归思路
1、找到list 1 和 list 2 中比较小的节点,假如list 1 的第一个节点的值小于 list 2 中第一个节点的值,那么就将 list 1 和 list 2 都合并到list 1,其首节点就是list 1 的首节点,然后list 1 的next中存放 list 1 的第二个节点和list 2首节点中较小的那个,然后指针继续向后移动一直到有一个列表为空(就像穿糖葫芦一样,将两串排列好的糖葫芦重新从小到大依次穿起来)
2、当有一个list 为空以后,将重新排列的list的指针指向另一个非空的list即可,排列结束。
结合代码更容易理解,如下所示:
class Solution {
public:
ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) {
if(!l1 or !l2) return l1 ? l1 : l2; //必不可少,递归的过程中,如果某个列表空了,那/么返回没有空的那个列表剩余元素的首地址
if(l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next,l2);
return l1;
}
else {
l2->next = mergeTwoLists(l1,l2->next);
return l2;
}
}
};
注意:1、上述代码中,递归的终止条件为其中有一个list为空。此时返回那个非空列表的首地址即可(递归结束以后的首地址)。
2、每一层递归都能得到一个结果,都得有一个返回值,因此不论是if中还是在else中,都得返回本次递归得到的结果,即两个list中较小的那个,可以这样理解,每次的比较(即每一层递归),都相当于剔除一个最小的节点,这样两个list中剩余的节点就会越来越少。一直剔除到一个list中没有节点的时候,递归结束。剔除掉的元素都通过指针串接起来,得到了一个新的长list。递归的终止条件就表示,将那个还有节点的list一起串到新的list中。
也有人写了如下的递归代码:(整体思想都是一样的,只不过是又新建了一个list而已)
class Solution {
public:
ListNode* mergeTwoLists(ListNode *l1, ListNode *l2) {
if(l1 && l2) {
if(l1->val < l2->val) {
ListNode* l3 = l1;
l3->next = mergeTwoLists(l1->next,l2);
return l3;
}
else {
ListNode* l3 = l2;
l3->next = mergeTwoLists(l1,l2->next);
return l3;
}
}
if(l1) return l1;
if(l2) return l2;
return NULL;
}
};
文中如果有不对的专业名词,恳请大家批评指教。