整列6(クイック)

クイックソート

クイックソートは世の中で最も速いソートアルゴリズムとして知られています。(これは、一つのCPUで順次処理を行なった場合である。最近では、計算機の機能が高まり、並列処理や分散処理が可能となっているため、複数のCPUを持った計算機上での並べ換えについては、クイックソートよりも速いアルゴリズムが提案されている。)

クイックソートは、データを分割してより小さな単位でソートし、あとであわせるという手法をつかって効率を上げています。つまり、

        1 データのブロック分割
        2 各ブロックごとの処理(並べ換え)
        3 ブロックの統合

という順に作業がすすめられるわけです。速くなるかどうかは1のデータの分割の仕方が工夫のしどころでいろいろなやり方が提案されています。ここで取り上げるのはその中の1つです。。

考え方

次のような数字で実際にやってみます。

(1)まず与えられたデータ列の真ん中(左から6番目)に位置する データを基準値として定めます。例では、35を基準値とします。

 52  46  24  12  15  35  18  56  63  23 

(2)データを左から順に基準値よりも大きいデータを捜します。まず52が見つかります。続いて、右から順に基準値よりも小さいデータを捜します。 23が見つかります。

 52  46  24  12  15  35  18  56  63  23 

(3)ここで、52と23を入れ替えます。現在注目しているデータの位置を次に進めます(左から見ている場合は一つ右へ、右からの場合は左へ一つ進める)。

 23  46  24  12  15  35  18  56  63  52 

(4)引き続き左から順に基準値よりも大きいデータを捜します。続きは46から始まるがすぐ46が見つかります。続いて、右から順に基準値よりも小さいデータを捜します。 18が見つかります。

 23  46  24  12  15  35  18  56  63  52 

(5)46と18を交換します。

 23  18  24  12  15  35  46  56  63  52 

(6)続けて左から基準値より大きいデータを探します。基準値に等しい場合も取り上げることにすると35そのものに合います。右からも同様に小さいものを探すと35に合います。これで左右からの捜索がぶつかりました。

ぶつかった所より左は基準値より小さなものが、右は大きいものが集まっています。

 23  18  24  12  15  35  46  56  63  52 

ここで、2つの部分に分けて、それぞれ(1)からと同様に作業をしていきます。

(7)前半分は1番から5番までです。データ列の真ん中(左から3番目)に位置するデータを基準値として定めます。例では、24になります。以下同様なのですが、今回は基準値が、たまたまその範囲の最高値になっているのでちょっと面倒です。こんなときでもうまくいきます。

 23  18  24  12  15  35  46  56  63  52 

(8)データを左から順に基準値(24)よりも大きいデータを捜します。24です。続いて、右から順に基準値よりも小さいデータを捜します。 15です。

 23  18  24  12  15  35  46  56  63  52 

(9)24と15を交換します。

 23  18  15  12  24  35  46  56  63  52 

(10)続けて左側から基準値より大きいものを探します。さっきは24が3番にありました。今度は4番から探します。すると5番の24が見つかります。右側からは、基準値より小さいものを探します。さっきは5番に15がありましたから今度は4番から始めます。すぐ12が小さいということになります。

 23  18  15  12  24  35  46  56  63  52 

(11)これで左右からの捜索がぶつかりました。左から来たのは5番の24まで来ましたし、右からのは4番の12まできましたから、交差して行き過ぎています。このようなときは交換せず、4番と5番の間でデータを2つに分けます。基準値であった24よりも小さいものと大きいものに分かれます。右側の24より大きいのは1つしかありませんが、たまたまこの範囲の最大値だったので仕方ありません。一つしかない要素はこのまま位置が確定します。

 23  18  15  12  24  35  46  56  63  52 

(12)分かれた1番から4番に対してふたたび(1)からのように、作業をします。繰り返すうち、要素が1つ部分ができては確定してゆきます。前半が終われば後半に進み、ついには全部整列されます。

ある基準値を決めて、それより大きいか小さいかで2つに分け、分かれたものについても同じことを繰り返す。ということです。基準値はデータの大きさで真ん中あたりの数であれば都合がいいのですが、それをじっくり選んでいると時間がかかるので位置で真ん中のものを「えいっ」とつかんで採用するわけです。

原理的にはここまでなのですが、実際にプログラムを組むときには分けるときの境界を含めるか含めないかで迷います。=< を使うか、< を使うかを間違えると、うまく働かないこともありますからじっくり考えなければならないことになります。

プログラム例

配列の用意

前と同じです。乱数でつくる方を使いましょう。

RANDOMIZE
INPUT n
DIM a(n)
FOR i=1 TO n
   LET  a(i)=INT(RND*1000)
NEXT i

整列の本体部分

この繰り返しは、単純ではありません。たしかにa番目からb番目までを基準値に従って2つに分けるという作業が繰り返されいますが、2つに分けたその両方をさらに2つに分けると、いうところで行き詰まります。

ここで新しい手法を使います。それは再帰と呼ばれます。

まず、「a番目からb番目までを基準値に従って2つに分ける」という作業を副プログラム(サブルーチン)として独立させます。データとa,bを与えてこの副プログラムを呼び出すと、データを2つに分けてくれる様にするということです。データを用意して作業員に「こっちへ来てやってくれ」と呼び出すという感覚から、副プログラムは「呼び出す」といいます。

この教科書では初めてですが、副プログラムの使用はそう難しくありません。問題は次です。この副プログラムの中で、自分自身を呼び出す様にするのです。(十進BASICではこれが可能です。システムによってはできません)この「副プログラムの中で、自分自身を呼び出す」ことを「再帰呼び出し」といいます。

再帰は考え方としては難しいのですが、とても強力な見方です。今回の例でいえば、「a番目からb番目までを基準値に従って2つに分ける」という副プログラムの最後に、2つに分けた部分をさらに2つに分けるように自分自身を呼びだす部分をつくっておけばよいのです。そうすれば2つに分けるたびに、新たにできた2つもさらに2つに分けられて、ついに1つづつになったときには整列が完成しているということになります。ドミノ倒しの様に最初の1つだけかけば後は全部自動的に終わってしまいます。という訳で整列の本体部分はこれだけです。

LET  t0=TIME
CALL sort(a,1,n)
LET  t1=TIME

t0=TIME などは時間を計るためのものですから、本体は本当は1行で終わりです。sort は「a番目からb番目までを基準値に従って2つに分ける」という副プログラムの名前で、変数の名前のように自分で決めます。後ろの(  )内は引数といって副プログラムに渡すデータです。a( ) の配列全部と、最初の番号と最後の番号です。はじめは1からnまでになります。副プログラムの内容は後で説明します。

注意 これまでは後で説明する内容で色つきの部分が書き換えて完成でしたが、今回は違います。この部分は本当にこれで完成です。

整列した結果の表示

結果表示はやはり1000件以下の時だけ結果を表示する様にします。かかった時間だけはいつも表示します。

IF n < 1000 THEN
   FOR i=1 TO n
      PRINT a(i);
   NEXT i
   PRINT
END IF
PRINT t1-t0;"秒"

主プログラムと副プログラムの関係

主プログラムと副プログラムの関係を説明します。(主プログラムと副プログラムはプログラマの間では普通メインルーチン、サブルーチンといいます)緑の部分が主プログラム青の部分が副プログラムです。

DECLARE EXTERNAL SUB sort    !使用する副プログラムの宣言(sort を使うぞ!)

配列の用意

CALL sort(a,1,n)     !ここで副プログラムを呼び出す

整列した結果の表示

END                  !ここが主プログラムの終わり。忘れずにここにENDを書く。

EXTERNAL SUB sort(a(),sml,big)   !副プログラム本体の始まり(これがsortだ!)
sml番目からbig番目までを基準値に従って2つに分け、
   前半を指定して再帰呼び出しをする……CALL sort(a,sml,bg)
   後半を指定して再帰呼び出しをする……CALL sort(a,sm,big)
END SUB                                  !ここが副プログラムの終わり。

副プログラムは EXTERNAL SUB 名前(引数)という行で始まり、END SUB で終わります。副プログラムは同じような処理が何度も出てくるときに使いますが、今回は再帰呼び出しで使っているので、副プログラム中にも CALL があります。

副プログラム

EXTERNAL SUB sort(a(),sml,big)   !副プログラム本体の始まり(これがsortだ!)
LET sm=sml
LET bg=big
LET x=ROUND((sm+bg)/2,0)
LET kjn=a(x)                 !これが基準値

DO
   DO WHILE a(sm) < kjn   !左から大きいものを検索
      LET sm=sm+1
   LOOP
   DO WHILE kjn < a(bg)   !右から小さいものを検索
      LET bg=bg-1
   LOOP
   IF sm =< bg THEN         !交換して先に進む
      LET t=a(sm) 
      LET a(sm)=a(bg)
      LET a(bg)=t
      LET sm=sm+1
      LET bg=bg-1
   END IF
LOOP WHILE sm =< bg

IF sml < bg THEN CALL sort(a,sml,bg)   !前半を指定して再帰呼び出し
IF sm < big THEN CALL sort(a,sm,big)   !後半を指定して再帰呼び出し
END SUB                                !ここが副プログラムの終わり。

例37

上記プログラムを完成させ、時間をはかりなさい。

time.txtに追加記入してBASIC内に提出

方式1000個2000個10000個
選択ソート x.xx xx.x xxx.x
バブルソート x.xx xx.x xxx.x
クイックソート x.xx xx.x xxx.x

聖愛高等学校
http://www.seiai.ed.jp/