第6章:データ構造の理論と連結リストの実装

6.1 データ構造の歴史と理論的基盤

6.1.1 データ構造の誕生 - 計算機科学の夜明け

コンピュータプログラミングの初期、データは単純な配列や変数として扱われていた。しかし、より複雑な問題を解決するために、データの組織化方法を体系的に研究する必要が生じた。

LISP と動的データ構造の革命(1958年)

John McCarthyが開発したLISPは、データ構造の歴史において革命的だった。LISPの名前自体が「List Processing」の略であり、連結リストはその核心にあった。

; LISPにおけるリスト表現
(1 2 3 4 5)

; 内部的にはコンスセルの連鎖
; (1 . (2 . (3 . (4 . (5 . NIL)))))

; car(最初の要素)とcdr(残り)
(car '(1 2 3))  ; => 1
(cdr '(1 2 3))  ; => (2 3)

LISPのコンスセル(cons cell)は、現代の連結リストノードの直接の祖先:

mermaid graph LR subgraph Cons Cell car[car: Value] cdr[cdr: Next] end car --> Data cdr --> Next[Next Cell / NIL]

リスト (1 2 3) の内部表現:
mermaid graph LR C1[1] --> C2[2] --> C3[3] --> NIL

なぜ連結リストが必要だったのか

  • 動的サイズ: 実行時にサイズが変化するデータを扱う
  • 効率的な挿入・削除: 要素の移動なしに操作可能
  • メモリの効率的使用: 必要な分だけ確保
  • 再帰的処理との親和性: 関数型言語の本質

6.1.2 抽象データ型(ADT)の理論

Barbara Liskovが1970年代に形式化した抽象データ型(Abstract Data Type)の概念は、データ構造を理解する上で根本的に重要。

ADTの定義

抽象データ型は以下で構成される:

  • 値の集合(ドメイン)
  • 操作の集合
  • 公理(操作の振る舞いを定義する規則)

リストADTの形式的定義:

Domain: List<T> = Empty | Cons(T, List<T>)

Operations:
  - new() → List<T>
  - add_front(List<T>, T) → List<T>
  - head(List<T>) → T
  - tail(List<T>) → List<T>
  - is_empty(List<T>) → Boolean
  - size(List<T>) → Integer

Axioms:
  - head(Cons(x, xs)) = x
  - tail(Cons(x, xs)) = xs
  - is_empty(Empty) = true
  - is_empty(Cons(x, xs)) = false
  - size(Empty) = 0
  - size(Cons(x, xs)) = 1 + size(xs)

ADTと実装の分離

抽象データ型の美点は、インターフェースと実装を分離すること:

/* 抽象インターフェース(ユーザーが見る) */
typedef struct s_list t_list;

t_list  *ft_lstnew(void *content);
void    ft_lstadd_front(t_list **lst, t_list *new);
int     ft_lstsize(t_list *lst);
/* ... */

/* 実装詳細(内部に隠蔽可能) */
struct s_list {
    void            *content;
    struct s_list   *next;
};

/*
 * ユーザーは内部構造を知らなくても使用可能
 * 実装を変更しても、インターフェースが同じなら影響なし
 */

情報隠蔽の原則

David Parnasが1972年に提唱した情報隠蔽(Information Hiding)は、ADTの実装に不可欠:

/* 不透明なポインタを使った情報隠蔽 */

/* list.h - 公開ヘッダ */
typedef struct s_list_impl *t_list_handle;  /* 不透明型 */

t_list_handle   list_create(void);
void            list_destroy(t_list_handle list);
int             list_add(t_list_handle list, void *item);
void            *list_get(t_list_handle list, int index);

/* list.c - 実装ファイル */
struct s_list_impl {
    void    **items;
    size_t  size;
    size_t  capacity;
    /* 実装詳細は外部から見えない */
};

6.1.3 計算量理論の基礎

アルゴリズムとデータ構造を評価するには、計算量理論の理解が不可欠。

Big-O記法の起源

Big-O記法は、1894年にドイツの数学者Paul Bachmannが導入し、Edmund Landauが普及させた。本来は数論の記法だったが、Donald Knuthがコンピュータ科学に適用した。

Big-O の形式的定義:

f(n) = O(g(n)) ⟺ ∃c > 0, n₀ > 0 such that
                   ∀n ≥ n₀: f(n) ≤ c · g(n)

つまり、十分大きなnに対して、f(n)はg(n)の定数倍以下で抑えられる

計算量のクラス

一般的な計算量クラス(増加順):

O(1)       - 定数時間(配列のインデックスアクセス)
O(log n)   - 対数時間(二分探索)
O(n)       - 線形時間(配列の走査)
O(n log n) - 準線形時間(効率的なソート)
O(n²)      - 二乗時間(単純なソート)
O(2ⁿ)      - 指数時間(総当たり)
O(n!)      - 階乗時間(順列の列挙)

n = 1,000,000 での概算:

| 計算量 | 操作数(概算) | |:-------|:---------------| | O(1) | 1 | | O(log n) | 20 | | O(n) | 1,000,000 | | O(n log n) | 20,000,000 | | O(n²) | 1,000,000,000,000 | | O(2ⁿ) | 宇宙の寿命より長い |

漸近解析の実践

/*
 * 計算量解析の例
 */

/* O(1) - 定数時間 */
void *get_first(t_list *lst)
{
    if (!lst)           /* 1 操作 */
        return NULL;    /* 1 操作 */
    return lst->content; /* 1 操作 */
    /* 合計: 3操作 → O(1) */
}

/* O(n) - 線形時間 */
int ft_lstsize(t_list *lst)
{
    int count = 0;      /* 1 操作 */

    while (lst)         /* n 回のループ */
    {
        count++;        /* n 操作 */
        lst = lst->next; /* n 操作 */
    }
    return count;       /* 1 操作 */
    /* 合計: 2n + 2 操作 → O(n) */
}

/* O(n²) - 二乗時間(悪い例) */
void bad_add_all_to_back(t_list **lst, t_list *items)
{
    while (items)
    {
        /* ft_lstadd_back は O(n)(毎回リスト末尾を探索) */
        ft_lstadd_back(lst, ft_lstnew(items->content));
        items = items->next;
    }
    /* n 回 × O(n) = O(n²) */
}

/* O(n) - 線形時間(良い例) */
void good_add_all_to_back(t_list **lst, t_list *items)
{
    t_list *tail = ft_lstlast(*lst);  /* 1回だけ O(n) */

    while (items)  /* n 回のループ */
    {
        t_list *new = ft_lstnew(items->content);
        if (!*lst)
        {
            *lst = new;
            tail = new;
        }
        else
        {
            tail->next = new;  /* O(1) */
            tail = new;
        }
        items = items->next;
    }
    /* O(n) + n × O(1) = O(n) */
}

償却計算量(Amortized Analysis)

単一操作のコストだけでなく、操作のシーケンス全体のコストを考える:

/*
 * 動的配列の償却解析
 *
 * 挿入時に容量が足りなければ2倍に拡張
 */

typedef struct s_dynamic_array {
    void    **items;
    size_t  size;
    size_t  capacity;
} t_dynamic_array;

void array_push(t_dynamic_array *arr, void *item)
{
    /* 容量が足りない場合、拡張(O(n)) */
    if (arr->size >= arr->capacity) {
        arr->capacity *= 2;
        arr->items = realloc(arr->items,
                             arr->capacity * sizeof(void *));
        /* n 要素をコピー */
    }

    arr->items[arr->size++] = item;  /* O(1) */
}

/*
 * 最悪ケース: O(n) (拡張時)
 * 平均ケース: O(1) (拡張なし)
 *
 * 償却解析:
 * n 回の挿入で、拡張は log₂(n) 回発生
 * 各拡張でコピーされる合計: 1 + 2 + 4 + ... + n ≈ 2n
 * n 回の挿入 + 2n 回のコピー = 3n 操作
 *
 * 1回あたりの償却コスト: 3n / n = O(1)
 */

6.1.4 配列と連結リストの比較

メモリレイアウトの違い

配列のメモリレイアウト:
mermaid block-beta columns 8 A["A"] B["B"] C["C"] D["D"] E["E"] S1[" "] S2[" "] S3[" "] space:8 style A fill:#f9f,stroke:#333 style B fill:#f9f,stroke:#333 style C fill:#f9f,stroke:#333 style D fill:#f9f,stroke:#333 style E fill:#f9f,stroke:#333
*連続したメモリ領域 (ランダムアクセス O(1))*

連結リストのメモリレイアウト:
mermaid graph LR A --> B --> C --> NULL style A fill:#bbf,stroke:#333 style B fill:#bbf,stroke:#333 style C fill:#bbf,stroke:#333
*任意のアドレスに散在 (シーケンシャルアクセス O(n))*

操作別の計算量比較

┌────────────────────┬───────────┬──────────────┐
│ 操作               │ 配列      │ 連結リスト    │
├────────────────────┼───────────┼──────────────┤
│ インデックスアクセス│ O(1)      │ O(n)         │
│ 先頭への挿入       │ O(n)      │ O(1)         │
│ 末尾への挿入       │ O(1)*     │ O(n)†        │
│ 中間への挿入       │ O(n)      │ O(1)‡        │
│ 先頭の削除         │ O(n)      │ O(1)         │
│ 末尾の削除         │ O(1)      │ O(n)         │
│ 中間の削除         │ O(n)      │ O(1)‡        │
│ 検索               │ O(n)      │ O(n)         │
│ メモリ使用量       │ n×size    │ n×(size+ptr) │
└────────────────────┴───────────┴──────────────┘

* 動的配列で償却O(1)
† 末尾ポインタを保持すればO(1)
‡ 挿入・削除位置が既知の場合

キャッシュ効率の考慮

/*
 * 現代のCPUでは、メモリアクセスパターンが重要
 *
 * 配列:優れたキャッシュ局所性
 * - 連続アクセスでプリフェッチが効く
 * - キャッシュラインを効率的に使用
 *
 * 連結リスト:キャッシュ効率が低い
 * - ノードがメモリ上に散在
 * - ポインタ追跡でキャッシュミスが発生
 */

/* ベンチマーク例 */
void benchmark_sequential_access(void)
{
    int array[1000000];
    t_list *list = create_list_with_n_elements(1000000);

    /* 配列の走査:非常に高速(キャッシュフレンドリー) */
    clock_t start = clock();
    long sum = 0;
    for (int i = 0; i < 1000000; i++)
        sum += array[i];
    clock_t array_time = clock() - start;

    /* リストの走査:配列より遅い(ポインタ追跡) */
    start = clock();
    sum = 0;
    t_list *curr = list;
    while (curr) {
        sum += *(int *)curr->content;
        curr = curr->next;
    }
    clock_t list_time = clock() - start;

    /* 典型的な結果:リストは配列の5-10倍遅い */
}

6.2 ポインタと参照の理論

6.2.1 間接参照の数学的モデル

ポインタは、メモリアドレスという「名前」から「値」への関数として理解できる。

ポインタの理論的モデル:

Memory: Address → Value
Pointer: Variable → Address
Dereference: *p = Memory(Pointer(p))

mermaid graph LR p[Variable p] -->|0x1000| addr[Address 0x1000] addr -->|Value| val[42]

間接参照のレベル

/*
 * ポインタのポインタ(ダブルポインタ)
 *
 * ft_lstadd_front が **lst を取る理由の理解
 */

void explain_double_pointer(void)
{
    int x = 42;
    int *p = &x;      /* p は x のアドレスを保持 */
    int **pp = &p;    /* pp は p のアドレスを保持 */

    printf("x = %d\n", x);      /* 42 */
    printf("*p = %d\n", *p);    /* 42 */
    printf("**pp = %d\n", **pp); /* 42 */
}

/*
 * メモリレイアウト:
 *
 * 
mermaid graph LR x[x: 42] p[p: &x] --> x pp[pp: &p] --> p
 */

6.2.2 なぜダブルポインタが必要か

/*
 * シングルポインタでの問題点
 */

/* 動作しない実装 */
void broken_add_front(t_list *lst, t_list *new)
{
    new->next = lst;
    lst = new;  /* ローカル変数を変更するだけ! */
    /* 呼び出し元の変数は変更されない */
}

void demonstrate_broken(void)
{
    t_list *head = NULL;
    t_list *node = ft_lstnew("Hello");

    broken_add_front(head, node);

    printf("head = %p\n", head);  /* まだ NULL */
}

/*
 * 関数呼び出しのメカニズム:
 *
 * 呼び出し前:
 *   head: NULL (アドレス 0x1000)
 *
 * broken_add_front 内:
 *   lst: NULL (アドレス 0x2000) ← headのコピー
 *
 * lst = new で変更されるのは 0x2000 番地
 * 0x1000 番地(元のhead)は変更されない
 */

/* 正しい実装 */
void ft_lstadd_front(t_list **lst, t_list *new)
{
    if (!lst || !new)
        return;

    new->next = *lst;  /* 現在の先頭を保存 */
    *lst = new;        /* 先頭を更新(呼び出し元のheadを変更) */
}

void demonstrate_correct(void)
{
    t_list *head = NULL;
    t_list *node = ft_lstnew("Hello");

    ft_lstadd_front(&head, node);

    printf("head = %p\n", head);  /* nodeのアドレス */
}

/*
 * 呼び出し前:
 *   head: NULL (アドレス 0x1000)
 *
 * ft_lstadd_front 内:
 *   lst: 0x1000 (アドレス 0x2000) ← headのアドレス
 *
 * *lst = new で変更されるのは 0x1000 番地
 * 呼び出し元の head が直接変更される
 */

ダブルポインタが必要な操作

/*
 * ポインタ自体を変更する必要がある操作:
 * - 先頭への追加(headポインタの変更)
 * - 先頭の削除(headポインタの変更)
 * - リスト全体の削除(headをNULLに)
 */

/* 先頭の削除 */
void *lst_pop_front(t_list **lst)
{
    if (!lst || !*lst)
        return NULL;

    t_list *first = *lst;
    void *content = first->content;

    *lst = first->next;  /* headを更新 */
    free(first);

    return content;
}

/* リストの反転 */
void lst_reverse(t_list **lst)
{
    t_list *prev = NULL;
    t_list *current = *lst;
    t_list *next;

    while (current)
    {
        next = current->next;
        current->next = prev;
        prev = current;
        current = next;
    }

    *lst = prev;  /* headを新しい先頭に更新 */
}

6.2.3 自己参照構造体の理論

構造体が自身へのポインタをメンバとして持つことで、再帰的なデータ構造が可能になる。

/*
 * 自己参照構造体の宣言
 *
 * 重要:型名は完全な定義の前に使用できない
 * しかし、ポインタは不完全な型でも宣言可能
 */

/* 正しい宣言 */
typedef struct s_list
{
    void            *content;
    struct s_list   *next;  /* struct s_list は既知 */
}   t_list;

/* コンパイルエラーになる例 */
/*
typedef struct s_list
{
    void    *content;
    t_list  *next;  // エラー! t_list はまだ未定義
}   t_list;
*/

/* 前方宣言を使う方法 */
typedef struct s_list t_list;

struct s_list
{
    void    *content;
    t_list  *next;  /* OK:t_list は前方宣言済み */
};

相互参照構造体

/*
 * 相互に参照し合う構造体
 */

/* 前方宣言 */
typedef struct s_parent t_parent;
typedef struct s_child t_child;

struct s_parent
{
    char        *name;
    t_child     *first_child;
};

struct s_child
{
    char        *name;
    t_parent    *parent;
    t_child     *sibling;
};

/*
 * 木構造やグラフなど、複雑なデータ構造に必要
 */

6.3 再帰的データ構造と再帰的処理

6.3.1 再帰の数学的基礎

再帰は数学的帰納法と密接に関連している。

帰納的定義(Inductive Definition)

自然数の帰納的定義:
  Base case:     0 は自然数
  Inductive case: n が自然数なら、succ(n) も自然数

リストの帰納的定義:
  Base case:     Empty は List
  Inductive case: x が要素、xs が List なら、Cons(x, xs) も List

これらの定義は、再帰的操作の構造を自然に導く

再帰的処理の構造

/*
 * 再帰的定義に対応した再帰的関数
 */

/* リストの長さ(再帰版) */
int list_length(t_list *lst)
{
    /* Base case: 空リストの長さは0 */
    if (!lst)
        return 0;

    /* Inductive case: 先頭 + 残りの長さ */
    return 1 + list_length(lst->next);
}

/*
 * 証明:この関数が正しいことの帰納法による証明
 *
 * Base case: lst == NULL のとき、0を返す ✓
 *
 * Inductive step:
 *   仮定:list_length(lst->next) が残りの長さを正しく返す
 *   ならば:1 + list_length(lst->next) は全体の長さを正しく返す ✓
 */

6.3.2 末尾再帰最適化

/*
 * 通常の再帰:スタックを消費
 */
int list_sum(t_list *lst)
{
    if (!lst)
        return 0;

    return *(int *)lst->content + list_sum(lst->next);
    /* ↑ この位置で加算が必要なため、フレームを保持 */
}

/*
 * 呼び出しスタック(リスト [1, 2, 3] の場合):
 *
 * list_sum([1,2,3])
 *   → 1 + list_sum([2,3])
 *        → 2 + list_sum([3])
 *             → 3 + list_sum([])
 *                  → 0
 *             ← 3 + 0 = 3
 *        ← 2 + 3 = 5
 *   ← 1 + 5 = 6
 *
 * 3つのスタックフレームが同時に存在
 */

/*
 * 末尾再帰:スタック消費なし(最適化可能)
 */
static int list_sum_helper(t_list *lst, int accumulator)
{
    if (!lst)
        return accumulator;

    /* 末尾位置での再帰呼び出し */
    return list_sum_helper(lst->next,
                           accumulator + *(int *)lst->content);
}

int list_sum_tail(t_list *lst)
{
    return list_sum_helper(lst, 0);
}

/*
 * 末尾呼び出し最適化(TCO):
 *
 * コンパイラは末尾再帰をループに変換可能
 *
 * list_sum_helper([1,2,3], 0)
 *   → list_sum_helper([2,3], 1)
 *     → list_sum_helper([3], 3)
 *       → list_sum_helper([], 6)
 *         → 6
 *
 * 各呼び出しで前のフレームは不要なため、
 * スタックフレームを再利用できる
 */

/*
 * 注意:C言語ではTCO が保証されない
 * -O2 以上の最適化で有効になる場合が多い
 * 確実を期すなら明示的にループを使用
 */

6.3.3 再帰から反復への変換

/*
 * 再帰と反復は等価(チューリング完全)
 * 任意の再帰は反復に変換可能
 */

/* 再帰版:リストの探索 */
t_list *list_find_recursive(t_list *lst, void *target,
                            int (*cmp)(void *, void *))
{
    if (!lst)
        return NULL;

    if (cmp(lst->content, target) == 0)
        return lst;

    return list_find_recursive(lst->next, target, cmp);
}

/* 反復版:同じ処理 */
t_list *list_find_iterative(t_list *lst, void *target,
                            int (*cmp)(void *, void *))
{
    while (lst)
    {
        if (cmp(lst->content, target) == 0)
            return lst;
        lst = lst->next;
    }
    return NULL;
}

/*
 * より複雑な再帰の変換:
 * 明示的なスタックを使用
 */

/* 再帰版:木の走査 */
typedef struct s_tree {
    void            *value;
    struct s_tree   *left;
    struct s_tree   *right;
} t_tree;

void tree_preorder_recursive(t_tree *node, void (*f)(void *))
{
    if (!node)
        return;

    f(node->value);
    tree_preorder_recursive(node->left, f);
    tree_preorder_recursive(node->right, f);
}

/* 反復版:明示的スタック */
void tree_preorder_iterative(t_tree *root, void (*f)(void *))
{
    if (!root)
        return;

    /* スタックとして連結リストを使用 */
    t_list *stack = NULL;
    ft_lstadd_front(&stack, ft_lstnew(root));

    while (stack)
    {
        /* pop */
        t_tree *node = (t_tree *)stack->content;
        t_list *old = stack;
        stack = stack->next;
        free(old);

        f(node->value);

        /* 右を先にpush(左を先に処理するため) */
        if (node->right)
            ft_lstadd_front(&stack, ft_lstnew(node->right));
        if (node->left)
            ft_lstadd_front(&stack, ft_lstnew(node->left));
    }
}

6.4 連結リストの変種

6.4.1 双方向連結リスト

/*
 * 双方向連結リスト:前後両方向に移動可能
 */

typedef struct s_dlist {
    void            *content;
    struct s_dlist  *next;
    struct s_dlist  *prev;
} t_dlist;

/*
 * 構造:
 *
 * NULL ← [A] ⟷ [B] ⟷ [C] ⟷ [D] → NULL
 *
 * 利点:
 * - 逆方向の走査が可能
 * - 任意のノードから両隣にアクセス可能
 * - 末尾からの削除が O(1)
 *
 * 欠点:
 * - 各ノードで追加のポインタ(メモリ消費)
 * - 挿入・削除でより多くのポインタ操作
 */

/* 双方向リストへの挿入 */
void dlist_insert_after(t_dlist *node, t_dlist *new)
{
    if (!node || !new)
        return;

    new->prev = node;
    new->next = node->next;

    if (node->next)
        node->next->prev = new;

    node->next = new;
}

/* 双方向リストからの削除 */
void dlist_remove(t_dlist *node)
{
    if (!node)
        return;

    if (node->prev)
        node->prev->next = node->next;

    if (node->next)
        node->next->prev = node->prev;

    /* node 自体は解放しない(呼び出し側の責任) */
}

6.4.2 循環連結リスト

/*
 * 循環連結リスト:最後のノードが最初のノードを指す
 */

/*
 * 構造:
 *
 * 
mermaid graph LR A --> B --> C --> D --> A
 *
 * 用途:
 * - ラウンドロビンスケジューリング
 * - 循環バッファ
 * - 無限ループ処理
 */

/* 循環リストの作成 */
void make_circular(t_list *lst)
{
    if (!lst)
        return;

    t_list *last = ft_lstlast(lst);
    last->next = lst;
}

/* 循環リストの走査 */
void circular_list_iter(t_list *start, void (*f)(void *), int max_iter)
{
    t_list *current = start;
    int count = 0;

    if (!start)
        return;

    do {
        f(current->content);
        current = current->next;
        count++;
    } while (current != start && count < max_iter);
}

/* 循環リストのサイズ(循環を考慮) */
int circular_list_size(t_list *start)
{
    t_list *current;
    int count;

    if (!start)
        return 0;

    current = start;
    count = 0;

    do {
        count++;
        current = current->next;
    } while (current != start);

    return count;
}

6.4.3 センチネルノード

/*
 * センチネル(番兵)ノード:境界条件を単純化
 */

typedef struct s_sentinel_list {
    t_list sentinel;  /* ダミーノード(実データなし) */
} t_sentinel_list;

/*
 * 構造:
 *
 * 
mermaid graph LR S[Sentinel] --> A --> B --> C --> NULL
*Sentinelは常に存在*
 *
 * 利点:
 * - head == NULL のチェックが不要
 * - 先頭への挿入・削除が特殊ケースにならない
 */

t_sentinel_list *sentinel_list_new(void)
{
    t_sentinel_list *list = malloc(sizeof(t_sentinel_list));
    if (!list)
        return NULL;

    list->sentinel.content = NULL;
    list->sentinel.next = NULL;
    return list;
}

/* センチネルを使った挿入(先頭/中間が同じコード) */
void sentinel_insert_after(t_list *node, void *content)
{
    t_list *new = ft_lstnew(content);
    if (!new)
        return;

    new->next = node->next;
    node->next = new;
}

void sentinel_add_front(t_sentinel_list *list, void *content)
{
    /* センチネルの直後に挿入 */
    sentinel_insert_after(&list->sentinel, content);
}

/* 最初の実データノードを取得 */
t_list *sentinel_get_first(t_sentinel_list *list)
{
    return list->sentinel.next;
}

6.5 libft連結リスト関数の実装

6.5.1 ft_lstnew - 新しいノードの作成

/*
 * ft_lstnew - 新しいリストノードを作成
 *
 * @content: ノードに格納するデータ
 * @return: 新しいノード、失敗時NULL
 *
 * 設計上の決定:
 * - contentのコピーではなく、ポインタを保存
 * - 所有権は呼び出し元が管理
 */

t_list *ft_lstnew(void *content)
{
    t_list *new_node;

    new_node = (t_list *)malloc(sizeof(t_list));
    if (!new_node)
        return (NULL);

    new_node->content = content;
    new_node->next = NULL;

    return (new_node);
}

/*
 * メモリレイアウト:
 *
 * malloc で確保された領域:
 * ┌─────────────────────────────┐
 * │ content: (contentのアドレス)│
 * │ next:    NULL               │
 * └─────────────────────────────┘
 *
 * content が指すデータは別のメモリ領域
 */

6.5.2 ft_lstadd_front - リストの先頭に追加

/*
 * ft_lstadd_front - リストの先頭に新しいノードを追加
 *
 * @lst: リストのヘッドポインタへのポインタ
 * @new: 追加するノード
 *
 * ダブルポインタの理由:headポインタ自体を変更するため
 */

void ft_lstadd_front(t_list **lst, t_list *new)
{
    if (!lst || !new)
        return;

    new->next = *lst;
    *lst = new;
}

/*
 * 操作の可視化:
 *
 * 呼び出し前:
 * 
mermaid graph LR head --> A --> B --> NULL new --> N

実行後:
mermaid graph LR head --> N --> A --> B --> NULL
 *
 * 計算量: O(1)
 */

6.5.3 ft_lstsize - リストのサイズを取得

/*
 * ft_lstsize - リスト内のノード数を返す
 *
 * @lst: リストの先頭ノード
 * @return: ノード数
 */

int ft_lstsize(t_list *lst)
{
    int count;

    count = 0;
    while (lst)
    {
        count++;
        lst = lst->next;
    }
    return (count);
}

/*
 * 計算量: O(n) - 全ノードを走査
 *
 * 注意:頻繁にサイズが必要な場合、
 * ラッパー構造体でサイズをキャッシュすることを検討
 */

6.5.4 ft_lstlast - 最後のノードを取得

/*
 * ft_lstlast - リストの最後のノードを返す
 *
 * @lst: リストの先頭ノード
 * @return: 最後のノード、空リストならNULL
 */

t_list *ft_lstlast(t_list *lst)
{
    if (!lst)
        return (NULL);

    while (lst->next)
        lst = lst->next;

    return (lst);
}

/*
 * 計算量: O(n) - 最後まで走査
 *
 * 末尾への追加を頻繁に行う場合、
 * 末尾ポインタを保持する構造を検討
 */

6.5.5 ft_lstadd_back - リストの末尾に追加

/*
 * ft_lstadd_back - リストの末尾に新しいノードを追加
 *
 * @lst: リストのヘッドポインタへのポインタ
 * @new: 追加するノード
 */

void ft_lstadd_back(t_list **lst, t_list *new)
{
    t_list *last;

    if (!lst || !new)
        return;

    if (!*lst)
    {
        *lst = new;
        return;
    }

    last = ft_lstlast(*lst);
    last->next = new;
}

/*
 * 計算量: O(n) - ft_lstlastの呼び出し
 *
 * 操作の可視化:

空リストの場合:
mermaid graph LR head --> N --> NULL

既存リストの場合:
mermaid graph LR head --> A --> B --> N --> NULL
 */

6.5.6 ft_lstdelone - 単一ノードの削除

/*
 * ft_lstdelone - 単一ノードを削除
 *
 * @lst: 削除するノード
 * @del: contentを解放する関数
 *
 * 注意:nextは解放しない(呼び出し側がリンクを管理)
 */

void ft_lstdelone(t_list *lst, void (*del)(void *))
{
    if (!lst)
        return;

    if (del && lst->content)
        del(lst->content);

    free(lst);
}

/*
 * 典型的な削除関数:
 */

/* 単純なfree */
void del_free(void *content)
{
    free(content);
}

/* 何もしない(静的データや外部管理データ用) */
void del_nothing(void *content)
{
    (void)content;
}

/* 構造体の深い解放 */
typedef struct s_user {
    char *name;
    char *email;
} t_user;

void del_user(void *content)
{
    t_user *user = (t_user *)content;
    if (user)
    {
        free(user->name);
        free(user->email);
        free(user);
    }
}

6.5.7 ft_lstclear - リスト全体の削除

/*
 * ft_lstclear - リスト全体を削除
 *
 * @lst: リストのヘッドポインタへのポインタ
 * @del: contentを解放する関数
 *
 * すべてのノードを削除し、*lstをNULLに設定
 */

void ft_lstclear(t_list **lst, void (*del)(void *))
{
    t_list *current;
    t_list *next;

    if (!lst || !*lst)
        return;

    current = *lst;
    while (current)
    {
        next = current->next;
        ft_lstdelone(current, del);
        current = next;
    }

    *lst = NULL;
}

/*
 * 重要なパターン:
 *
 * current = *lst;
 * while (current)
 * {
 *     next = current->next;  // 次を保存してから
 *     // currentを解放      // 解放
 *     current = next;        // 次に移動
 * }
 *
 * nextを先に保存しないと、解放後に
 * current->next にアクセスできない
 */

6.5.8 ft_lstiter - リストの反復処理

/*
 * ft_lstiter - リストの各要素に関数を適用
 *
 * @lst: リストの先頭
 * @f: 各contentに適用する関数
 *
 * リスト構造は変更しない
 */

void ft_lstiter(t_list *lst, void (*f)(void *))
{
    if (!f)
        return;

    while (lst)
    {
        f(lst->content);
        lst = lst->next;
    }
}

/*
 * 使用例:
 */

void print_string(void *content)
{
    printf("%s\n", (char *)content);
}

void print_int(void *content)
{
    printf("%d\n", *(int *)content);
}

void double_int(void *content)
{
    int *num = (int *)content;
    *num *= 2;  /* contentを変更 */
}

void example_lstiter(void)
{
    t_list *list = NULL;
    int nums[] = {1, 2, 3, 4, 5};

    /* リストを作成 */
    for (int i = 0; i < 5; i++)
        ft_lstadd_back(&list, ft_lstnew(&nums[i]));

    printf("Before:\n");
    ft_lstiter(list, print_int);  /* 1 2 3 4 5 */

    ft_lstiter(list, double_int);  /* 各要素を2倍 */

    printf("After:\n");
    ft_lstiter(list, print_int);  /* 2 4 6 8 10 */

    ft_lstclear(&list, NULL);
}

6.5.9 ft_lstmap - リストの変換

/*
 * ft_lstmap - 各要素に関数を適用した新しいリストを作成
 *
 * @lst: 元のリスト
 * @f: 各contentに適用する変換関数
 * @del: エラー時にcontentを解放する関数
 * @return: 新しいリスト、エラー時NULL
 *
 * 関数型プログラミングの「map」操作に相当
 */

t_list *ft_lstmap(t_list *lst, void *(*f)(void *), void (*del)(void *))
{
    t_list  *new_list;
    t_list  *new_node;
    void    *new_content;

    if (!f || !del)
        return (NULL);

    new_list = NULL;
    while (lst)
    {
        /* 変換関数を適用 */
        new_content = f(lst->content);
        if (!new_content)
        {
            ft_lstclear(&new_list, del);
            return (NULL);
        }

        /* 新しいノードを作成 */
        new_node = ft_lstnew(new_content);
        if (!new_node)
        {
            del(new_content);
            ft_lstclear(&new_list, del);
            return (NULL);
        }

        ft_lstadd_back(&new_list, new_node);
        lst = lst->next;
    }

    return (new_list);
}

/*
 * 使用例:
 */

void *string_to_upper(void *content)
{
    char *str = (char *)content;
    char *upper = ft_strdup(str);

    if (!upper)
        return NULL;

    for (int i = 0; upper[i]; i++)
        upper[i] = ft_toupper(upper[i]);

    return upper;
}

void example_lstmap(void)
{
    t_list *original = NULL;
    t_list *uppercase;

    /* 元のリスト作成 */
    ft_lstadd_back(&original, ft_lstnew(ft_strdup("hello")));
    ft_lstadd_back(&original, ft_lstnew(ft_strdup("world")));

    /* 大文字に変換した新リスト作成 */
    uppercase = ft_lstmap(original, string_to_upper, free);

    /* 結果を出力 */
    ft_lstiter(uppercase, print_string);  /* HELLO WORLD */

    /* クリーンアップ */
    ft_lstclear(&original, free);
    ft_lstclear(&uppercase, free);
}

6.6 高度な連結リスト操作

6.6.1 リストの反転

/*
 * リストの反転(インプレース)
 *
 * 計算量: O(n) 時間, O(1) 空間
 */

t_list *ft_lstrev(t_list *lst)
{
    t_list *prev;
    t_list *current;
    t_list *next;

    prev = NULL;
    current = lst;

    while (current)
    {
        next = current->next;   /* 次を保存 */
        current->next = prev;   /* 向きを反転 */
        prev = current;         /* prevを進める */
        current = next;         /* currentを進める */
    }

    return (prev);
}

/*
 * 操作の可視化:
 *
 * 初期状態:
 * prev=NULL  current=[1]→[2]→[3]→NULL
 *
 * ステップ1:
 * NULL←[1]  current=[2]→[3]→NULL
 *      ↑prev
 *
 * ステップ2:
 * NULL←[1]←[2]  current=[3]→NULL
 *           ↑prev
 *
 * ステップ3:
 * NULL←[1]←[2]←[3]  current=NULL
 *                ↑prev
 *
 * 結果:prev が新しい先頭
 */

6.6.2 マージソート

/*
 * 連結リストのマージソート
 *
 * 計算量: O(n log n) 時間, O(log n) 空間(再帰スタック)
 */

/* リストを2つに分割 */
static t_list *split_list(t_list *head)
{
    t_list *slow;
    t_list *fast;
    t_list *mid;

    slow = head;
    fast = head->next;

    /* fast が末尾に到達したとき、slowは中間 */
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }

    mid = slow->next;
    slow->next = NULL;  /* 前半を切断 */

    return (mid);
}

/* 2つのソート済みリストをマージ */
static t_list *merge_sorted(t_list *a, t_list *b,
                            int (*cmp)(void *, void *))
{
    t_list dummy;
    t_list *tail;

    dummy.next = NULL;
    tail = &dummy;

    while (a && b)
    {
        if (cmp(a->content, b->content) <= 0)
        {
            tail->next = a;
            a = a->next;
        }
        else
        {
            tail->next = b;
            b = b->next;
        }
        tail = tail->next;
    }

    /* 残りを連結 */
    tail->next = a ? a : b;

    return (dummy.next);
}

/* マージソート本体 */
t_list *ft_lstsort(t_list *lst, int (*cmp)(void *, void *))
{
    t_list *second_half;

    /* ベースケース:0または1要素 */
    if (!lst || !lst->next)
        return (lst);

    /* 分割 */
    second_half = split_list(lst);

    /* 再帰的にソート */
    lst = ft_lstsort(lst, cmp);
    second_half = ft_lstsort(second_half, cmp);

    /* マージ */
    return (merge_sorted(lst, second_half, cmp));
}

/*
 * 比較関数の例
 */
int cmp_int(void *a, void *b)
{
    return (*(int *)a - *(int *)b);
}

int cmp_string(void *a, void *b)
{
    return (ft_strcmp((char *)a, (char *)b));
}

6.6.3 循環検出(フロイドのアルゴリズム)

/*
 * フロイドの循環検出アルゴリズム(亀とウサギ)
 *
 * 計算量: O(n) 時間, O(1) 空間
 */

int ft_lsthas_cycle(t_list *lst)
{
    t_list *slow;
    t_list *fast;

    slow = lst;
    fast = lst;

    while (fast && fast->next)
    {
        slow = slow->next;          /* 1歩進む */
        fast = fast->next->next;    /* 2歩進む */

        if (slow == fast)
            return (1);  /* 循環を検出 */
    }

    return (0);  /* 循環なし */
}

/*
 * なぜこれが機能するか:
 *
 * 循環がある場合:
 * - fastはslowより早く循環に入る
 * - 循環内で、fastはslowに1歩ずつ近づく
 * - 最終的に同じノードで出会う
 *
 * 循環がない場合:
 * - fastは末尾(NULL)に到達してループ終了
 */

/* 循環の開始点を見つける */
t_list *ft_lstfind_cycle_start(t_list *lst)
{
    t_list *slow;
    t_list *fast;

    slow = lst;
    fast = lst;

    /* 循環を検出 */
    while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;

        if (slow == fast)
            break;
    }

    if (!fast || !fast->next)
        return (NULL);  /* 循環なし */

    /* slowを先頭に戻し、両方を1歩ずつ進める */
    slow = lst;
    while (slow != fast)
    {
        slow = slow->next;
        fast = fast->next;
    }

    return (slow);  /* 循環の開始点 */
}

/*
 * なぜこれが機能するか(数学的証明):
 *
 * 設定:
 * - リストの先頭から循環開始点までの距離 = m
 * - 循環の長さ = n
 * - 出会った点から循環開始点までの距離(逆向き)= k
 *
 * 出会った時点:
 * - slowの移動距離 = m + k
 * - fastの移動距離 = m + k + p*n (p周回)
 * - fastはslowの2倍移動:2(m + k) = m + k + p*n
 * - よって:m + k = p*n
 * - よって:m = p*n - k = (p-1)*n + (n-k)
 *
 * これは「m歩進むと循環開始点に到達」を意味する
 * 出会った点からn-k歩進んでも循環開始点に到達
 * 両者を同時に1歩ずつ進めると、循環開始点で出会う
 */

6.7 メモリ管理のベストプラクティス

6.7.1 所有権モデル

/*
 * 所有権の3つのパターン
 */

/* パターン1:リストがcontentを所有 */
typedef struct s_owning_list {
    t_list  *head;
    void    (*destructor)(void *);
} t_owning_list;

void owning_list_add(t_owning_list *list, void *content)
{
    t_list *node = ft_lstnew(content);
    ft_lstadd_front(&list->head, node);
    /* list がcontentの所有権を取得 */
}

void owning_list_clear(t_owning_list *list)
{
    ft_lstclear(&list->head, list->destructor);
    /* すべてのcontentを解放 */
}

/* パターン2:リストはcontentを借用 */
typedef struct s_borrowing_list {
    t_list  *head;
} t_borrowing_list;

void borrowing_list_add(t_borrowing_list *list, void *content)
{
    t_list *node = ft_lstnew(content);
    ft_lstadd_front(&list->head, node);
    /* contentの所有権は呼び出し元に残る */
}

void borrowing_list_clear(t_borrowing_list *list)
{
    ft_lstclear(&list->head, NULL);  /* ノードのみ解放 */
    /* contentは解放しない */
}

/* パターン3:リストがcontentをコピー */
typedef struct s_copying_list {
    t_list  *head;
    void    *(*copy)(void *);
    void    (*destructor)(void *);
} t_copying_list;

void copying_list_add(t_copying_list *list, void *content)
{
    void *copy = list->copy(content);
    t_list *node = ft_lstnew(copy);
    ft_lstadd_front(&list->head, node);
    /* listが所有する独立したコピー */
}

6.7.2 安全なイテレータパターン

/*
 * イテレータ:リストの走査を抽象化
 */

typedef struct s_list_iterator {
    t_list  *current;
    t_list  *next;  /* 削除を安全にするため */
} t_list_iterator;

t_list_iterator list_iter_begin(t_list *list)
{
    t_list_iterator iter;

    iter.current = list;
    iter.next = list ? list->next : NULL;

    return iter;
}

int list_iter_has_next(t_list_iterator *iter)
{
    return iter->current != NULL;
}

void *list_iter_next(t_list_iterator *iter)
{
    void *content;

    if (!iter->current)
        return NULL;

    content = iter->current->content;
    iter->current = iter->next;
    iter->next = iter->current ? iter->current->next : NULL;

    return content;
}

/* 安全なイテレーション(削除も可能) */
void safe_iteration_example(t_list **list, int (*should_remove)(void *))
{
    t_list *prev = NULL;
    t_list_iterator iter = list_iter_begin(*list);

    while (list_iter_has_next(&iter))
    {
        t_list *current = iter.current;
        void *content = list_iter_next(&iter);

        if (should_remove(content))
        {
            if (prev)
                prev->next = current->next;
            else
                *list = current->next;

            ft_lstdelone(current, free);
        }
        else
        {
            prev = current;
        }
    }
}

6.8 パフォーマンス最適化

6.8.1 メモリプールの活用

/*
 * ノード用のメモリプール
 *
 * 頻繁な小さなmallocを避けて性能向上
 */

#define POOL_BLOCK_SIZE 64

typedef struct s_node_pool {
    t_list  *free_list;
    void    *blocks;        /* 割り当てたブロックのリスト */
    size_t  total_nodes;
} t_node_pool;

t_node_pool *node_pool_create(void)
{
    t_node_pool *pool = malloc(sizeof(t_node_pool));
    if (!pool)
        return NULL;

    pool->free_list = NULL;
    pool->blocks = NULL;
    pool->total_nodes = 0;

    return pool;
}

/* 新しいブロックを割り当て */
static int pool_grow(t_node_pool *pool)
{
    /* ノードのブロックを割り当て */
    t_list *block = malloc(sizeof(t_list) * POOL_BLOCK_SIZE);
    if (!block)
        return -1;

    /* フリーリストに追加 */
    for (size_t i = 0; i < POOL_BLOCK_SIZE; i++)
    {
        block[i].next = pool->free_list;
        pool->free_list = &block[i];
    }

    pool->total_nodes += POOL_BLOCK_SIZE;
    return 0;
}

t_list *pool_alloc_node(t_node_pool *pool, void *content)
{
    t_list *node;

    if (!pool->free_list)
    {
        if (pool_grow(pool) < 0)
            return NULL;
    }

    node = pool->free_list;
    pool->free_list = node->next;

    node->content = content;
    node->next = NULL;

    return node;
}

void pool_free_node(t_node_pool *pool, t_list *node)
{
    node->next = pool->free_list;
    pool->free_list = node;
}

6.8.2 キャッシュ効率の改善

/*
 * 「展開」連結リスト(Unrolled Linked List)
 *
 * 各ノードに複数の要素を格納してキャッシュ効率を改善
 */

#define UNROLLED_BLOCK_SIZE 16

typedef struct s_unrolled_node {
    void                    *items[UNROLLED_BLOCK_SIZE];
    int                     count;
    struct s_unrolled_node  *next;
} t_unrolled_node;

typedef struct s_unrolled_list {
    t_unrolled_node *head;
    t_unrolled_node *tail;
    size_t          size;
} t_unrolled_list;

/*
 * 通常の連結リスト vs 展開連結リスト
 *
 * 通常:
 * [ptr|next] → [ptr|next] → [ptr|next] → ...
 * キャッシュミスが多い
 *
 * 展開:
 * [item1|item2|...|item16|next] → [item1|item2|...|item16|next]
 * 連続アクセスでキャッシュを有効活用
 */

void unrolled_add(t_unrolled_list *list, void *item)
{
    t_unrolled_node *node;

    if (!list->tail || list->tail->count >= UNROLLED_BLOCK_SIZE)
    {
        /* 新しいノードが必要 */
        node = malloc(sizeof(t_unrolled_node));
        node->count = 0;
        node->next = NULL;

        if (!list->head)
            list->head = node;
        else
            list->tail->next = node;

        list->tail = node;
    }

    list->tail->items[list->tail->count++] = item;
    list->size++;
}

6.9 本章のまとめ

6.9.1 学習した内容の総括

この章では、連結リストの実装を通じて以下の重要概念を学んだ:

1. データ構造の理論的基盤

  • 抽象データ型(ADT)の概念
  • 計算量理論とBig-O記法
  • 配列と連結リストのトレードオフ

2. ポインタと参照の理論

  • 間接参照のレベル
  • ダブルポインタの必要性
  • 自己参照構造体

3. 再帰的データ構造と処理

  • 帰納的定義と再帰的関数
  • 末尾再帰最適化
  • 再帰から反復への変換

4. 連結リストの変種

  • 双方向連結リスト
  • 循環連結リスト
  • センチネルノード

5. 高度なアルゴリズム

  • リストの反転
  • マージソート
  • 循環検出(フロイドのアルゴリズム)
  • 6.9.2 実装チェックリスト

    libft連結リスト関数の実装時に確認すべき項目:

    □ ft_lstnew: contentをそのまま保存(コピーしない)
    □ ft_lstadd_front: ダブルポインタで headを更新
    □ ft_lstsize: 全ノードを走査してカウント
    □ ft_lstlast: NULL チェックを忘れずに
    □ ft_lstadd_back: 空リストの特殊ケースを処理
    □ ft_lstdelone: del(content) の後に free(node)
    □ ft_lstclear: nextを保存してから現在ノードを削除
    □ ft_lstiter: fのNULLチェック
    □ ft_lstmap: 部分的な失敗時のクリーンアップ
    

    6.9.3 次のステップ

    連結リストをマスターしたら:

  • より高度なデータ構造
- 二分木、AVL木、赤黒木 - ハッシュテーブル - グラフ

  • 42の次のプロジェクト
- ft_printf:書式付き出力の実装 - get_next_line:バッファ管理とファイルI/O - push_swap:スタック操作とソートアルゴリズム

  • 実務での応用
- カスタムメモリアロケータ - 高性能キューとバッファ - イベント駆動システム

連結リストは、すべてのデータ構造の基礎となる。この章で学んだ概念は、より複雑な構造(木、グラフ、ハッシュテーブル)を理解する上で不可欠な基盤となる。