第30問の答え

答え

次のように、再帰呼び出しを使うのが正解!
int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumListValues(pNode->pNext);
}

解説

これは、「再帰呼び出し(recursive call)」に関する出題でした。再帰呼び出しとは、ある関数の処理中で、その関数を再び呼び出す手法のことです。

問題の関数は、次のような形をしていました。

int SumListValues(const ListNode *pNode) {
  ……
}

この関数は、引数pNodeで連結リストのノードを受け取ります。そして、そのノード以降の値の合計を返すというものです。

値の合計を再帰呼び出しで求めるには、次の2つに分けて考えればいいですね。

  • pNodeの値(pNode->value
  • pNodeの次のノード(pNode->pNext)以降の値の合計

この2つを足せば、pNode以降の値の合計になるはずです。

次のノード以降の値を合計するところで再帰呼び出しを使うのよ。つまり、自分自身を呼び出すということね。
あ、分かったかも!
2つに分けて、自分自身を呼び出す……と、できた!
int SumListValues(const ListNode *pNode) {
  return pNode->value + SumListValues(pNode->pNext);
}
ふむふむ。それじゃあ、実行してみましょうか。
はーい!……ぐはぁ、クラッシュしました。
そうよね。でも、おしいところまできてるのよ。

自分自身を無制限に呼び出し続けようとすれば、いずれプログラムはクラッシュしてしまいます。そのため、再帰呼び出しは「どこでストップするか」が重要です。

今のクラッシュは、ノードが4つしか存在しないのに対し、5回目の呼び出しを行ったために発生したものでした。このとき、引数は0(NULL)になっていたはずです。最後のノードに対するpNode->pNextには、0が格納されているためです。

  ListNode node4 = { 40, 0 }; /* ← 最後のノードのpNextは0(NULL) */
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };
引数pNodeが0だったときに、自分自身を呼び出さないようにすればいいわね。
ふむふむ。じゃあif文を使って……
int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    /* ここでストップさせたい! */
  }

  return pNode->value + SumListValues(pNode->pNext);
}
えっと、呼び出しをストップさせるのにbreakは使えないですよね?
うん、ループじゃないものね。ここではreturnが使えるわよ。
そっか!pNodeが0のときは値がないから……
int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumListValues(pNode->pNext);
}
こうやって0を返せばいいんですね!
正解!これでクラッシュせずに、合計を求められるようになったはずよ。
なんだか、すごくシンプルなプログラムになっちゃいましたね。
そう見えるのは、レオ君が仕組みを理解したからよ。
えっ?
再帰呼び出しを知らない人から見れば、これは難解なプログラムだと思うわ。
たしかに……じゃあ、普通にループを使ったほうが人に優しいってことになりませんか?
そうかもしれないわね。でもね、やっぱり再帰呼び出しが必要っていう場面もあるのよ。

今回の題材は、連結リスト構造でした。そのノードは、次のような形をしています。

typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;

ノードが一列につながるため、ループで処理できることも多いでしょう。

では、データがツリー構造だった場合はどうでしょうか。例えば、つながりが左右2方向に枝分かれする「二分木(binary tree)」のノードは、次のような形をしています。

typedef struct TreeNode {
  int value;
  struct TreeNode *pLeft;
  struct TreeNode *pRight;
} TreeNode;

ツリー内の値を合計したいと思ったとき、whileループやforループを使うのは無理があります。でも、次のように再帰呼び出しを使えば、プログラムはシンプルになります。

int SumTreeValues(const TreeNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumTreeValues(pNode->pLeft) + SumTreeValues(pNode->pRight);
}

これは、値の合計を次の3つに分けて考え、足し合わせているのです。

  • pNodeの値(pNode->value
  • pNodeの左側のノード(pNode->pLeft)以降の値の合計
  • pNodeの右側のノード(pNode->pRight)以降の値の合計
データが一列に並んでいればループで処理できるけど、枝分かれしているなら再帰呼び出しって感じですかね?
そうね。そういうイメージでいいと思うわ。
ここがポイント!
再帰呼び出しを使いこなせば、いろいろな構造のデータをシンプルに処理できる!

さて。

ここまで読んでいただき、ありがとうございました!C言語にまつわる全30問のクイズ、いかがだったでしょうか?現実の問題解決に役立てられる部分が少しでもあったなら、とても嬉しいです。

本シリーズの問題は、読み物としても楽しんでいただけるよう心がけて制作しました。もし「途中から読んだよ」という場合は、ぜひ「問題一覧」に戻って、ほかの問題にもチャレンジしてみてください!

僕ももう1回、第01問から見直してみたいです!
そうね。時間をあけてから見直すと、何か新しい発見があるかもしれないわね!

修正後のプログラム

main.c
#include <stdio.h>
#include <stdlib.h>

typedef struct ListNode {
  int value;
  struct ListNode *pNext;
} ListNode;

int SumListValues(const ListNode *pNode) {
  if (pNode == 0) {
    return 0;
  }

  return pNode->value + SumListValues(pNode->pNext);
}

int main(void) {
  ListNode node4 = { 40, 0 };
  ListNode node3 = { 30, &node4 };
  ListNode node2 = { 20, &node3 };
  ListNode node1 = { 10, &node2 };

  printf("Sum: %d\n", SumListValues(&node1));

  return EXIT_SUCCESS;
}
実行結果
Sum: 100