程序员老鬼

被裁后的第3个月,面试官问我空窗期在干嘛。 我说:"跑外卖。"他愣住了。我接着说:"送了1278单,超时率0.3%,差评0条。

刚看到个贴子:哥们儿被裁第三个月去面试,面试官问空窗期干嘛,他很实在地说去送外卖,还报了单量、超时率、差评数,结果全场安静。

Image

我觉得这事吧,尴尬的不是“跑外卖”,而是很多人骨子里还是嫌这活儿“不体面”。网友们的回复我看了看,有人觉得他不懂包装自己,有人说这才是真正的靠谱打工人。我更倾向后者。能把外卖送出KPI,把服务做到0差评,这种执行力、抗压能力,放哪家公司都是资产。

但话说回来,职场毕竟有偏见,面试场上还是要讲点“翻译话术”,比如说体验即时配送行业、做数据复盘之类,让对方听得舒服点。外卖不丢人,脑子清醒、有手有脚,比躺在家里刷手机强多了。

面试题:两数相加

你是不是也有过这种体验,晚上刷题刷到困得要死,结果一道“看起来很简单”的题,硬是缠了你一晚上,还不如一堆动态规划好受 😂 我第一次遇到“两数相加”这个题,就是这种感觉。跟我以前写数据库那篇文章的心情差不多。

题目大概意思是这样的(我不用官方那种教科书描述啊,直接说人话):

  • 有两条链表 l1 和 l2

  • 每个节点存的是 一位数字(0~9)

  • 数字是倒着存的,也就是:

    • 2 -> 4 -> 3 其实代表的是 342
    • 5 -> 6 -> 4 代表的是 465
  • 现在要你把这两个数相加,结果还是用同样格式的链表返回

举个例子:

  • l1 = 2 -> 4 -> 3 (342)
  • l2 = 5 -> 6 -> 4 (465)

342 + 465 = 807,结果链表要长这样:

  • 7 -> 0 -> 8(因为还是倒序存)

看起来就一个“加法”,但坑在哪? 链表长短可能不一样,还有进位,比如 9 + 9 = 18,这个进位怎么往后传,这是关键。

你想象一下我们小学算竖式:

   342
 + 465
 -----
   807

是不是从个位开始,一位一位往上加,有进位就往前传?

链表版本也是一模一样的套路:

  1. 准备一个 carry 表示进位,一开始是 0

  2. 同时从 l1 和 l2 的头结点开始往后走

  3. 每一位的计算:

  • 当前位的两个数字:x = (l1 == null ? 0 : l1.val)y = (l2 == null ? 0 : l2.val)
  • 总和:sum = x + y + carry
  • 新节点的值:sum % 10
  • 新的进位:carry = sum / 10
  • 把这个新节点接到结果链表后面

  • l1 / l2 往后挪一位

  • 最后循环结束后,如果 carry > 0,还要再补一个节点

  • 唯一有点绕的是“结果链表怎么优雅地拼起来”,一般都是用一个“虚拟头结点”(dummy head):

    • 先 new 一个头结点 dummy = new ListNode(0)
    • 用一个指针 cur 指向当前结果链表的尾巴
    • 每算出一个新节点,就 cur.next = 新节点,然后 cur = cur.next
    • 最后返回 dummy.next 就是结果链表的真正头结点

    这样就不用去单独处理“第一个节点”的特殊情况,写起来省心很多。

    先假设题目给的链表结构是这种:

    publicclassListNode{
    int val;
        ListNode next;
        ListNode(int x) { 
            val = x; 
        }
    }

    主逻辑代码可以这么写:

    publicclassSolution{

    public ListNode addTwoNumbers(ListNode l1, ListNode l2){
    // 虚拟头结点,方便操作
            ListNode dummy = new ListNode(0);
            ListNode cur = dummy;

    int carry = 0; // 进位

    // 只要有一个链表没走完,或者还有进位,就继续
    while (l1 != null || l2 != null || carry != 0) {
    int x = (l1 != null) ? l1.val : 0; // l1 当前位
    int y = (l2 != null) ? l2.val : 0; // l2 当前位

    int sum = x + y + carry; // 当前总和
                carry = sum / 10;        // 新的进位

    // 当前这一位的结果
                ListNode node = new ListNode(sum % 10);
                cur.next = node;
                cur = cur.next;

    // 往后挪
    if (l1 != null) {
                    l1 = l1.next;
                }
    if (l2 != null) {
                    l2 = l2.next;
                }
            }

    // 结果从 dummy.next 开始
    return dummy.next;
        }
    }

    你可以在脑子里跑一个例子:

    • 第一轮:2 + 5 + 0 = 7节点值 7,carry = 0
    • 第二轮:4 + 6 + 0 = 10节点值 0,carry = 1
    • 第三轮:3 + 4 + 1 = 8节点值 8,carry = 0
    • 两个链表都走完、carry 也没了,循环结束 链表就是 7 -> 0 -> 8,完全对上。

    这个题本身不难,但面试的时候很容易因为细节扣分:

    1. 链表长度不一样

      比如:

      我们的 while (l1 != null || l2 != null || carry != 0)加上 x = (l1 != null ? l1.val : 0) 这种写法,其实已经自然处理好了不同长度的问题。

    • l1 = 9 -> 9 -> 9(999)
    • l2 = 1(1)
  • 最后的进位

    经典用例:

    99 + 1 = 100,结果应该是 0 -> 0 -> 1如果你的循环条件写成 while (l1 != null || l2 != null)那最后那个进位 1 就没地方放了,会直接丢掉。

    所以循环条件里一定要把 carry != 0 加上。

    • l1 = 9 -> 9(99)
    • l2 = 1(1)
  • 不要想着把链表转成整数再加

    有些同学第一反应是: “要不我把链表转成 int,然后相加,再转回链表?”

    问题在于:

    所以正规写法就是上面这种“逐位相加 + carry”。

    • 链表可能很长,超过 int / long 范围就炸了
    • 而且题目本来就是想让你练习“模拟加法+链表操作”

    面试官有时候会顺嘴问一句,你就不要愣着了:

    • 时间复杂度:O(max(m, n))m、n 是两条链表的长度,因为每条链表各自只遍历一遍
    • 空间复杂度:O(1)(不算结果链表的话) 我们只用了常数级别的额外变量:carry、几个指针

    差不多就这样,这道“两数相加”你只要把“竖式加法”那个画面记住,代码其实就是在翻译你脑子里的竖式过程。

    -END-

    我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html