【C言語】中央値を求める方法

中央値を求める方法の解説ページアイキャッチ

このページでは、C言語で「中央値」を求める方法について解説していきます。

まず前提として、C言語の標準ライブラリ関数には中央値そのものを求めてくれるような関数は存在しません。

なので、中央値を求めたい場合、自身で中央値を求めるようプログラミングする必要があります(もちろん他の人が作った中央値を求める関数があるのであれば、それを利用しても良いです)。

また、中央値を求める方法といってもたくさんありますが、このページで求める方法は「考え方は簡単だけど遅い」方法になります。なので、とりあえず中央値を求めたい、速さは気にしない、という方にオススメの方法になります。

中央値の求め方

中央値の求め方は、中央値がどんなものであるか?を考えればすぐに分かると思います。

あるデータの集合における中央値とは、その集合の中に存在するデータを小さい順 or 大きい順に並べたときに、中央の位置に存在するデータのことを言います。

中央値の求め方の考え方

もうちょっとC言語っぽく考えれば、中央値とは配列に格納されている値を小さい順 or 大きい順に並び替えたときに、配列の中央の位置に存在する値のことと言えます。

配列に格納されている値から中央値を求める考え方

配列に格納されている値の個数が奇数の場合は、中央の位置に存在する値が1つのみなので、この値そのものが中央値となります。

より具体的には、並び替え後の配列を values とし、さらに配列に格納されている値の個数を num とすれば、values[num / 2] が中央値となります。

データの個数が奇数個の場合の中央値の求め方

MEMO

num は整数型の変数(or 整数型の値)を想定しています

num が整数型の変数であれば、num / 2 の計算結果の小数点以下は切り捨てられ、上手く配列の中央の位置を求めることができます

その一方で、配列に格納されている値の個数が偶数の場合は、中央の位置に存在する値が2つとなります。この場合は、この2つの値の平均値が中央値となります。

より具体的には、先ほどと同様に並び替え後の配列を values とし、さらに配列に格納されている値の数を num とすれば、values[num / 2 - 1]values[num/2] の平均が中央値となります。

データの個数が偶数個の場合の中央値の求め方

以上をまとめると、配列に格納された値の中央値を求めるためには、下記の3つの処理が必要であることになります。

  • 配列の値を並び替える
  • データの個数が奇数か偶数かを判断する
  • 奇数か偶数かの判断結果に基づいて中央値を求める

一番大変なのは1つ目の「配列の値を並び替える」だと思います。ただ、C言語の標準ライブラリには「配列の値を並び替える関数」として qsort 関数が用意されていますので、これを利用すれば一発で並び替えが完了します。

qsort 関数の使い方については下記ページで詳しくまとめていますので、詳しく知りたい方はこのページを参照していただければと思います。

qsort関数の使い方の解説ページアイキャッチ【C言語】qsort関数の使い方

ただ、qsort 関数はちょっと使い方が難しいので、qsort を使いたく無いような場合は、並び替え処理自体も自身で実装してしまっても良いと思います。

例えば簡単に実装したいなら下記ページで紹介している選択ソートがオススメですし、

選択ソート解説ページのアイキャッチ選択ソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

並び替えを行う時間を少なくしたいのであれば、下記ページで紹介しているクイックソートがオススメです。

クイックソート解説ページのアイキャッチクイックソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

以降で紹介する中央値を求める関数では配列の並び替えに qsort 関数を利用しますが、並び替え自体もプログラミングしたい場合は、上記のようなページを参考にしながら実装してみていただければと思います。

中央値を求める関数

下記ソースコードの calcMedian が中央値を求める関数になります。

calcMedian 関数は、第1引数の配列 values に格納された値から算出した中央値を返却する関数になります(値の個数は第2引数 num 個)。

中央値を求める対象となるデータに合わせて配列 values に格納する値を変更すれば、おそらく上手く中央値を求めることができると思います。ただし、後述で解説するように、calcMedian には注意点が2つあります。

中央値を求める関数
#include <stdio.h> /* printf */
#include <stdlib.h> /* qsort */

int compare(const void *pa, const void *pb) {
    int a = *(int*)pa;
    int b = *(int*)pb;

    if (a > b) {
        return 1;
    } else if (a < b) {
        return -1;
    } else {
        return 0;
    }
}

/*********************************************
 *  中央値を計算する関数
 *  values:中央値を計算したい値を格納した配列
 *  num:valuesに格納された値の個数
 *********************************************/
double calcMedian(int values[], unsigned int num) {
    double median;

    /* 配列valuesの値の並び替え */
    qsort(values, num, sizeof(int), compare);

    /* 値の数が偶数個であるかどうかを判断 */
    if (num % 2 == 0) {
        /* 値の数が偶数個の場合は中央の2つの値の平均を中央値とする */
        median = (double)(values[num / 2] + values[num / 2 - 1]) / 2;
    } else {
        /* 値の数が奇数個の場合は中央の値そのものを中央値とする */
        median = values[num / 2];
    }
    return median;
}

int main(void) {
    int values[] = {
        11, 195, 200, 500, 1, 6, 8, 7, 4, 9, 100, 500, 149, 283
    };

    unsigned int num = sizeof(values) / sizeof(values[0]);
    double median = calcMedian(values, num);

    printf("MEDIAN = %f\n", median);

    return 0;
}

calcMedian で行っているのは、前述でも紹介した中央値を求める手順の下記の3つの処理になります。

  • 配列の値を並び替える
  • データの個数が奇数か偶数かを判定する
  • 奇数か偶数かの判定結果に基づいて中央値を求める

1. の処理は qsort 関数により行っています。上記のように qsort 関数を実行すれば、配列 values の値が小さい順(昇順)に並び替えられます。

qsort 関数呼び出し部分の意味が分からない方は、前述でも紹介した下記ページで解説していますのでこちらをご参照いただければと思います。

qsort関数の使い方の解説ページアイキャッチ【C言語】qsort関数の使い方

2. の処理は num % 2 == 0 が成立するかどうかで判定しています。

3. の処理は下記部分で行っています。

中央値を求める
if (num % 2 == 0) {
    /* 値の数が偶数個の場合は中央の2つの値の平均を中央値とする */
    median = (double)(values[num / 2] + values[num / 2 - 1]) / 2;
} else {
    /* 値の数が奇数個の場合は中央の値そのものを中央値とする */
    median = values[num / 2];
}

スポンサーリンク

中央値を求める際の注意点

前述の通り、calcMedian 関数には2つの注意点があります。

  • オーバーフローが発生しうる
  • 配列の値の順番が変わってしまう

これらは上記の calcMedian 関数だけでなく、中央値を求める際の一般的な注意点となります。

オーバーフローが発生しうる

1点目の注意点はオーバーフローが発生しうる点です。

オーバーフローが発生する可能性があるのは下記部分の足し算です。

オーバーフロー発生箇所
median = (double)(values[num / 2] + values[num / 2 - 1]) / 2;

valuesint 型の配列ですので、values[num / 2]values[num / 2 - 1] を足し合わせた結果が int 型で扱える最大値よりも大きくなる or 最小値よりも小さくなる場合はオーバーフローが発生することになります(int 型変数同士の足し算結果は int 型として扱われる)。

そうなると、求められる中央値は意図しない値になります。

例えば int 型の最大値を 2147483647 とすれば、values の値を下記のように変更した場合、求められる中央値は -1.5 になります(期待値は 2147483646.5 のはず)。

オーバーフローが発生する場合のvalues
int values[] = {
    2147483646, 2147483647
};

1つの解決策は、int 型よりもサイズが大きな型にキャストしてから足し算することです。私の環境では int 型よりもサイズが大きな型として long long 型が扱えるので、下記のように変更すればオーバーフローが防げます。

オーバーフローを防ぐ1
median = (double)((long long)values[num / 2] + (long long)values[num / 2 - 1]) / 2;

values は int 型の配列なので、おそらく下記でもうまく計算できるはずです(int 型の最大値や最小値を double 型の変数に格納しても誤差は出ない、はず)。

オーバーフローを防ぐ2
median = ((double)values[num / 2] + (double)values[num / 2 - 1]) / 2;

あとはオーバーフローしないように式を変形する手も考えられます。一応下記のような average 関数を考えてみましたが、動作確認はあまりできてないです(多分うまく動いてくれると思いますが…)。

要は (x - y)(x + y) 実行時にオーバーフローが発生しないように条件判断や式の変形を行なっています。

オーバーフローを防ぐ3
double average(int x, int y) {
    if ((x >= 0 && y >= 0) || (x < 0 && y < 0)) {
        return x - (double)(x - y) / 2;
    } else {
        return (double)(x + y) / 2;
    }
}

もちろんオーバーフローするほど扱う値が大きくなることは少ないかもしれませんが、こういった現象が起こりうることは頭に入れておいた方が良いですし、扱う値の範囲も考慮しながら実装する方が良いとは思います。

配列の値の順番が変わってしまう

注意点の2点目は、配列の値の順番が中央値を求める前後で変わってしまう点です。

もちろん配列の値の順番が変わってしまっても問題ない場合は気にしなくても良い注意点となります。

もし配列の値の順番が変わってしまうと困る場合は、下記のような対応が必要です。

  • 配列の退避を行なってから calcMedian 関数を呼び出すようにしてもらう
  • calcMedian 関数の中で配列の退避を行う

前者を行う場合、calcMedian 関数を呼び出す main 関数は下記のように変更する必要があります。memcpy を行なっているので string.h をインクルードする必要があります。

配列の値の順番が変わらないようにする
int main(void) {
    int values[] = {
        11, 195, 200, 500, 1, 6, 8, 7, 4, 9, 100, 500, 149, 283
    };
    unsigned int num;
    int *copy;
    double median;

    num = sizeof(values) / sizeof(values[0]);

    /* コピー用の配列に必要なメモリを確保 */
    copy = malloc(num * sizeof(int));
    if (copy == NULL) {
        printf("malloc error\n");
        return -1;
    }
    /* コピー用の配列にvaluesを丸ごとコピー */
    memcpy(copy, values, num * sizeof(int));
    
    /* コピー用の配列から中央値を算出 */
    median = calcMedian(copy, num);

    /* 不要になったのでコピー用の配列のメモリを解放 */
    free(copy);

    printf("MEDIAN = %f\n", median);

    return 0;
}

配列のサイズが事前に分かっているのであれば、わざわざ malloc 関数を利用しなくても int copy[SIZE] などのように変数宣言時に配列を作成してしまっても良いです。

まとめ

このページでは、C言語で中央値を求める方法について解説しました!

中央値を求める手順は下記のようになります。

  • 配列の値を並び替える
  • データの個数が奇数か偶数かを判断する
  • 奇数か偶数かの判断結果に基づいて中央値を求める

配列の値の並び替えさえできてしまえば中央値は簡単に求められると思います。

ただし、データの個数が偶数の場合は平均値を求める必要があります。配列内の値として大きなものを扱う場合、オーバーフローが発生しないように気をつけてください。

また、中央値を求める際に配列の値が並び替えられてしまうため、配列の値が並び替えられると困るような場合は、事前に配列の退避を行なってから中央値を求めるようにしましょう!