标题暂时这么起着,做多了再考虑要不要改吧。
写过很多乱七八糟的代码,但是算法实在做的不多。想要提升静下心来一题一题刷是没办法的事,一开始也只能做点简单题。我的想法是:做一题一定要做到位,能得到什么启发就记录下来,不要好高骛远,这也是之前所犯的错误。总结性的东西用颜色标注出来,复习的时候可以减少阅读时间。消化掉的才是好东西,简单题就少消化,难题就多消化,第一个小目标是200题。如果后续做题觉得实在容易,就一篇文章多塞几道题。整理时间另作决定。不做说明都是使用C++编写。
中等题
/**
* 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* addTwoNumbers(ListNode* l1, ListNode* l2) {
}
};
我先研究了一下力扣的输入输出格式。给了一个函数,参数是两个ListNode* 类型的,输出也是ListNode* 类型的。ListNode是定义好的结构体,ListNode* 就是结构体指针了。ListNode结构体有三个构造函数,可以不传参,传一个参,传两个参;有两个成员变量,值val和下一个节点next,也是结构体指针,完全符合链表的定义。如果在本地测试想写实例测试就很麻烦,要自己造链表一个个装节点,因为刚开始写不太熟练,我还是写了。
int main()
{
Solution sl = Solution();
//h1 = [8,3,9]
//用cl去创建新节点,用curosr去连接
ListNode* h1 = new ListNode(9); //头节点
ListNode* cursor = h1; //游标
ListNode* c1 = new ListNode(3);
cursor->next = c1;
cursor = c1;
c1 = new ListNode(8);
cursor->next = c1;
cursor = c1;
//h2 = [9,7,8]
ListNode* h2 = new ListNode(8);
cursor = h2;
c1 = new ListNode(7);
cursor->next = c1;
cursor = c1;
c1 = new ListNode(9);
cursor->next = c1;
cursor = c1;
//函数执行
ListNode* res = sl.addTwoNumbers(h1, h2);
//打印结果,用de接住前面的节点并释放,最后把h1和h2也释放了
while (res != nullptr) {
ListNode* de = res;
cout << res->val << endl;
res = res->next;
delete de;
}
while (h1 != nullptr) {
ListNode* de = h1;
h1 = h1->next;
delete de;
}
while (h2 != nullptr) {
ListNode* de = h2;
h2 = h2->next;
delete de;
}
return 0;
}
先暂时不看题目,先复习一下C++的new和delete。下面分条罗列:
- 原则。谁new的,谁delete,也就是创建者负责回收,不是自己创建的就不要回收;自己new的一定要注意管理好指针,至少要有一种方式(直接变量名访问也好,通过一个什么循环能找到也好)能找到new的这块空间,最后用完了释放一次,也不能多次释放。堆区、内存泄漏问题嘛。
- delete操作是删除指针所指向的那块空间存的东西,不是删除这个指针变量。这个指针变量还在,只是变成了空指针。
- 力扣的判题不考虑内存泄漏的问题,所以也不用delete。不过就算要delete,也是在主函数里(像我这里写的),不可能在要提交的函数里的,至于力扣自己怎么决定内存泄漏的事,就没必要太深究。以后遇到类似问题,先new了再说。
然后再复习一下遗忘的东西。nullptr是空指针,区别于null,大概有个初始值是不是0产生歧义的问题在里面。判断条件cur == nullptr可以换成!cur,上面没改。
复习完了,先贴第一遍怎么详细怎么来的代码:
//第一遍
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* cur1 = l1; //第一个数游标
ListNode* cur2 = l2; //第二个数游标
ListNode* res = new ListNode(-1); //结果头节点
ListNode* cur = res; //结果游标
int carry = 0; //进位
while ((cur1 != nullptr || cur2 != nullptr) || carry != 0) {
cur->next = new ListNode(0);
cur = cur->next;
if (cur1 == nullptr && cur2 == nullptr) {
//两个数都到头了,但进位不为0
cur->val = 1;
carry = 0;
}
else if (!cur1) {
//第一个数到头了
cur->val = (cur2->val + carry) % 10;
carry = (cur2->val + carry) / 10;
cur2 = cur2->next;
}
else if (!cur2) {
//第二个数到头了
cur->val = (cur1->val + carry) % 10;
carry = (cur1->val + carry) / 10;
cur1 = cur1->next;
}
else {
//两个数都没到头
cur->val = (cur1->val + cur2->val + carry) % 10;
carry = (cur1->val + cur2->val + carry) / 10;
cur1 = cur1->next;
cur2 = cur2->next;
}
}
res = res->next; //把第一个哨兵节点抠了
return res;
}
原始方法是彻底的分类讨论。两个游标同时向右移动,然后某个游标因为右边没有了(有效位用完了)就停止移动,直到两个数的有效位全部相加结束。还要考虑最后一位有进位的情况。这种想法类似二路归并里的归并步骤。
下面是官方解答,相当于我的优化版:
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *head = nullptr, *tail = nullptr;
int carry = 0;
while (l1 || l2) {
int n1 = l1 ? l1->val: 0; // 空指针直接赋0,省去讨论两个指针哪个有效位用完了
int n2 = l2 ? l2->val: 0; // 三目运算符,前后写赋值,中间写逻辑表达式
int sum = n1 + n2 + carry; // 直接求和
if (!head) {
head = tail = new ListNode(sum % 10); // 把头节点直接放在这里新建,省去创建-1这个无效节点
} else {
tail->next = new ListNode(sum % 10); // 直接用计算完成的值创建节点,省去先创建再赋值
tail = tail->next;
}
carry = sum / 10; // 进位无论如何都要更新
//右移
if (l1) {
l1 = l1->next;
}
if (l2) {
l2 = l2->next;
}
}
//最后有进位的情况放在后面讨论
if (carry > 0) {
tail->next = new ListNode(carry);
}
return head;
}
};
阅读示例代码,发现有非常多的改进之处。尽管是一道看起来很简单的题,但是有很大的压缩空间,所以我不厌其烦地记录了。启发如下:
- 三目运算符的使用。可以替换掉一个if赋值…else赋值,一定要想到后多使用,多用才能熟悉。
- 想办法把特殊情况转换成一般情况。如空指针无法加减,那么把空指针设定其值为0就可以加减。
- 逻辑表达式尽量写得简单。l1 == nullptr 这种就不要写了。
- 无效变量。显然我没有使用l1和l2两个变量,而是将其保存起来了,但到最后他们都没用上。如果按照示例代码的写法,l1和l2在循环过后,已经不再是这两个数的头节点了,换句话说,在这个函数里,往后已经没办法再去获得这两个数的头节点了。不过这个函数到此结束了,也不需要获得。而且在这个函数外还是能获得的(比如我的main函数中,h1和h2保存着这两个数的头节点),所以并无关系。在这道题中,像我那样复制了l1和l2的头节点以保护他们其实是没必要的。不过这不是什么大问题,两个变量占不了什么空间。
以上就是今天的总结,一个偏向于数据结构基础的题吧。