サンプル問題 科目B 問13

問13 次の記述中の [  ] に入れる正しい答えを,解答群の中から選べ。ここで,配列の要素番号は 1 から始まる。

関数 search は,引数 data で指定された配列に,引数 target で指定された値が含まれていればその要素番号を返し,含まれていなければ  -1 を返す。data は昇順に整列されており,値に重複はない。 

関数 search には不具合がある。例えば,data の [  ] 場合は,無限ループになる。 

 

〔プログラム〕 
○整数型: search(整数型の配列: data, 整数型: target) 
  整数型: low, high, middle 
 
  low ← 1 
  high ← dataの要素数 
 
  while (low ≦ high) 
    middle ← (low + high) ÷ 2 の商 
    if (data[middle] < target) 
      low ← middle 
    elseif (data[middle] > target) 
      high ← middle 
    else 
      return middle 
    endif 
  endwhile 
 
  return -1 
 
解答群 
ア  要素数が 1 で,target がその要素の値と等しい 
イ  要素数が 2 で,target が data の先頭要素の値と等しい 
ウ  要素数が 2 で,target が data の末尾要素の値と等しい 
エ  要素に-1 が含まれている 

出典:基本情報技術者試験 サンプル問題

問題を見て思うこと

このプログラムを見ると、これまでの業務の経験から、慎重に進める必要があると直感しました。配列でリングバッファを作るような場合とか、双方向リストを操作するような場合とか、今まで何度も失敗してきた経験があります(基本情報技術者試験では出ないのかも知れませんが)。この問題も、それに近いものを感じました。言わば、野生の勘です。

このプログラムで行いたいこと

このプログラムは、配列で二分探索を行おうとしています。つまり、配列の真ん中の値とtargetを比較して、targetが小さければ配列の前半に対して同じことを実行し、targetが大きければ配列の後半に対して同じことを実行する、ということを繰り返します。こうすることで、探索範囲がまず半分、さらにその半分、さらにその半分・・・と狭くなっていき、探索を効率よく行うことができるはずです。

なぜ無限ループになるのか

探索範囲がどんどん半分になれば、最終的に探索範囲が配列の要素1個になって、無限ループにならないように思います。そうならないということは、探索範囲を狭めるやり方に問題があるのかも知れません。

具体的に考えてみる

いつものように、具体例で考えてみます。次の図のように、dataが4つの要素からなり、その値は要素番号の2倍になっているものとします。ここで、target=8、つまり要素番号が最も大きい値を探します。

図に書き忘れましたが、変数lowの値を青色、変数highの値を赤っぽい色、変数middleの値を黄色で塗っています。配列dataの右側にあるlow, middle, highと書いて色が塗ってあるセルは、単に変数と色の関係を説明するものです。

1週目

まず、#1でlow(=1)とhigh(=4)を配列の両端に設定しています。

#3で配列の真ん中であるmiddle(=2.5の商で2)を計算します。

#4でtargetとの大小を判断して、targetは配列の真ん中の値より大きいので、#5でlowを配列の真ん中まで移動して、探索範囲を配列の後半にします。

2週目

#6から1週目と同じパターンで、#7でmiddle(=3)を計算して、targetは配列の真ん中の値より大きいので、1週目と同様にlowを配列の真ん中に移動します。

3週目(ここで無限ループ発生)

#10から3週目に入り、#11でmiddleを計算します。

ところが、low=3, high=4なので、middle=3になります。これは、#7と同じ計算結果です。

こうなると、計算を繰り返しても2週目と同じ結果になり、変数low, high, middleの値がどれも動かず、無限ループになります。

答えを考える

無限ループになるパターンが見つかったので、答えを考えてみます。最終的に、探索範囲の左端であるlow=3、探索範囲の右端であるhigh=4なので、配列の要素数は2と同じことです。targetはその2つの要素のうち、要素番号が大きい方(つまり末尾要素)と等しくなっています。この特徴にあう答えを解答群から探します。

ア  要素数が 1 で,target がその要素の値と等しい

上で考えた条件とは、まず要素数が異なります。

素数が1であれば、middleがその要素を指すので、targetと一致するかどうかすぐに判断でき、無限ループにはならないはずです。

こういうことから、この選択肢は違います。

イ  要素数が 2 で,target が data の先頭要素の値と等しい

素数が2というのは、上で考えた条件と一致します。

しかし上の例からわかるように、middleを計算すると配列の先頭要素を指します。すると、targetと一致するかどうかすぐに判断でき、無限ループにはならないはずです。

したがって、この選択肢は違います。

ウ  要素数が 2 で,target が data の末尾要素の値と等しい

素数が2で、targetが配列の末尾要素というのは、上で考えた条件と一致します。

ただし、よく考えてみると、targetとdataの末尾要素の値を比較しない(なぜならmiddleが末尾要素を指さないから)ので、targetがdataの末尾要素の値と等しくなくても無限ループになります。言い換えると、要素数が2で、targetがdataの先頭要素の値より大きい、というのが条件のように思います。

そんなこともありますが、この設問であれば、この選択肢が正しいです。

エ  要素に-1 が含まれている

このプログラムでは、要素の値の大小は見ますが、特定の値で処理を変えたりしないので、この選択肢は違います。

正しく動くプログラムにするには

このプログラムでは、target が配列の真ん中の値より大きければ low ← middle 、小さければ high ← middle を実行しています。

しかし、これらを実行している時 data[middle] の値は target と一致しないのだから、新しい探索範囲を計算するときにmiddleが指す配列要素は除外できます。

つまり、次のように変えても問題ないはずです。

    if (data[middle] < target) 
      low ← middle + 1
    elseif (data[middle] > target) 
      high ← middle - 1

こうすることで、low、highの値がどちらも変わらない、という状況を避けられます。

このように変更して実行すると、#5, #9でlowの値が変わります。そして、最終的に#11でmiddleが配列の末尾要素を指すので、次にtargetと比較すると一致するかどうか判断でき、無限ループにはなりません。