このページでは、C言語で「中央値」を求める方法について解説していきます。
まず前提として、C言語の標準ライブラリ関数には中央値そのものを求めてくれるような関数は存在しません。
なので、中央値を求めたい場合、自身で中央値を求めるようプログラミングする必要があります(もちろん他の人が作った中央値を求める関数があるのであれば、それを利用しても良いです)。
また、中央値を求める方法といってもたくさんありますが、このページで求める方法は「考え方は簡単だけど遅い」方法になります。なので、とりあえず中央値を求めたい、速さは気にしない、という方にオススメの方法になります。
Contents
中央値の求め方
中央値の求め方は、中央値がどんなものであるか?を考えればすぐに分かると思います。
あるデータの集合における中央値とは、その集合の中に存在するデータを小さい順 or 大きい順に並べたときに、中央の位置に存在するデータのことを言います。
もうちょっとC言語っぽく考えれば、中央値とは配列に格納されている値を小さい順 or 大きい順に並び替えたときに、配列の中央の位置に存在する値のことと言えます。
配列に格納されている値の個数が奇数の場合は、中央の位置に存在する値が1つのみなので、この値そのものが中央値となります。
より具体的には、並び替え後の配列を values
とし、さらに配列に格納されている値の個数を num
とすれば、values[num / 2]
が中央値となります。
num
は整数型の変数(or 整数型の値)を想定しています
num
が整数型の変数であれば、num / 2
の計算結果の小数点以下は切り捨てられ、上手く配列の中央の位置を求めることができます
その一方で、配列に格納されている値の個数が偶数の場合は、中央の位置に存在する値が2つとなります。この場合は、この2つの値の平均値が中央値となります。
より具体的には、先ほどと同様に並び替え後の配列を values
とし、さらに配列に格納されている値の数を num
とすれば、values[num / 2 - 1]
と values[num/2]
の平均が中央値となります。
以上をまとめると、配列に格納された値の中央値を求めるためには、下記の3つの処理が必要であることになります。
- 配列の値を並び替える
- データの個数が奇数か偶数かを判断する
- 奇数か偶数かの判断結果に基づいて中央値を求める
一番大変なのは1つ目の「配列の値を並び替える」だと思います。ただ、C言語の標準ライブラリには「配列の値を並び替える関数」として qsort
関数が用意されていますので、これを利用すれば一発で並び替えが完了します。
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
関数呼び出し部分の意味が分からない方は、前述でも紹介した下記ページで解説していますのでこちらをご参照いただければと思います。
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;
values
は int
型の配列ですので、values[num / 2]
と values[num / 2 - 1]
を足し合わせた結果が int
型で扱える最大値よりも大きくなる or 最小値よりも小さくなる場合はオーバーフローが発生することになります(int
型変数同士の足し算結果は int
型として扱われる)。
そうなると、求められる中央値は意図しない値になります。
例えば int
型の最大値を 2147483647
とすれば、values
の値を下記のように変更した場合、求められる中央値は -1.5
になります(期待値は 2147483646.5
のはず)。
int values[] = {
2147483646, 2147483647
};
1つの解決策は、int
型よりもサイズが大きな型にキャストしてから足し算することです。私の環境では int
型よりもサイズが大きな型として long long
型が扱えるので、下記のように変更すればオーバーフローが防げます。
median = (double)((long long)values[num / 2] + (long long)values[num / 2 - 1]) / 2;
values
は int
型の配列なので、おそらく下記でもうまく計算できるはずです(int
型の最大値や最小値を double
型の変数に格納しても誤差は出ない、はず)。
median = ((double)values[num / 2] + (double)values[num / 2 - 1]) / 2;
あとはオーバーフローしないように式を変形する手も考えられます。一応下記のような average
関数を考えてみましたが、動作確認はあまりできてないです(多分うまく動いてくれると思いますが…)。
要は (x - y)
や (x + y)
実行時にオーバーフローが発生しないように条件判断や式の変形を行なっています。
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言語で中央値を求める方法について解説しました!
中央値を求める手順は下記のようになります。
- 配列の値を並び替える
- データの個数が奇数か偶数かを判断する
- 奇数か偶数かの判断結果に基づいて中央値を求める
配列の値の並び替えさえできてしまえば中央値は簡単に求められると思います。
ただし、データの個数が偶数の場合は平均値を求める必要があります。配列内の値として大きなものを扱う場合、オーバーフローが発生しないように気をつけてください。
また、中央値を求める際に配列の値が並び替えられてしまうため、配列の値が並び替えられると困るような場合は、事前に配列の退避を行なってから中央値を求めるようにしましょう!
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/