翻倍以链表形式表示的数字 图解题解
这道题到底在问什么
- 输入
- head = [1,8,9]
- 输出
- [3,7,8]
- 输入
- head = [9,9,9]
- 输出
- [1,9,9,8]
最优解:一步一步想明白
- 3记牢这条主线:反转让个位在前,从个位一路翻倍进位,最后别忘了收尾那一次进位,再反转回来就是答案。为什么不能直接把链表拼成一个整数算?因为节点最多一万个,这个数会大到任何整型都装不下,只能逐位算。下面从 189 开始。
- 4目标:189 乘 2先看这条输入链表,从左到右是 1,8,9,表头 1 是最高位、末尾 9 是个位,合起来就是 189。末尾的 null 表示到头了。我们的目标是把它乘以 2,答案应该是 378,后面一步步把这个 378 拼出来。
- 5反转到个位在前翻倍是竖式乘法,得从个位算起,可现在个位 9 躲在链表最后。所以第一步把链表整体反转,变成 9,8,1,这样个位 9 就来到了表头,后面从左往右扫就是从低位往高位算,进位也顺着往右带,非常顺手。
- 6进位=0 · 倍数=2现在摆开两行看。上面这行是反转后的输入 9,8,1,用一个当前指针从个位开始往右读;下面这行是马上要生成的结果链,现在还是空的。约定一个进位记号,初始是 0。倍数固定是 2。准备好了,读第一位。
- 7个位 ×2 = 18当前指针停在个位的 9。这一位算 9 乘 2 加上进位 0,等于 18。它到了两位数,说明这一位会产生进位。把结果拆成个位和进位两部分。
- 8进位=1 · 结果 8把 18 拆开:个位是 8,接到结果链末尾;十位 1 就是要带给下一位的进位。这次进位是 1,下一位要多加 1。结果链现在是 8。上面这一位读过了,置灰,当前指针准备右移。
- 9十位 ×2 = 17当前指针停在十位的 8。这一位算 8 乘 2 再加上上一位带来的进位 1,等于 17。它到了两位数,说明这一位会产生进位。把结果拆成个位和进位两部分。
- 10进位=1 · 结果 8,7把 17 拆开:个位是 7,接到结果链末尾;十位 1 就是要带给下一位的进位。这次进位是 1,下一位要多加 1。结果链现在是 8,7。上面这一位读过了,置灰,当前指针准备右移。
- 11百位 ×2 = 3当前指针停在百位的 1。这一位算 1 乘 2 再加上上一位带来的进位 1,等于 3。它还是一位数,这次没有进位。把结果拆成个位和进位两部分。
- 12进位=0 · 结果 8,7,3把 3 拆开:个位是 3,接到结果链末尾;十位 0 就是要带给下一位的进位。这次进位是 0,下一位照常算。结果链现在是 8,7,3。上面这一位读过了,置灰,当前指针准备右移。
- 13收尾 · 进位=0输入三位全部读完,收尾要看进位。现在进位是 0,不用再补新节点。结果链此刻是 8,7,3,它是低位在前的顺序,也就是把 378 倒着放。最后一步,只要把它反转回来。
- 14答案 378把低位在前的结果链 8,7,3 整体反转回来,得到 3,7,8,高位重新回到表头,正好表示 378。189 乘 2 等于 378,和一开始说的答案对上了。这就是主流程:反转、逐位翻倍进位、收尾、再反转。
- 15目标:999 乘 2再看一个更有意思的例子,9,9,9 表示 999。它每一位都是 9,翻倍时会连续进位,而且最高位也会溢出,正好演示前面说的补最高位那一步。答案应该是 1998。
- 16进位=0 · 倍数=29,9,9 是个回文,反转以后还是 9,9,9,但个位在前这一步不能省,规则照旧。摆开两行,进位从 0 起。开始逐位读。
- 179 ×2 = 18当前读到第一个 9。算 9 乘 2 加进位 0,等于 18,又是个两位数,还会往上进 1。拆成个位和进位。
- 18进位=1 · 结果 8个位 8 接到结果链,进位保持为 1。结果链现在是 8。进位继续往下一位带。
- 199 ×2 = 19当前读到第二个 9。算 9 乘 2 加进位 1,等于 19,又是个两位数,还会往上进 1。拆成个位和进位。
- 20进位=1 · 结果 8,9个位 9 接到结果链,进位保持为 1。结果链现在是 8,9。进位继续往下一位带。
- 219 ×2 = 19当前读到最高位 9。算 9 乘 2 加进位 1,等于 19,又是个两位数,还会往上进 1。这是输入的最高位,读完它就要看收尾进位了。拆成个位和进位。
- 22进位=1 · 结果 8,9,9个位 9 接到结果链,进位保持为 1。结果链现在是 8,9,9。注意此刻输入读完了,进位还挂着一个 1,不能丢。
- 23补进位 · 结果 8,9,9,1收尾这一步是最容易漏的。输入三位读完,进位还是 1,说明最高位翻倍溢出了。必须给结果链再补一个节点,值就是这个进位 1。补完结果链变成 8,9,9,1,这就是为什么 999 乘 2 会从三位变成四位。
- 24答案 1998把低位在前的 8,9,9,1 反转回来,得到 1,9,9,8,正好是 1998。刚才补的那个进位 1,反转后落在了表头,成了新的最高位。999 乘 2 等于 1998,一位不差。
- 25进位只会是 0 或 1两个例子回顾一下。189 乘 2 得 378,每位翻倍带着进位往高位走;999 乘 2 得 1998,最高位溢出还补了一个节点。有个规律值得记:每位是 0 到 9,翻倍最多 18,再加上一个进位也就 19,所以进位永远只会是 0 或 1,绝不会更大。抓住反转、逐位翻倍进位、收尾补进位、再反转这四步,这题就稳了。
⚠️ 容易写错的地方
✗ 错:把整条链拼成一个整数,乘 2 再拆回去
✓ 对:逐位翻倍,进位手动往高位带
节点最多一万个,拼出来的数远超 long 甚至任何定长整型能表示的范围,一转就溢出出错。必须逐位算
✗ 错:走完最后一位就返回,忘了收尾进位
✓ 对:循环结束后若进位仍大于 0,补一个最高位节点
像 999 乘 2 这种,最高位翻倍会溢出,漏掉收尾进位会把答案从 1998 变成 998,少了最高位
✗ 错:从高位(表头)开始翻倍
✓ 对:先反转让个位在前,从低位开始翻倍
进位是从低位往高位传的,从高位算你没法提前知道低位会不会进上来,顺序就乱了
✗ 错:结果链忘了反转回来
✓ 对:逐位生成的是低位在前的链,最后要反转
从个位开始生成,结果自然是低位在前,直接返回会把 378 变成 873,数值全错
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def doubleIt(self, head: Optional[ListNode]) -> Optional[ListNode]:
def reverse(head):
dummy = ListNode()
cur = head
while cur:
next = cur.next
cur.next = dummy.next
dummy.next = cur
cur = next
return dummy.next
head = reverse(head)
dummy = cur = ListNode()
mul, carry = 2, 0
while head:
x = head.val * mul + carry
carry = x // 10
cur.next = ListNode(x % 10)
cur = cur.next
head = head.next
if carry:
cur.next = ListNode(carry)
return reverse(dummy.next)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
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) {}
};
/**
* 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* doubleIt(ListNode* head) {
head = reverse(head);
ListNode* dummy = new ListNode();
ListNode* cur = dummy;
int mul = 2, carry = 0;
while (head) {
int x = head->val * mul + carry;
carry = x / 10;
cur->next = new ListNode(x % 10);
cur = cur->next;
head = head->next;
}
if (carry) {
cur->next = new ListNode(carry);
}
return reverse(dummy->next);
}
ListNode* reverse(ListNode* head) {
ListNode* dummy = new ListNode();
ListNode* cur = head;
while (cur) {
ListNode* next = cur->next;
cur->next = dummy->next;
dummy->next = cur;
cur = next;
}
return dummy->next;
}
};Java
import java.util.*;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public ListNode doubleIt(ListNode head) {
head = reverse(head);
ListNode dummy = new ListNode();
ListNode cur = dummy;
int mul = 2, carry = 0;
while (head != null) {
int x = head.val * mul + carry;
carry = x / 10;
cur.next = new ListNode(x % 10);
cur = cur.next;
head = head.next;
}
if (carry > 0) {
cur.next = new ListNode(carry);
}
return reverse(dummy.next);
}
private ListNode reverse(ListNode head) {
ListNode dummy = new ListNode();
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = dummy.next;
dummy.next = cur;
cur = next;
}
return dummy.next;
}
}复杂度
时间
O(n)
n 是链表节点数。反转一遍 O(n),逐位翻倍再走一遍 O(n),最后反转回来又 O(n),三段都是线性,合起来随节点数线性增长
空间
O(1)
按峰值算,不计返回的结果链。两次反转都是原地改指针,只用常数个指针变量;进位 carry 也是一个数。除了必须建出来的答案链,额外空间是常数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 翻倍以链表形式表示的数字 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心做法是什么?+
因为竖式乘 2 要从个位算起,先把链表反转让个位在前;然后从个位逐位翻倍,每位算数字乘 2 加进位,写下个位数、把进位带给下一位;走完若进位还在就补一个最高位节点;最后把结果反转回来。全程逐位处理,不受数值大小限制。
有没有不反转链表的更省空间做法?+
有。注意翻倍时每位的进位只可能是 0 或 1,而且某位会进位当且仅当它的下一位大于等于 5。于是可以从高位一遍扫过去:先看头节点是否大于等于 5,是就先补一个值为 1 的新头;再对每个节点把值改成它乘 2 对 10 取余,若它的下一个节点大于等于 5 就再加 1。这样不反转、原地一遍完成,额外空间是常数,和参考解思路等价、复杂度相同。
复杂度是多少?+
时间都是 O(n),不管反转两遍还是一遍扫,都是线性。空间上,反转法两次反转原地进行,不计返回链是 O(1);一遍扫的写法同样是 O(1) 额外空间。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 翻倍以链表形式表示的数字 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。