N.Y.Cityのまちかど

partial_sort_for_2darray

二次元配列の部分行ソート

先日、仕事で組んでいるCプログラムの中で、ちょっと面白いコードを組んだので、メモを兼ねて残します。C言語におけるポインタの振る舞いを良く知っていないと、わけのわからないコードだと思います。

やりたいこと

年・月・データを1組のデータとし、これを1行として複数のデータをまとめた2次元配列を考えます。データはすべてint形式です。

【datatable】 [0] [1] [2]
[0] a a データa
[1] b b データb
[2] c c データc
[3] d d データd
[4] e e データe
[5] f f データf
[6] g g データg
[7] h h データh
[8] i i データi
[9] j j データj

この表のうち、一部の行だけを月日別にソートして取り出したいとします(上の例では太字で示した5~9行目のみを対象にソートを実行するものとする)。

検討した解決方法

ソートしたい部分だけ別配列にコピー

ソートしたい部分だけ配列のコピーを作って、それに対してソートアルゴリズムを考えるのが普通の実装だと思います。上例は説明の簡略化のためにわずか3列の配列にしていますが、実際に私がソートしようとした配列は、もっと列数の多い配列だったので、ソートのためだけにループを回して配列のコピーを行うのもちょっとバカバカしい感じでした。

memcpyを使えば早いという声があるかもしれませんが、memcpyで配列のコピーが正しくできる保証はありません。また仮にmemcpyが正しく配列コピーをしてくれるとして、今回はたまたま上例と同じようにソート対象行が5~9行目までと連続していますが、これが飛び飛びの行になると結局余計な行の掃除作業が必要になってmemcpyの恩恵は薄れます。

連結リストを作成してソート

配列のコピーを作るという意味では上の解決方法と変わらないのですが、ポインタを使って連結リストを作成し、連結リスト形式で配列のデータを取り出しておくと、配列に比べてもう少しフレキシブルな扱いができます。

ただ、いちいち配列リスト用の型を宣言しなければならず、ただソートがしたいだけにしては、手間がかかりすぎるように思い、この方法もやめました。

採用した解決方法

そこで、配列からソートしたい行の丸々コピーを作るのではなく、「ソートしたい行の先頭アドレス(ポインタ)だけを取り出したインデックス行列」を作り、このポインタを足掛かりにして値を参照しながら、インデックス行列だけをソートすればよいと考えました。

ソース



#include<stdio.h>
#include<stdlib.h>

#define ARYSIZE(x) (sizeof(x)/sizeof(x[0]))


int parysort( const void * a , const void * b ) {                       //【D】
    //引数はvoid*型と規定されているのでint*型にcastする
    //得られたポインタはdatatable配列の行先頭を現す。
    //サフィックスをつけて比較値を取出す
    //比較値は年+月を結合して10進数値としたもの

    
    int *a_pointer = *(int **)a;                                        //【E】
    int *b_pointer = *(int **)b;
    
    //年に100をかけて月を足す(例 2015年3月→201503)
    long int a_value = a_pointer[0]*100 + a_pointer[1];                 //【F】
    long int b_value = b_pointer[0]*100 + b_pointer[1];
    
    //ソート順序は昇順
    if( a_value < b_value ) {                                           //【G】
      return -1;
    }
    else
    if( a_value == b_value ) {
      return 0;
    }
    return 1;
}


int main(){

    int datatable[10][3];                                               //【A】
    //          Year    Month   data
    //          [0]     [1]     [2]
    //  [00]    年a     月a     データa
    //  [01]    年b     月b     データb
    //  [02]    年c     月c     データc
    //  [03]    年d     月d     データd
    //  [04]    年e     月e     データe
    //  [05]    年f     月f     データf
    //  [06]    年g     月g     データg
    //  [07]    年h     月h     データh
    //  [08]    年i     月i     データi
    //  [09]    年j     月j     データj

    



    //dataindex配列を作る
    int* dataindex[5];                                                  //【B】
    for(i=0;i<5;i++){
        dataindex[i] = datatable[i+5];
    }
    
    
    //dataindex配列をソートする。
    
    qsort(dataindex,ARYSIZE(dataindex),sizeof(dataindex[0]),parysort);  //【C】
    
    
    //順番に値を取出して出力する。
    int i;
    for(i=0;i<5;i++){ //【H】
        int *col_pt = dataindex[i];
        printf("%d,%d,%d,\r\n"col_pt[0],col_pt[1],col_pt[2]);
    }
    
    
}

解説

下記【A】~【G】はソースコード中のコメントに対応

【A】

説明の必要もないでしょうが、元データ配列datatableの宣言です。具体的なデータを収める処理は割愛しています。

【B】

インデックスとなるdataindex配列の宣言・値格納です。

datatableがint型の配列のため、dataindexの型はintのポインタ(int*)です。

今回は、datatable[5][ ]~datatable[9][ ]という連続した範囲を部分ソートしたいため、for文を利用してdataindexの生成を行っています。C言語の2次元配列は、「1次元配列の1次元配列」として作られるため、2次元配列の2つ目のサフィックスを省略する、例えば
datatable[5]
とすれば、5行目のデータ(を格納した1次元配列)を意味します。また、C言語においては「宣言された配列名は配列の先頭ポインタ」を意味するため、datatable[5]は「datatable[5]の先頭ポインタ」、言い換えれば「datatable[5][0]のポインタ=&(datatable[5][0])」を意味します。

【C】

ソートアルゴリズムはいちいち作りたくなかったので、stdlib.hのqsort関数を利用したクイックソートとしています。もちろん必要であれば自分で好きなアルゴリズムを実装して良いです。

ARYSIZEは配列の大きさを取得する簡単なマクロです。ソースの拡張性を高めるのにとても便利なのですが、あちこちに出てくるとやや長くて邪魔なので私は良くマクロ化して使います。

最後の引数には、ソートの根拠となる比較関数を渡します。本ソースにおけるparysortは、【D】で宣言しているparysort関数のことです。(関数名をそのまま書いているので、C言語の内部処理としては関数ポインタとして処理されている)

【D】

parysort関数の宣言です。(Partial ArRaY SORTの略語としたつもりです)

qsort関数に渡す比較関数は

  • const void *型の引数(仮に仮引数名をa,bとする)を2つ持つ。
  • 関数の戻り値はint型で、(a<b)なら負の値、(a==b)なら0、(a>b)なら正の値をreturnする関数とする。*1

という2つの条件を満たす必要があります。

【E】

わざわざa_pointer,b_pointerなんて変数に落とさず、そのまま【F】に展開しても良いのですが、あまりにも読みにくくなるので1段階分けています。*2

仮引数が汎用化のためにvoid *で宣言されているため、実際に使用するときには型キャストをして受け取る必要があります。今回取り出したいのは、「dataindexに収められたint *」を指し示すポインタなので、intポインタのポインタ*3、すなわち「(int **)」というキャストを行います。

その上で、実際に使いたいのはdataindexに入っているポインタの中身ですから、(int **)の外側にさらに*をつけて、「(int **)ポインタが指し示す参照先の中身=*(int **)」を取り出します。取り出された値はdataindexの中身と同じ、すなわちint*型ですので、変数定義式を兼ねた左辺はint *a_pointerおよびint *b_pointerとなります。

【F】

年と月にわかれた2つのintデータを結合して、日付順に並べ替える必要があります。簡単な方法として、年を100倍として月を足す、例えば(2015年4月=2015*100+04=201504)という値を作ると、年月の前後関係を単純に値の大小関係で比較できます。値が大きくなるので、int(16ビット幅)でなくlong(32ビット幅)が必要なことに注意が必要です。

【B】での処理を振り返ると、*a_pointer*b_pointerとして取り出した値は、datatableのある行(を現す1次元配列)の先頭ポインタです。たとえばdatatable5行目が仮引数aとして選択されていたとすると、(*a_pointer == datatable[5])の関係が成り立ちます。そして、配列名は配列の先頭ポインタと同義(datatable[5] == &(datatable[5][0]))であることと合わせて考えると、「a_pointer[1]という表記はdatatable[5][1]と同義」であり、【A】のコメントでいう「月f」という値が参照できることになります。*b_pointerも全く同じ動きです。

【G】

得られた「年*100+月」値の大小関係を比較して、適切な戻り値を戻します。

【H】

以上で、dataindexが「datatableの月日順)に並べ替え終わっているので、dataindexの値を先頭から順番に取り出して、[0],[1],[2]とサフィックスをつけて取り出せば、datatableに直接触れることも、datatableのコピーを取る必要もなく、部分整列が完了します。

*1ただしこれは昇順ソートをしたい場合で、降順ソートをしたい場合は正負が反転する

*2【F】に直接展開すると long int a_value = *(int **)a[0]*100 + *(int **)a[1];

*3ダブルポインタ(=ポインタのポインタ)といいます。


現在ご覧のページの最終更新日時は2015/04/05 10:17:48です。

Copyright (C) N.Y.City ALL Rights Reserved.

Email: info[at]nycity.main.jp