クイックソート
Copyright(C) 1May2008 coskx TNCT
ソートのアルゴリズムのうち高速なアルゴリズムとして知られているクイックソートについて解説する
1.クイックソート | |
与えられた数値配列についての昇順クイックソートは,与えられた配列を,「ある値未満のグループ」と「ある値以上のグループ」に分割し,それぞれのグループに対してクイックソートを適応する,という再帰で実現される。要素数が1つか2つ程度になったら,それ以上分割しないで,処理を行ない,元に戻ればよい。
ここで,ある値は,ピボット(pivot)と呼ばれ,配列の先頭の値が用いる。(配列の先頭を用いるのは,最悪の場合,ソートが非常に遅くなるので,配列の中央位置の値を使うことが多い。)
Fig.1 配列表現 |
グループに分ける時,最悪の場合は,すべての値が片側に偏ってしまうこともあるので,元と同じ大きさの新しい配列を2つ用意して2つのグループに分ければよい。
しかし,再帰で呼び出すことを考えると,毎回新しい配列を作っていたら,メモリを非常に多く消費してしまう。これを避けるため,元の1つの配列上で2つのグループに分ける手法を考える。
2.クイックソートのプログラミング | |
(1)配列の先頭をピボットとし,別の一時退避場所に退避させる。
(先頭(左端)に空席が出来た。)
(2)配列末尾(右端)から順にピボットと比較し,ピボット未満の値が1つ見つかったら,その値を左側の空席に移動する。
(今度は右側に空席が出来た。)
(3)左側の新しい数が入ってきた位置の1つ右隣から順にピボットと比較し,ピボット以上の値が1つ見つかったら,その値を右側の空席に移動する。
(今度は左側に空席が出来た。)
(4)「(2)」,「(3)」を繰り返し行ない,左右の比較位置が交わったら,繰り返しを終わる。この時,中間に空席が出来るので,退避させておいたピボットをその空席に戻す。
ここまでで分割が終了で,ピボットの左側には「ピボット値未満のグループ」ができ,右側には「ピボット値以上のグループ」が出来上がっている
(5)「ピボット値未満のグループ」と「ピボット値以上のグループ」についてクイックソートを呼び出せばよい。
List 1
クイックソートの関数(再帰) |
void QuickSort(int *x, int
num) |
3.クイックソートプログラム例 | |
List.2 クイックソートプログラム例 |
#include "stdio.h" #define DEBUG #ifdef DEBUG void makedata(int x[], int
num) void printdata(int x[], int
num) #ifdef DEBUG2 /*小さい順ソート済みデータのチェック /*配列の要素数numの配列xをクイックソートする*/ #ifdef DEBUG |
実行例の一部 |
41 67 34 0 69 24 78 58 62 64 5 45 81
27 61 91 95 42 27 36 =>41 67 34 0 69 24 78 58 62 64 5 45 81 27 61 91 95 42 27 36 =>36 27 34 0 27 24 5 41 62 64 58 45 81 78 61 91 95 42 69 67 =>36 27 34 0 27 24 5 => 5 27 34 0 27 24 36 => 5 27 34 0 27 24 => 0 5 34 27 27 24 =>34 27 27 24 =>24 27 27 34 =>24 27 27 =>24 27 27 =>62 64 58 45 81 78 61 91 95 42 69 67 =>42 61 58 45 62 78 81 91 95 64 69 67 =>42 61 58 45 =>42 61 58 45 =>61 58 45 =>45 58 61 =>78 81 91 95 64 69 67 =>67 69 64 78 95 91 81 =>67 69 64 =>64 67 69 =>95 91 81 =>81 91 95 0 5 24 27 27 34 36 41 42 45 58 61 62 64 67 69 78 81 91 95 good |
もし,すでにソート済のデータをクイックソートで並び変えると,再帰呼び出し回数が非常に多くなる可能性があるため,
並べ替え範囲の先頭の値をピボットにするのは望ましくない。
そこで,ソート済の場合でも,ソート前の場合でも均一な分割をするための工夫として,
配列の中央位置の値をピボットに使う場合は,次のように一箇所変更すればよい
List 3
クイックソートの関数(ピボットに中央位置の値を使う) |
void QuickSort(int *x, int
num) |
4.C標準ライブラリのクイックソート(1) | |
クイックソートを手軽に使えるようにするため,C言語では標準ライブラリとして提供されている。
昇順にソートしようとする時も降順にソートしようとする時も同じクイックソート関数を使うことができるように,2つのデータの比較方法を与える関数を別に定義し,標準ライブラリのクイックソート関数に,この比較関数を使ってほしいとお願いするプログラミングとなる。
クイックソート関数を使う場合は,「stdlib.h」をインクルードする。
qsort()はstdlib.h中で次のようにプロトタイプ宣言されている
void qsort(void *base, size_t num, size_t size, int (*compare)(const void*, const void*)) |
ここで「int (*compare)(const void*, const
void*)」は「2つのデータの比較方法を与える」関数へのポインタであり,その返す値はint型である。
引数は2つあり,比較すべき2つのデータへのポインタとなっている。
なおこの比較関数の名前はcompareである必要はなく,なんでもよい。
List.4 標準ライブラリのクイックソート関数を使うプログラム例 |
#include "stdio.h" #define SIZE 20 void printdata(int x[], int
num) /*小さい順ソート済みデータのチェック /*比較関数 昇順ソートの場合 */ |
実行例 |
41 67 34 0 69 24 78 58 62 64 5 45 81
27 61 91 95 42 27 36 0 5 24 27 27 34 36 41 42 45 58 61 62 64 67 69 78 81 91 95 good |
5.C標準ライブラリのクイックソート(2) | |
C標準ライブラリのクイックソートは比較関数仕様が柔軟にできているため(扱いにくいが),構造体のソートも比較関数をスイッチするだけでできる。
実はこのプログラム例は紹介のために示しているが,だめなプログラムである。このプログラムを理解してから次の「6」を見なさい。
List.5 標準ライブラリのクイックソート関数で構造体を扱うプログラム例 |
#include <stdio.h> typedef struct { island_t Island[]={ void printData(island_t data[], int num, char
*title) int compfuncName(const void *a, const void
*b) int compfuncPopulation(const void *a, const void
*b) int compfuncArea(const void *a, const void
*b) int main() |
実行例 |
初期状態 名前順 人口順 面積順 |
6.C標準ライブラリのクイックソート(3) | |
「5」で示したプログラムで扱っているレコードサイズを考えてみよう。レコードとは,1つの島の持っているすべての情報のことであり,「名前」「人口」「面積」で構成されている。次のプログラムの冒頭で示されているように1つのレコードは48バイトで構成されている。(不思議*1)「5」で示したプログラムでは,レコードを並び替えるたびに48バイトを移動していたことになる。これでは,いくらクイックソートとはいえ,ソート速度は遅くなる。そこで,レコード番号の配列を作り,この番号の配列(int型を使ってもせいぜい4バイト)をソートするだけにすると,ソート速度を向上させることができる。例えば1レコードが数百バイトもあるようであれば,この改良は飛躍的な速度向上をもたらす。
不思議*1 このレコードの内部には,文字列32バイト,int型変数4バイト,double型変数8バイトしかなく,合計44バイトであった,しかし,コンパイラは,隙間に4バイトを使い,CPUが扱いやすい8の倍数でレコードを構成している。
List.6 標準ライブラリのクイックソート関数で構造体を扱い,インデックステーブルのみをソートするプログラム例 |
#include <stdio.h> typedef struct { island_t Island[]={ int *index;
/*ソートしたい配列の要素番号列を保存し,実際にソートされる配列*/ void printData(int index[], island_t data[], int
num, char *title) int compfuncName(const void *a, const void
*b) int compfuncPopulation(const void *a, const void
*b) int compfuncArea(const void *a, const void
*b) int main() |
実行例 |
size of (int)= 4 名前順 人口順 面積順 |