1 概要
問題: 2つの非空連結リストで表された非負整数を加算し、結果を連結リストで返す。各ノードは単一の桁を保持し、桁は逆順で格納されている。
例:
l1 = [2,4,3], l2 = [5,6,4] → [7,0,8]
(342 + 465 = 807)
アルゴリズム戦略: 2つのリストを同時走査し、各桁の和と繰り上がり(carry)を計算。番兵ノードとテールポインタでO(1)追加を実現。
- 時間計算量: O(n) - n = max(len(l1), len(l2))
- 空間計算量: O(1) - 出力ノードを除く補助空間
- データ構造: ListNode(単方向連結リスト)
2 ステップバイステップ解説
3 Python実装(LeetCode形式)
ポイント: 番兵ノードで先頭処理を簡潔化、テールポインタでO(1)追加、非破壊的な実装
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
class ListNode:
val: int
next: Optional[ListNode]
def __init__(self, val: int = 0, next: Optional[ListNode] = None) -> None: ...
else:
try:
from leetcode_structures import ListNode # type: ignore
except (ImportError, ModuleNotFoundError):
class ListNode:
__slots__ = ("val", "next")
def __init__(self, val: int = 0, next: Optional[ListNode] = None) -> None:
self.val = val
self.next = next
class Solution:
"""
LeetCode 2: Add Two Numbers
2つの逆順連結リストで表された非負整数を加算し、
結果を逆順連結リストで返す。
時間計算量: O(n) - n = max(len(l1), len(l2))
空間計算量: O(1) - 出力ノードを除く補助空間
"""
def addTwoNumbers(
self,
l1: Optional[ListNode],
l2: Optional[ListNode]
) -> Optional[ListNode]:
"""
2つの逆順リストの和を逆順リストで返す。
Args:
l1: 第1の数の逆順連結リスト(最下位桁が先頭)
l2: 第2の数の逆順連結リスト(最下位桁が先頭)
Returns:
和を表す逆順連結リスト
"""
# 番兵ノード: 先頭ノードの特別処理を排除
dummy: ListNode = ListNode(0)
tail: ListNode = dummy
# 走査ポインタと繰り上がり
p: Optional[ListNode] = l1
q: Optional[ListNode] = l2
carry: int = 0
# いずれかのリストが残るか、繰り上がりが残る限り処理
while p is not None or q is not None or carry != 0:
# 現在の桁の値を取得(リストが終了していれば0)
x: int = p.val if p is not None else 0
y: int = q.val if q is not None else 0
# 桁の和 + 繰り上がりを計算
sum_: int = x + y + carry
carry = sum_ // 10 # 新しい繰り上がり
digit: int = sum_ % 10 # 現在の桁の値
# 結果ノードを生成して末尾に追加
tail.next = ListNode(digit)
tail = tail.next
# ポインタを前進(存在する場合のみ)
if p is not None:
p = p.next
if q is not None:
q = q.next
# 番兵の次が実際の結果の先頭
return dummy.next
4 アルゴリズムフローチャート
ループ条件でいずれかのポインタまたはcarryが存在する限り処理を継続。各反復で桁を計算し、新ノードを追加してポインタを前進。
5 計算量分析
時間計算量
O(n)
n = max(len(l1), len(l2))
各ノードを1回ずつ訪問し、定数時間の演算のみ実行。
空間計算量
O(1)
出力ノードを除く補助空間。番兵ノード、ポインタ変数のみ使用。入力リストは変更しない。
効率性のポイント
- ✓ 単一パス: 1回の走査で全処理完了
- ✓ 定数空間: 追加の配列やスタック不要
- ✓ 非破壊的: 入力リストを変更しないため安全
- ✓ スケーラブル: 桁数が増えても線形に対応