DSA Interview Prep · レッスン

マージ、分割、末尾からN番目を探す

2つのソート済み連結リストをO(n)でマージし、slow-fastポインターで中央からリストを分割し、末尾からn番目のノードを見つけます。

レッスン 4/413 ステップ

「マージ、分割、末尾からN番目を探す」はCoddyKit上の無料DSA Interview Prepレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDSA Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DSA Interview Prepコースには全4レッスンが含まれています。

連結リストに不可欠な3つのパターン

このレッスンでは、より難しい問題の基本部品として頻繁に登場する、連結リストの3つの基本操作を扱います。2つのソート済みリストのマージ(マージソートや k-way マージで使用)、リストを中央で分割すること(マージソートや回文の判定で使用)、そして末尾から n 番目のノードを見つけること(末尾から n 番目のノードの削除で使用)です。

3つとも、すでに学んだ技法であるダミーの先頭ノード、スロー・ファストポインタ、そして境界の慎重な追跡を利用します。

2つのソート済みリストのマージ

LeetCode 21 の「Merge Two Sorted Lists」では、2つのソート済み連結リストを受け取り、1つのソート済みリストにマージして返します。ダミーの先頭と、末尾を指す curr ポインタを使います。各ステップで2つのリストの先頭を比較し、小さい方のノードを curr に接続します。一方のリストを使い切ったら、もう一方の残りを接続します。時間計算量は O(n+m)、空間計算量は O(1) です(その場でポインタをつなぎ替えます)。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def mergeTwoLists(l1, l2):
    dummy = ListNode(0)
    curr  = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next
    curr.next = l1 or l2  # attach remaining nodes
    return dummy.next

def build(arr):
    d = ListNode(); c = d
    for v in arr:
        c.next = ListNode(v); c = c.next
    return d.next

def to_list(h):
    r=[]
    while h: r.append(h.val); h=h.next
    return r

print(to_list(mergeTwoLists(build([1,2,4]), build([1,3,4]))))

マージをステップごとにトレース

mergeTwoLists([1,2,4], [1,3,4]) をトレースします。1と1を比較し、l1(1)を選び、l1を2へ進めます。2と1を比較し、l2(1)を選び、l2を3へ進めます。2と3を比較し、l1(2)を選び、l1を4へ進めます。4と3を比較し、l2(3)を選び、l2を4へ進めます。4と4を比較し、l1(4)を選び、l1を None へ進めます。残りの l2(4) を接続します。結果は [1,1,2,3,4,4] です。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def mergeTwoLists(l1, l2):
    dummy = ListNode(0)
    curr  = dummy
    step  = 0
    while l1 and l2:
        step += 1
        if l1.val <= l2.val:
            print(f'Step {step}: pick l1({l1.val})')
            curr.next = l1; l1 = l1.next
        else:
            print(f'Step {step}: pick l2({l2.val})')
            curr.next = l2; l2 = l2.next
        curr = curr.next
    curr.next = l1 or l2
    return dummy.next

def build(arr):
    d=ListNode();c=d
    for v in arr: c.next=ListNode(v);c=c.next
    return d.next

mergeTwoLists(build([1,2,4]),build([1,3,4]))

スロー・ファストポインタで中央を見つける

リストを中央で分割するには、スロー・ファストポインタのパターンを使います。slow は1ステップ、fast は2ステップ進みます。fast が None(または最後のノード)に到達したとき、slow は中央に位置します。長さが偶数のリストでは、中央にある2つのノードのうち前のノードになります。これはマージソートでの分割における一般的な扱いです。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def split_at_mid(head):
    '''Returns (first_half_head, second_half_head).'''
    slow, fast = head, head
    while fast.next and fast.next.next:
        slow = slow.next
        fast = fast.next.next
    mid = slow.next   # second half starts here
    slow.next = None  # sever the list
    return head, mid

def build(arr):
    d=ListNode();c=d
    for v in arr: c.next=ListNode(v);c=c.next
    return d.next

def to_list(h):
    r=[]
    while h: r.append(h.val); h=h.next
    return r

head=build([1,2,3,4,5])
first, second = split_at_mid(head)
print(to_list(first), to_list(second))  # [1,2,3] [4,5]

連結リストのマージソート

LeetCode 148「リストのソート」:連結リストを O(n log n) 時間、O(log n) 空間でソートします。アプローチは、リストを中点で分割し、それぞれの半分を再帰的にソートしてからマージするというものです。連結リストでは中点での分割が O(n)(配列のように O(1) ではありません)ですが、全体の計算量は O(n log n) で、スタック領域も O(log n) しか必要としないため、連結リストにはマージソートが自然に適しています。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def sortList(head):
    if not head or not head.next:
        return head
    # Split
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    mid = slow.next
    slow.next = None
    # Recurse
    left  = sortList(head)
    right = sortList(mid)
    # Merge
    dummy = ListNode(0)
    curr  = dummy
    while left and right:
        if left.val <= right.val:
            curr.next = left;  left  = left.next
        else:
            curr.next = right; right = right.next
        curr = curr.next
    curr.next = left or right
    return dummy.next

def build(arr):
    d=ListNode();c=d
    for v in arr: c.next=ListNode(v);c=c.next
    return d.next
def to_list(h):
    r=[]
    while h: r.append(h.val);h=h.next
    return r

print(to_list(sortList(build([4,2,1,3]))))  # [1,2,3,4]

末尾から N 番目のノードを見つける

LeetCode 19「リストの末尾から N 番目のノードを削除」:1回の走査で末尾から n 番目のノードを見つけます。ちょうど n 個分離した2つのポインタを使います。fast を slow より n ステップ先に進めます。その後、fast が最後のノードに到達するまで、両方を同時に進めます。この時点で slow は末尾から (n+1) 番目のノード、つまり削除対象ノードの直前のノードにあります。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def removeNthFromEnd(head, n):
    dummy = ListNode(0, head)
    fast = dummy
    for _ in range(n + 1):  # advance fast n+1 steps
        fast = fast.next
    slow = dummy
    while fast:             # advance both until fast is None
        slow = slow.next
        fast = fast.next
    slow.next = slow.next.next  # remove nth node
    return dummy.next

def build(arr):
    d=ListNode();c=d
    for v in arr: c.next=ListNode(v);c=c.next
    return d.next
def to_list(h):
    r=[]
    while h: r.append(h.val);h=h.next
    return r

print(to_list(removeNthFromEnd(build([1,2,3,4,5]), 2)))  # [1,2,3,5]

Remove Nth における n+1 ステップの理由

重要なポイントは、ダミーヘッドから n ではなく n+1 ステップだけ fast を進めることです。n+1 ステップ後、どちらもダミーヘッドから開始しているため、fast は slow より n+1 個先にあります。fast が None に到達すると(末尾の1つ先)、slow は None の n+1 個前、つまりゼロ始まりで位置 (length - n - 1)、すなわち対象ノードの直前にあります。これにより、slow.next = slow.next.next で末尾から n 番目のノードを簡潔に削除できます。

# Visual: list = [1,2,3,4,5], n=2
# dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None
# After n+1=3 forward steps from dummy, fast=3
# dummy(slow)  1  2  3(fast)  4  5  None
# Advance both until fast=None:
# Step 1: slow=1, fast=4
# Step 2: slow=2, fast=5
# Step 3: slow=3, fast=None
# slow is at 3, slow.next=4 (the 2nd from end) -> delete
print('slow.next (to delete): 4')
print('Result: [1, 2, 3, 5]')

2つの連結リストの交点

LeetCode 160「2つの連結リストの交点」:2つのリストが初めて交わるノードを見つけます。O(1) 空間で実現する方法は、各リストに1つずつポインタを進めることです。ポインタが None に到達したら、もう一方のリストの先頭に移します。最大 len(A) + len(B) ステップ後には、両方のポインタが同じ総距離を進んでいるため、交点ノードに到達します。交点がない場合は、両方とも None に到達します。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def getIntersectionNode(headA, headB):
    a, b = headA, headB
    while a is not b:
        a = a.next if a else headB
        b = b.next if b else headA
    return a  # None if no intersection

# Build: A: 4->1->\  B: 5->6->1->\ both -> 8->4->5
shared = [ListNode(v) for v in [8, 4, 5]]
shared[0].next = shared[1]; shared[1].next = shared[2]
A = ListNode(4); A.next = ListNode(1); A.next.next = shared[0]
B = ListNode(5); B.next = ListNode(6); B.next.next = ListNode(1); B.next.next.next = shared[0]
print(getIntersectionNode(A, B).val)  # 8

K 個のソート済みリストをマージ(分割統治)

LeetCode 23「K 個のソート済みリストをマージ」:k 個のソート済みリストが与えられたとき、それらを1つにマージします。最適なアプローチは、分割統治を使ってリストのペアを繰り返しマージし、各ラウンドでリストの数を半分にすることです。平均長 n のリストが k 個ある場合、計算量は O(n k log k) で、順番にマージする場合の O(n k²) より効率的です。min-heap を使う方法も O(n k log k) です。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def mergeKLists(lists):
    def merge_two(l1, l2):
        dummy = ListNode(0); curr = dummy
        while l1 and l2:
            if l1.val <= l2.val:
                curr.next = l1; l1 = l1.next
            else:
                curr.next = l2; l2 = l2.next
            curr = curr.next
        curr.next = l1 or l2
        return dummy.next

    if not lists: return None
    while len(lists) > 1:
        merged = []
        for i in range(0, len(lists), 2):
            l1 = lists[i]
            l2 = lists[i+1] if i+1 < len(lists) else None
            merged.append(merge_two(l1, l2))
        lists = merged
    return lists[0]

def build(arr):
    d=ListNode();c=d
    for v in arr: c.next=ListNode(v);c=c.next
    return d.next
def to_list(h):
    r=[]
    while h: r.append(h.val);h=h.next
    return r

lists=[build([1,4,5]),build([1,3,4]),build([2,6])]
print(to_list(mergeKLists(lists)))  # [1,1,2,3,4,4,5,6]

奇数・偶数連結リスト

LeetCode 328「奇数・偶数連結リスト」:すべての奇数番目のノードを先に並べ、その後に偶数番目のノードを並べます(1始まり)。2本の独立したチェーン(奇数用と偶数用)を維持し、最後に接続します。リストを1回走査するだけでよいため、計算量は O(n)、空間計算量は O(1) です。これは、異なる進み幅で2つのポインタを同時に進める方法の、分かりやすい例です。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def oddEvenList(head):
    if not head:
        return head
    odd  = head
    even = head.next
    even_head = even
    while even and even.next:
        odd.next  = even.next
        odd       = odd.next
        even.next = odd.next
        even      = even.next
    odd.next = even_head
    return head

def build(arr):
    d=ListNode();c=d
    for v in arr: c.next=ListNode(v);c=c.next
    return d.next
def to_list(h):
    r=[]
    while h: r.append(h.val);h=h.next
    return r

print(to_list(oddEvenList(build([1,2,3,4,5]))))  # [1,3,5,2,4]

すべてを組み合わせる

このレッスンで扱った3つのパターン、つまりソート済みリストのマージ、中点での分割、末尾から n 番目の要素の検索には、追加のメモリを使わず、位置を追跡するために追加のポインタ変数を使うという共通点があります。ダミーヘッドはマージと削除を簡単にし、スロー・ファストポインタ間の間隔は特定の相対位置を固定します。また、一方のポインタを先に進めることで、必要な間隔を作ります。

面接では、コードを書く前に使用するパターンを説明してください。「1回の走査で末尾から n 番目を見つけるため、2ポインタの間隔テクニックを使います」と述べれば、構造的に考えていることを示せます。

理解度チェック

このレッスンで扱った Data Structures & Algorithms — Coding Interview Prep の概念について、理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、2つのソート済みリストのマージでは、ダミーヘッドと各ステップでの比較を使い、O(n+m) 時間、O(1) 空間で処理できること、中点での分割では、ファストポインタを最後の有効なペアで止めるスロー・ファストポインタを使うこと、そして末尾から n 番目の要素を見つけるには、ファストポインタを n+1 ステップ先に進めることで、スローポインタを直前のノードに置けることを学びました。次はスタックとキューを作り、典型的な面接問題に応用します。

無料で開始

AI チューターと学ぶ Python — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
30
レッスン
120

よくある質問

「マージ、分割、末尾からN番目を探す」レッスンは無料ですか?

はい。「マージ、分割、末尾からN番目を探す」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DSA Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DSA Interview Prepコースには全4レッスンが含まれています。

「マージ、分割、末尾からN番目を探す」で何を学びますか?

2つのソート済み連結リストをO(n)でマージし、slow-fastポインターで中央からリストを分割し、末尾からn番目のノードを見つけます。 ブラウザで直接実行するハンズオンコードでDSA Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

DSA Interview Prepを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのDSA Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「マージ、分割、末尾からN番目を探す」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このDSA Interview Prepレッスンでコードを書いて実行できますか?

はい。すべてのDSA Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Nodeクラスとリストの構築
  2. 連結リストを反転する
  3. Floydのアルゴリズムによる循環検出
  4. マージ、分割、末尾からN番目を探す
← DSA Interview Prepに戻る