0
点赞
收藏
分享

微信扫一扫

2.两数相加

天行五煞 2022-04-06 阅读 26

标题暂时这么起着,做多了再考虑要不要改吧。
写过很多乱七八糟的代码,但是算法实在做的不多。想要提升静下心来一题一题刷是没办法的事,一开始也只能做点简单题。我的想法是:做一题一定要做到位,能得到什么启发就记录下来,不要好高骛远,这也是之前所犯的错误。总结性的东西用颜色标注出来,复习的时候可以减少阅读时间。消化掉的才是好东西,简单题就少消化,难题就多消化,第一个小目标是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的头节点以保护他们其实是没必要的。不过这不是什么大问题,两个变量占不了什么空间。

以上就是今天的总结,一个偏向于数据结构基础的题吧。

举报

相关推荐

0 条评论