第54回
アルゴリズムの基礎・4~二分探索

二分探索をプログラムにする

この処理をプログラムにしてみましょう。例に掲げた単純な探索処理は、リスト1のようになります。

変数の初期化

探索対象のデータ群は、あらかじめint型の配列“arry”として初期化しておきます。
int arry[] = {1, 3, 5, 8, 12, 16, 19, 24, 26, 31, 35};

配列の範囲を半分ずつ狭めていくため、範囲を表す添字の最小・中間・最大を示す変数を用意します。
int lowid, midid, highid;

探索する値24はint型の変数“target”に保存しておきます。
int target = 24;

添字の初期化

これで前準備が整いました。ここから探索処理に入ります。

まず、添字の最大値と最小値を初期化します。最初は最小値=0、最大値=10として、配列全体を対象とします。
highid = 10;
lowid = 0;

whileループの処理

続いてwhileループに入り、目的の値(24)が見付かるまで『探索対象の範囲を半分に絞っていく』処理を繰り返します。

ループの継続条件は
最小値を示すlowidの値が、最大値を示すhighidの値以下である間
です。

while (lowid <= highid) {

ループの先頭では、探索対象の配列の中間値を変数mididに保存します。
midid = (lowid + highid) / 2;

続くprintf文では、処理の過程を見せるために添字の変化を表示しています。探索処理の本質とは無関係です。
printf("lowid=%d, midid=%d. highid=%d¥n",
   lowid, midid, highid) ;

繰り返しと脱出の条件

探索範囲の中間値が探索値(24)と等しくなったら値が見付かったことになるので、そのときにはループを抜けます。
if (arry[midid] == target) {
  break;
}

そうでなく、探索範囲の中間値が探索値(24)より小さい場合は、探索値が中間値より大きい範囲に存在することになるので、対象範囲の最小の添字(lowid)を中間値の添字(midid)にします。これで、探索対象の範囲が現時点での中間から最大の範囲に狭まります。
else if (arry[midid] < target) {
  lowid = midid + 1;

そうでなければ、探索範囲の中間値が探索値(24)より大きいことになるため、対象範囲の最大の添字(highid)を中間値の添字(midid)にします。これで、探索対象の範囲が現時点での最小から中間の範囲に狭まります。
else {
  highid = midid - 1;
}

こうしてループを繰り返せば、ループを抜けた時点で添字“midid”の示す要素(arry[midid])に探索値が保存されています。

リスト1:11個の配列から24を探す処理~ex5401.c
#include <stdio.h>

/* 探索対象の配列 -- 11個 */
int arry[] = {1, 3, 5, 8, 12, 16, 19, 24, 26, 31, 35};

int main(void)
{
  /* 配列の範囲を狭めるための添字 */
  int lowid, midid, highid;
  /* 探索する値(固定) */
  int target = 24;

  int i;
  printf("探索対象の値 : ");
  for (i=0; i<10; i++) {
    printf("%2d, ", arry[i]);
  }
  printf("%2d¥n", arry[10]);

  /* 添字の範囲を初期化 */
  highid = 10;
  lowid = 0;

  /* 値が見つかるまで繰り返す */
  while (lowid <= highid) {
    midid = (lowid + highid) / 2;
    /* 添字の変化を表示 */
    printf("lowid=%d, midid=%d. highid=%d¥n",
            lowid, midid, highid) ;
    /* 値が見つかればループを抜ける */
    if (arry[midid] == target) {
      break;
      /* 値の大小を調べて探索範囲を狭める */
    } else if (arry[midid] < target) {
      lowid = midid + 1;
    } else {
      highid = midid - 1;
    }
  }

  /* arry[midid] に探索する値 */
  printf("値 %d は %d 番目に見つかりました。¥n",
          target, midid + 1);
  return (0);
}

汎用性を持たせる

この処理にもう少し汎用性を持たせてみましょう。探索対象の値を17個にし、その中からユーザーが探索したい値を入力できるようにしたものがリスト2です。

冒頭で#defineプリプロセッサ指令によって記号定数_YESと_NOを定義していますが、これは値が見付かった場合と見付からなかった場合とでメッセージを切り替えるため、変数isfoundに代入する値です。

探索処理の仕組み自体はex5401.cと変わりません。その他の動作はコメントを参照してください。

リスト2:リスト1(ex5401.c)に汎用性を持たせて探索値を入力できるようにした~ex5402.c
/* ---------------------------------------------------------
  ex5402.c -- 二分探索・汎用版
              配列にない値を入力すると正しく動作しません。
   --------------------------------------------------------- */
#include <stdio.h>

/* 探索結果を示す論理値(intで代用) */
#define _YES 1
#define _NO  0

/* 探索対象の配列 -- 17個 */
int arry[] = {1, 3, 5, 8, 12, 16, 19, 24, 26,
                31, 35, 38, 40, 46, 47, 51, 63};

int main(void)
{
  int lowid, midid, highid;
  int target;

  int isfound = _NO;

  int i;
  printf("探索対象の値 : ¥n  ");
  for (i=0; i<16; i++) {
    printf("%2d, ", arry[i]);
  }
  printf("%2d¥n", arry[16]);

  printf("探索値を入力してください : ");
  scanf("%d", &target);

  highid = 16;
  lowid = 0;

  while (lowid <= highid) {
    midid = (lowid + highid) / 2;
    /* 添字の変化を表示 */
    printf("lowid=%d, midid=%d. highid=%d¥n",
            lowid, midid, highid) ;
    /* 値が見つかればループを抜ける */
    if (arry[midid] == target) {
      isfound = _YES;
      break;
      /* 値の大小を調べて探索範囲を狭める */
    } else if (arry[midid] < target) {
      lowid = midid + 1;
    } else {
      highid = midid - 1;
    }
  }

  if (isfound == _YES) {
    /* arry[midid] にターゲット */
    printf("値 %d は %d 番目に見つかりました。¥n",
          target, midid + 1);
    return (0);
  } else {
    printf("値 %d は見付かりませんでした。¥n", target);
    return (-1);
  }
}


今回紹介した二分探索は、データ群が整列されていることが前提となります。整列されていないデータ群を対象とする場合には、以前取り上げた二分探索木の場合のように、元の配列の添字と値とを1組にした構造体を作らなければなりません。

今回は処理の仕組みを分かりやすくするため、整列の過程は割愛しました。

二分探索木では、木構造を生成する過程で整列と対象となるデータ群を半分にする処理を行っています。つまり二分探索木は、二分探索をさらに効率的にしたデータ構造なのです。

アルゴリズムとデータ構造は常に一体、と考えてよいでしょう。

あとがき ~宿題~

冒頭で紹介したパズル「9枚の金貨」では、金貨を3枚の組に分けることで効率的に偽物を発見できました。

では、偽物を含む金貨が10枚あった場合には、どのような手順を採れば効率的に偽物を見つけ出せるでしょう?

100円硬貨9枚(本物の金貨)と10円硬貨1枚(偽の金貨)などを使って、実際に手順をシミュレーションしながら考えてみましょう。