Add Two Numbers

LeetCode 2 - 逆順連結リスト加算(同時走査 + 繰り上がり伝搬)

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 アルゴリズムフローチャート

開始 初期化 dummy, tail, p=l1, q=l2 carry=0 p or q or carry? いいえ はい 値を取得 x = p.val または 0, y = q.val または 0 計算 sum = x + y + carry 分割 carry = sum // 10, digit = sum % 10 作成と追加 tail.next = ListNode(digit) 返却 dummy.next

ループ条件でいずれかのポインタまたはcarryが存在する限り処理を継続。各反復で桁を計算し、新ノードを追加してポインタを前進。

5 計算量分析

時間計算量

O(n)

n = max(len(l1), len(l2))
各ノードを1回ずつ訪問し、定数時間の演算のみ実行。

空間計算量

O(1)

出力ノードを除く補助空間。番兵ノード、ポインタ変数のみ使用。入力リストは変更しない。

効率性のポイント

  • 単一パス: 1回の走査で全処理完了
  • 定数空間: 追加の配列やスタック不要
  • 非破壊的: 入力リストを変更しないため安全
  • スケーラブル: 桁数が増えても線形に対応