JavaScript | 再帰関数

JavaScript JavaScript
スポンサーリンク

JavaScriptの再帰関数を初心者向けにやさしく解説

まず、不安や「難しそう…」って気持ちがあるならそれで普通。再帰は最初ひっかかりやすいテーマだけど、ポイントさえ掴めば一気に分かりやすくなる。例題で「どう動いているのか」を目で追えるようにしていくね。


再帰の基本と考え方

  • 定義: 関数の中で「自分自身」をもう一度呼び出す書き方のこと。
  • イメージ: 問題を「少しだけ小さい同じ問題」に分けて、終わりまで小さくしていく。
  • 絶対ルール: 終了条件(ベースケース)を必ず作る。これがないと永遠に呼び出しが続いて止まらない。

たとえるなら、階段を下りるときに「1段ずつ下りる(問題を小さくする)」と「1階に着いたら終わり(終了条件)」の2つが必要、という感じ。


再帰で大事な2パーツ

  • 終了条件(ベースケース): ここに来たらもう呼び出しをやめて値を返す。
  • 再帰ステップ: 問題を少し小さくして、同じ関数をもう一度呼ぶ。
function example(n) {
  // 終了条件
  if (n === 0) return; 

  // ここに処理を書く(例: 表示)
  console.log(n);

  // 再帰ステップ(問題を小さく)
  example(n - 1);
}
JavaScript

例題1:カウントダウン(流れが見える入門)

ループでの書き方

for (let n = 5; n > 0; n--) {
  console.log(n);
}
JavaScript

再帰での書き方(同じ動き)

function countdown(n) {
  if (n === 0) {
    console.log("Start!");
    return; // 終了条件
  }
  console.log(n);
  countdown(n - 1); // 問題を少し小さく
}

countdown(5);
// 5
// 4
// 3
// 2
// 1
// Start!
JavaScript
  • ポイント: 呼ぶたびに数字が1つずつ小さくなる。0になったら「Start!」を出して終了。

例題2:階乗(「戻りながら計算する」タイプ)

  • 意味: 階乗は「(n!) は (n \times (n-1)!)」で定義される。特別に「(0! = 1)」がベースケース。
  • 再帰の動き: 下へ下へ潜って、最後に戻りながら掛け算が完成する。
function factorial(n) {
  if (n === 0) return 1; // 0! = 1(終了条件)
  return n * factorial(n - 1); // 再帰ステップ
}

console.log(factorial(5)); // 120
JavaScript
  • 頭の中の動き:
    • factorial(5) = 5 × factorial(4)
    • factorial(4) = 4 × factorial(3)
    • factorial(0) = 1(ここで止まる)
    • 1をもって順に「戻りながら」掛け算が解決される。

例題3:配列の合計(ループが苦手な人に優しい考え方)

ループでの書き方

const arr = [2, 4, 6];
let sum = 0;
for (let i = 0; i < arr.length; i++) {
  sum += arr[i];
}
console.log(sum); // 12
JavaScript

再帰での書き方

function sumArray(arr, i = 0) {
  if (i === arr.length) return 0;      // 終了条件: 末尾まで来たら0
  return arr[i] + sumArray(arr, i + 1); // 現在の要素 + 残りの合計
}

console.log(sumArray([2, 4, 6])); // 12
JavaScript
  • ポイント: 「先頭の値 + 残り全部の合計」という形に分解していく。

例題4:入れ子のデータを全部なめる(再帰が光る場面)

入れ子(ネスト)構造は、深さが分からないからループだけだと大変。再帰だと「中身が配列ならもう一度同じことをする」と書ける。

function flatten(sum, data) {
  for (const item of data) {
    if (Array.isArray(item)) {
      sum = flatten(sum, item); // 配列ならさらに深く
    } else {
      sum += item; // 値なら加算(例の処理)
    }
  }
  return sum;
}

const nested = [1, [2, [3, 4]], 5];
console.log(flatten(0, nested)); // 15
JavaScript
  • ポイント: 形が同じなら「同じ関数」をまた使える。それが再帰の強み。

再帰 vs ループの違い(どっちを選ぶ?)

  • 分かりやすさ:
    • ループ: 処理の順番が一直線で見えやすい。
    • 再帰: 入れ子構造や「同じ形の問題に分割」する時はシンプルに書ける。
  • 性能と注意:
    • 再帰: 呼び出しが深すぎるとメモリ(スタック)を使いすぎることがある。
    • ループ: 一般に軽くて安全。深さの制限がない。
  • 結論: 木構造やネストの走査、分割統治(クイックソートなど)は再帰が得意。それ以外はループで十分なことが多い。

よくあるつまずきと回避法

  • 終了条件が曖昧: 明確に「ここで止める」を書く。数値なら「0になったら」、配列なら「末尾まで来たら」など。
  • 問題を小さくし忘れ: nを減らす、インデックスを進める、配列の残りを渡すなど、1歩進める処理を必ず入れる。
  • 副作用だらけ: なるべく「引数を変えて同じ関数を呼ぶ」形に。グローバル変数の書き換えは混乱のもと。

手を動かす練習問題

  • 練習1: countUp(n) を作って、1からnまで表示する(終了条件と再帰ステップを自分で決める)。
  • 練習2: sumNested(arr) を作って、入れ子配列の数値合計を返す。
  • 練習3: 文字列を反転する reverse(str) を再帰で書く(先頭 + 残り、の分割を考える)。
  • 練習4: maxNested(arr) を作って、入れ子配列から最大値を見つける。

ヒントは「終了条件を先に書く → 問題を1歩だけ小さくして同じ関数を呼ぶ」。この順番で組み立てると迷いにくい。


模範解答と解説

練習1:countUp(n)

問題: 1からnまで順番に表示する。

解答例

function countUp(n, current = 1) {
  if (current > n) return;     // 終了条件
  console.log(current);        // 表示
  countUp(n, current + 1);     // 次の数へ
}

countUp(5);
// 1
// 2
// 3
// 4
// 5
JavaScript

解説

  • current を1から始めて、毎回 +1 していく。
  • current > n になったら終了。
  • 「終了条件」と「1歩進める」がそろっているので無限ループにならない。

練習2:sumNested(arr)

問題: 入れ子(ネスト)した配列の合計を求める。

解答例

function sumNested(arr) {
  let sum = 0;
  for (const item of arr) {
    if (Array.isArray(item)) {
      sum += sumNested(item);   // 配列なら再帰
    } else {
      sum += item;              // 数値なら加算
    }
  }
  return sum;
}

console.log(sumNested([1, [2, [3, 4]], 5])); // 15
JavaScript

解説

  • 配列の中にさらに配列があったら「同じ処理をもう一度」呼ぶ。
  • これが再帰の真骨頂。深さがいくらあっても同じ関数で処理できる。

練習3:reverse(str)

問題: 文字列を逆順にする。

解答例

function reverse(str) {
  if (str === "") return "";          // 終了条件: 空文字
  return reverse(str.slice(1)) + str[0];
}

console.log(reverse("hello")); // "olleh"
JavaScript

解説

  • str.slice(1) で先頭を除いた残りを再帰的に反転。
  • 最後に str[0] を後ろにくっつける。
  • こうして戻りながら文字が逆順に並ぶ。

練習4:maxNested(arr)

問題: 入れ子配列の中から最大値を探す。

解答例

function maxNested(arr) {
  let max = -Infinity;
  for (const item of arr) {
    if (Array.isArray(item)) {
      max = Math.max(max, maxNested(item)); // 配列なら再帰
    } else {
      max = Math.max(max, item);            // 数値なら比較
    }
  }
  return max;
}

console.log(maxNested([1, [7, [3, 9]], 5])); // 9
JavaScript

解説

  • 合計のときと同じ構造だが、処理が「足し算」ではなく「最大値の比較」になっている。
  • 再帰のパターンは「配列なら再帰」「値なら処理」という形で応用できる。

まとめ

  • countUp → 数字を1歩ずつ進める
  • sumNested → ネストをほどきながら合計
  • reverse → 先頭と残りに分けて逆順に組み立て
  • maxNested → ネストをほどきながら最大値を更新

再帰のコツは「終了条件」と「問題を小さくする」の2つを必ずセットにすること。

タイトルとURLをコピーしました