第2回、「ソート」「ソートの動作確認」

課題の説明

今回の課題は、1と2が必須、3は余力あらば、という出題形式でした。

  1. データ例(num_data.txt)を読みとり、ソートして返すプログラムを作成せよ
  2. プログラムの実行にはリダイレクションを用いよ
  3. プログラムの処理結果が正しいか検証する方法を考え、可能であれば実現せよ

プログラムが a.out の場合の実行例
% ./a.out < num_data.txt
1
4
7
19
28 

簡単ですね。
今回の課題のねらいは、ソートプログラムの作り方...、ではなくてテストについてです。なんというか、テストはとても大事です。

テストについて

プログラムのテストを作る目的として「そのプログラムの動作保証をする」ということと、どんなテストをするかを定義する過程で、「そのプログラムが何をアウトプットするべきかを考える」ということがあります。

今回のテストについての考え方は、開発手法の一つである XP(エクストリーム・プログラミング)の考えにのっとったものですので、興味のある人は調べてみて下さい。

アウトプットを思い描く

今回の課題でいえば、出力は

1
4
7
19
28 

となるべきことは提示されています。つまり、課題中では「ソートして返す」としか言っていませんが、具体例を見ることで、

  • 昇順にソート
  • 各行毎にソートした数値を出力

という風に、出力について曖昧な箇所がなくなります。

このように、これからどんな実装をするのか、どんなアルゴリズムを使うべきか、を考える上でも、最終的なアウトプットを考えることは重要なのです。


動作確認

最終的なアウトプットの予定あれば、それと実際のアウトプットを比べる仕組みを作ることは、そんなに難しくありません。実際に、簡単なテストを定義してみましょう。使うのは、Linux のコマンドである sort, diff で、プログラムのコンパイルとテストを実行するために make を用います。

で、今回の課題を提出した結果、

3ですが、「ソートが完了した配列について、隣の数との大小を比較して
きちんと昇順になっているか確かめる」くらいしか思いつきませんでした。

という意見を貰いました。まぁそんな感じですが、今回については、「信頼できる物と比べる」という手法を取りましょう。例えるならば、「私の身長は 170cm だ!」と言い張る人が正しいかをテストするには、信頼できる長さの指標(メジャー)とか、を持ってきて実際に比べてみればよいのです。つまり、確実に正しい結果と、自分が正しい思っている結果を比べればよいのです。
今回は sort を使ってソートした結果と、プログラムを実行してソートした結果を比較します。

つまり、

% sort -n num_data.txt
1
4
7
19
28 

という風にソートできることを利用し、この結果と自分のプログラムのアウトプットを比べる仕組みを作ります。これには、シェルコマンドの diff を用います。

% sort -n num_data.txt > expected.txt
% ./a.out < num_data.txt > actual.txt
% diff expected.txt actual.txt

この手順を実行すると、sort と a.out の結果が一致していればなにも表示されず、一致していなければ diff の結果が出力されます。

このようなコマンドをコンパイル毎に手入力で実行するのは面倒です。というか、無駄です。コンパイルとテストの実行を簡単に行うための Makefile を以下に示します。

# Makefile for sort
# Satofumi KAMIMURA
# $Id$

# Compile options
CXXFLAGS = -O0 -Wall -Werror
LDFLAGS =
LDLIBS =

# Target
TARGET = sort

all : ${TARGET}

clean :
	${RM} *.o ${TARGET} actual.txt expected.txt

test : all
	@sort -n num_data.txt > expected.txt
	@./sort < num_data.txt > actual.txt
	@diff expected.txt actual.txt && echo "O.K." || echo "!!! fail"

.PHONY : all clean
######################################################################j

この例では、

% make

と入力すると sort.cpp をコンパイルして sort を生成し、

% make test

と入力すると、先ほどのテストを行い、テストが成功したかどうかを出力します。ちなみに「make test」はコンパイルするようにも調整してあるので、毎回「make test」と入力すると、プログラムは必要に応じてコンパイルされます。

テストとテストを実行する仕組み、これを確実に実現して、いつでも実行できるようにするのが大事です。なれてきたら、「xUnit」と呼ばれるテストの枠組みなどを勉強するとよいでしょう。

作ったプログラム

今回の課題はテストについて意識してもらうことなので、プログラムはおまけです。私が作ったプログラムは、C++ ですが、こんな感じです。C++ の標準ライブラリである STL を利用して、さくっと作りました。

/*
  STL によるソートプログラム
  Satofumi KAMIMURA
  $Id$
*/

#include <vector>
#include <iostream>
#include <iterator>
#include <algorithm>

using namespace std;


int main(int argc, char *argv[]) {
  // 標準入力からのデータ読み出し
  vector<int> data((istream_iterator<int>(cin)), istream_iterator<int>());

  // ソート
  sort(data.begin(), data.end());

  // 標準出力へのデータ出力
  for (std::vector<int>::iterator it = data.begin(); it != data.end(); ++it) {
    cout << *it << endl;
  }

  return 0;
} 

プログラムが数行しかありませんが、プログラムが短いことで、バグの混入する可能性も減るので、いい感じです。

他の実装の例としては、ソートするデータに重複がないと分かっていれば、ビット列にソートの数値があったかどうかを格納していく、という荒技もあります。おそらくトータルで必要なバイト数は減るはずです。

提出された課題と、課題へのコメント

C言語編

#include <stdio.h>
#define NUM 5 /*ソートする整数の個数*/

/*ソートプログラム*/
int main(void)
{
  int box[NUM];
  int i, j, tmp;

  for(i=0; i<NUM; i++) {
    scanf("%d", &box[i]);
  }

  for(i=0; i<NUM-1; i++) {
    for(j=i+1; j<NUM; j++) {
      if(box[j] < box[i]) {
        tmp = box[j];
        box[j] = box[i];
        box[i] = tmp;
      }
    }
  }

  for(i=0; i<NUM; i++) {
    printf("%d\n", box[i]);
  }
  return 0;
}
↑ぶっちゃけ、「バブルソートで読み込む数は 5 限定かよ!」とか思いました。数値を読み込むあたりについては、以下のようにするとよいと思います。

数値を読み込むサンプルプログラム
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
  enum { DATA_SIZE = 1024 };
  int data[DATA_SIZE];
  int size;
  int i;
  int ret;

  /* 標準入力からの読み出し */
  for (size = 0; size < DATA_SIZE;  ++size) {
    int ret = 0;
    if ((ret = scanf("%d", &data[size])) == EOF) {
      break;
    }
    if (ret <= 0) {             /* 読み込みに失敗した場合 */
      --size;
    }
  }
  if (i == DATA_SIZE) {
    printf("data is too many! (over %d)\n", DATA_SIZE);
    exit(1);
  }

  /* ソート処理 */ 

あと、「#define NUM 5」の後ろに全角の空白があってコンパイルに失敗しました。メールに添付する直前にコメントを付けたのでしょうか...。できれば、コメントを先に書いてプログラムすべきです。何を作るのか意識してプログラミングすることになりますから。

あと、emacs で、全角の空白はカラー表示するための設定は、こんな感じです。(.emacs より設定を抜粋) この設定を .emacs に追記することで、全角スペースや空白文字だけの行がカラーで強調表示されます。


//読み込んだ数字をソートして表中出力
#include<stdio.h>
#include<stdlib.h>

//qsort用比較関数
int cmp(int *n, int *m)
{
  if(*n>*m)
    return 1;
  if(*n<*m)
    return -1;
  else
    return 0;
}

int main(void)
{
  int i=0,sort[5],c;
  while(i!=5){
    scanf("%d",&c);
    sort[i]=c;
    i++;
  }

  qsort(sort,5,sizeof(int),(int(*)(const void*, const void*))cmp);

  for(i=0;i<5;i++)
    printf("%d\n",sort[i]);

  /*
    ここから正確性の検証
    配列のすべての値において、その隣の値の大小関係は同じかどうかの検証
  */
  i=1;
  while(cmp(&sort[i],&sort[i-1])>=0){
    i++;
    if(i==5){//大小関係の比較が配列の最後まで終われば
      printf("Sorting ended accurately\n");
      return 0;
    }
  }

  printf("Sorting failed\n");
  for(i=0;i<5;i++)
    printf("%d\n",sort[i]);

  return 0;
}

qsort() 使っているのは、いい感じです。あと、main() は本の目次のように、何が書いてあるかすぐわかるような構成になっていると、さらによいです。

main() の中に、何をするか分かるような関数を定義する。
int main(int argc, char *argv[]) {
  enum { DataSize = 1024 };
  int data[DataSize];

  inputData(data, DataSize);
  sort(data, DataSize);
  outputData(data, DataSize);
}

こんな感じなら、コメントがなくても、何をしているかの推測はつきます。でも、コメントはあった方がよいです。1行でいいので。

// 標準入力からの数値を昇順にソートし、数値毎に改行して標準出力に出力する

あと、ソート正当性の検証部分については、やり方はともかく明らかに for ループを使うべき箇所です。あと、できれば関数化。

while ループを for ループに変更し、エラー処理をループ内に移動
  /*
    ここから正確性の検証
    配列のすべての値において、その隣の値の大小関係は同じかどうかの検証
  */
  for (i=1;i<5;++i) {
    if(cmp(&sort[i],&sort[i-1])<0) {
      printf("Sorting failed\n");
      for(i=0;i<5;i++)
        printf("%d\n",sort[i]);
      exit(1);
    }
  }

  //大小関係の比較が配列の最後まで終われば
  printf("Sorting ended accurately\n");
  return 0;
} 

ただ、この処理だとソート結果が 1, 1, 1, 1, 1 みたいなのでもテストを通過します。あと、どうせなら accurately でなく、correctly かと。


/*五個のデータのソートプログラム。リダイレクションver*/
#include <stdio.h>
#include <stdlib.h>


int cmp(const void *x, const void *y);

int main(){
  int a[5],i;
  for(i=0;i<5;i++)
    {
      scanf("%2d",&a[i]);/*データの入力*/

    }

   qsort(a,5,sizeof(int),cmp);/*クイックソート関数*/
   for(i=0;i<5;i++){
     printf("%2d\n",a[i]);
   }

   return 0;
}


int cmp(const void *x, const void *y)/*クイックソート関数の比較関数:昇順*/
{
  return *(int*)x - *(int*)y;
} 

cmp() が減算で組んであるあたり、C++ なら、演算子オーバーロードがあるので、< で作りましょう、とかいうコメントが有効ですが...、まぁ、減算でもよいです。 でも、最低限、数値の 5 はマクロ化しましょう。


//入力された数列をソートするプログラム

#include<stdio.h>

int main(void){

  int n=5; //ソートする数字の数
  int num[n];
  int i,j;
  int min;

  for(i=0;i<n;i++) //数値を配列に入れる
    scanf("%d",&num[i]);

  for(i=0;i<n;i++){ //ソート
    min=num[i];
    for(j=i;j<n;j++){
      if(min>num[j]){
        min=num[j];
        num[j]=num[i];
        num[i]=min;
      }
    }
    printf("%d\n",num[i]);
  }
} 

できれば、ソート部と出力部は分けましょう。
int n=5; にコメントがあるのは、よいです。できれば num も重要なのでコメントをつけましょう。


C++言語編


#include <iostream>
#include <vector>

using namespace std;

typedef vector<int> IntVector;

void
QuickSort(IntVector& arr, int first, int last)
{
        int i, j;
        int x;

        x = arr[(first + last) / 2];
        i = first;
        j = last;

        for(;;){
                int tmp;

                while(arr[i] < x) i++;
                while(x < arr[j]) j--;

                if(i >= j) break;

                tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;

                i++;
                j--;
        }

        if(first < i - 1)
                QuickSort(arr, first, i - 1);
        if(j + 1 < last)
                QuickSort(arr, j + 1, last);
}

int
main()
{
        IntVector arr;

        // read cin
        for(;;){
                int num;
                cin >> num;
                if(cin.eof()) break;
                arr.push_back(num);
        }

        // sort
        QuickSort(arr, 0, arr.size() - 1);

        // display result
        int arrSize = arr.size();
        for(int i = 0; i < arrSize; i++){
                cout << arr[i] << endl;
        }

        // check array sorted
        arrSize = arr.size();
        int tmp = -INT_MAX;
        for(int i = 0; i < arrSize; i++){
                if(tmp > arr[i]){
                        cerr << "sort failed" << endl;
                        break;
                }
                else{
                        tmp = arr[i];
                        if( i == (arrSize - 1) )
                                cerr << "sort succeeded" << endl;
                }
        }

        return 0;
} 

どうせなら、失敗時の main() の戻り値は return 1 が通例です。あとは、arrSize を取得し直す必要がないのと、INT_MIN と INT_MAX は値が違います。それに、C++ 風に書くなら #include <limits> を使うべきです。

あと、ソートが成功とのメッセージを表示するあたりは、チェックに失敗したらフラグがたつように変更すると、すっきりするかも。

        // check array sorted
        bool isCorrect = true;
        int tmp = numeric_limits<int>::min();
        for(int i = 0; i < arrSize; ++i){
                if(tmp > arr[i]){
                        cerr << "sort failed" << endl;
                        isCorrect = false;
                        break;
                }
                tmp = arr[i];
        }
        if(isCorrect) {
                cerr << "sort succeeded" << endl;

        return 0;
}

こんな感じかな?

第3回、「ソート時間の測定」 へ」

Generated on Mon Apr 13 22:52:06 2009 by  doxygen 1.5.7.1