FC2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »C#
カテゴリー「C#」の記事一覧

【C#】倍数での Floor, Ceil, Round(一定間隔での切り捨て、切り上げ、四捨五入) [double 版]  



 通常の Floor, Ceil, Round は小数の位を切り捨てたり切り上げたりして1ごとの整数にするものだが、5ごとの値とか10の倍数(下1桁を揃える)にしたいと思うことも少なくない。

 ゲームなどで一定間隔の座標に揃えたり、値の下n桁を0したいときなど応用範囲は広いので、毎回書くくらいなら簡単なライブラリとして作っておこう。





■nの倍数で切り捨てる(nおきの数に切り捨てる)

●double 版 MultipleFloor()
using System;

/// <summary>
/// より小さい倍数を求める(倍数で切り捨てられるような値)
///(例)倍数 = 10 のとき、12 → 10, 17 → 10
/// </summary>
/// <param name="value">入力値</param>
/// <param name="multiple">倍数</param>
/// <returns>倍数で切り捨てた値</returns>
public static double MultipleFloor(double value, double multiple)
{
return Math.Floor(value / multiple) * multiple;
}


//メインでは…
Console.WriteLine("5 -> " + MultipleFloor(5, 10)); //0
Console.WriteLine("12 -> " + MultipleFloor(12, 10)); //10
Console.WriteLine("27 -> " + MultipleFloor(27, 10)); //20

(例)n を10おきに切り捨てる(下1桁を0にする)
 5, 12, 27 → 0, 10, 20

(例)n を5おきに切り捨てる
 5, 12, 27 → 5, 10, 25




■nの倍数で切り上げる(nおきの数に繰り上げる)

●double 版 MultipleCeil()
using System;

/// <summary>
/// より大きい倍数を求める(倍数で繰り上がるような値)
///(例)倍数 = 10 のとき、12 → 20, 17 → 20
/// </summary>
/// <param name="value">入力値</param>
/// <param name="multiple">倍数</param>
/// <returns>倍数で切り上げた値</returns>
public static double MultipleCeil(double value, double multiple)
{
return Math.Ceiling(value / multiple) * multiple;
}


//メインでは…
Console.WriteLine("5 -> " + MultipleCeil(5, 10)); //10
Console.WriteLine("12 -> " + MultipleCeil(12, 10)); //20
Console.WriteLine("27 -> " + MultipleCeil(27, 10)); //30

(例)n を10おきに切り上げる(倍数で繰り上げる)
 5, 12, 27 → 10, 20, 30

(例)n を5おきに切り上げる
 5, 12, 27 → 5, 15, 30




■nの倍数で四捨五入のような値を求める(nおきの数の中間の値で切り捨て・切り上げをする)

●double 版 MultipleRound()
using System;

/// <summary>
/// 倍数での四捨五入のような値を求める(nおきの数の中間の値で切り捨て・切り上げをする)
///(例)倍数 = 10 のとき、12 → 10, 17 → 20
/// </summary>
/// <param name="value">入力値</param>
/// <param name="multiple">倍数</param>
/// <returns>倍数の中間の値で、切り捨て・切り上げした値
public static double MultipleRound(double value, double multiple)
{
return MultipleFloor(value + multiple * 0.5, multiple); //四捨五入的
//return Math.Round(value / multiple) * multiple; //五捨六入的(正の数のとき)
}


//メインでは…
Console.WriteLine("5 -> " + MultipleRound(5, 10)); //10
Console.WriteLine("12 -> " + MultipleRound(12, 10)); //10
Console.WriteLine("27 -> " + MultipleRound(27, 10)); //30

(例)n を10おきに切り捨て・切り上げる(10の中間の5を閾値として切り捨て・切り上げをする:4→0, 5→10)
 5, 12, 27 → 10, 10, 30

(例)n を5おきに切り捨て・切り上げる(5の中間の2.5を閾値として切り捨て・切り上げをする:2.4→0, 2.5→5)
 5, 12, 27 → 5, 10, 25

 1つだけ注意点は MultipleRound() に関しては、関数内部で MultipleFloor() を使っている点だ。これは C# の Math.Round() の丸め方はどちらかと言うと五捨六入に近いので、他の言語に移植などするとき、そのままでは結果が異なってしまうため、Round(v) の代わりに Floor(v + 0.5) を使っている。この辺りは C# 限定で Math.Round の仕様に則った値にしたいなら、コメントアウトしてある方を使っても良いだろう。他の言語との Round の値の違いは以下のページの一覧やフォーラムなどを参照して欲しい。

Java, C#, PHP, Ruby, Python, JavaScript での Math.round(四捨五入・五捨六入)比較
Math.Roundって五捨六入?


(関連記事)
【Unity】倍数での Floor, Ceil, Round(一定間隔での切り捨て、切り上げ、四捨五入) [float 版]
 Java, C#, PHP, Ruby, Python, JavaScript での Math.round(四捨五入・五捨六入)比較
【Java】Math.floor(), ceil(), round() 動作互換アルゴリズムを試す


category: C#

thread: プログラミング

janre: コンピュータ

tag: C#ライブラリ 
tb: 0   cm: --

【C#】連想配列(Dictionary)を値 or キーでソート  


 このテーマはググればいくらでも出てくるので、どちらかというと自分用メモ。先に述べてしまうと、初めの LINQ 的な書き方が一番簡単だろう。ただ、他の方法もあったので、それも少しまとめてみた感じ。色々な書き方を知っておくと、他言語に移植するとき等にも役に立つからだ。




■IEnumerable でのソート (LINQ)

 LINQ (System.Linq) が使えるなら、とりあえずこれを使うのが一番楽だろう。内容的には Enumerable(IEnumerable) を使うことになる。キーでのソートも、値でのソートもプロパティを変えるだけで同様にできる。

●値でのソート (IEnumerable)
using System;
using System.Collections.Generic;
using System.Linq;

Dictionary<string, int> dic = new Dictionary<string, int>() {
{ "Becky", 85 },
{ "Daisy", 72 },
{ "Alice", 95 },
{ "Eliza", 78 },
{ "Cindy", 100 },
};

var sorted = dic.OrderBy((x) => x.Value); //昇順
//var sorted = dic.OrderByDescending((x) => x.Value); //降順

foreach (var v in sorted)
{
Console.WriteLine(v.Key + " => " + v.Value);
}

Daisy => 72
Eliza => 78
Becky => 85
Alice => 95
Cindy => 100

 コメントアウトの方は降順になる。キーでのソートはソート対象を変えるだけで良い(x.Value → x.Key に変更)。

●キーでのソート (IEnumerable)
var sorted = dic.OrderBy((x) => x.Key);  //昇順
//var sorted = dic.OrderByDescending((x) => x.Key); //降順

Alice => 95
Becky => 85
Cindy => 100
Daisy => 72
Eliza => 78




■KeyValuePair の List でのソート (ラムダ式)

 これはキーと値のペア(KeyValuePair)のリストを作って、それをソートする方法だ。他の言語でもよく使われる方法なので、覚えておくと役に立つ。List.Sort() はラムダ式が使えるようなので、簡潔に書くことも可能だ。

●値でのソート (KeyValuePair の List と ラムダ式)
using System;
using System.Collections.Generic;

Dictionary<string, int> dic = new Dictionary<string, int>() {
{ "Becky", 85 },
{ "Daisy", 72 },
{ "Alice", 95 },
{ "Eliza", 78 },
{ "Cindy", 100 },
};

List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(dic);
list.Sort((a, b) => a.Value - b.Value); //昇順
//list.Sort((a, b) => b.Value - a.Value); //降順

foreach (var v in list)
{
Console.WriteLine(v.Key + " => " + v.Value);
}

Daisy => 72
Eliza => 78
Becky => 85
Alice => 95
Cindy => 100

 コメントアウトの方は降順になる。キーでのソートはソート対象を変えるだけで良い(x.Value → x.Key に変更)。なお、比較には CompareTo() を使っている。

●キーでのソート (KeyValuePair の List と ラムダ式)
list.Sort((a, b) => a.Key.CompareTo(b.Key));  //昇順
//list.Sort((a, b) => b.Key.CompareTo(a.Key)); //降順

Alice => 95
Becky => 85
Cindy => 100
Daisy => 72
Eliza => 78




■KeyValuePair の List でのソート (delegate)

 これは上記のラムダ式版の別表記と言っても良いのだが、改めて delegate を使えると覚えておくと、例えば、リアルタイムにソート方法を変更するような実装もできると考えることができる。まぁ、引き出しは多いことに越したことはない。

●値でのソート (KeyValuePair の List と delegate)
using System;
using System.Collections.Generic;

Dictionary<string, int> dic = new Dictionary<string, int>() {
{ "Becky", 85 },
{ "Daisy", 72 },
{ "Alice", 95 },
{ "Eliza", 78 },
{ "Cindy", 100 },
};

List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(dic);
list.Sort(delegate(KeyValuePair<string, int> a, KeyValuePair<string, int> b) {
return a.Value - b.Value; //昇順
//return b.Value - a.Value; //降順
});

foreach (var v in list)
{
Console.WriteLine(v.Key + " => " + v.Value);
}

Daisy => 72
Eliza => 78
Becky => 85
Alice => 95
Cindy => 100

 コメントアウトの方は降順になる。キーでのソートはソート対象を変えるだけで良い(x.Value → x.Key に変更)。なお、比較には CompareTo() を使っている。

●キーでのソート (KeyValuePair の List と delegate)
list.Sort(delegate(KeyValuePair<string, int> a, KeyValuePair<string, int> b) {
return a.Key.CompareTo(b.Key); //昇順
//return b.Key.CompareTo(a.Key); //降順
});

Alice => 95
Becky => 85
Cindy => 100
Daisy => 72
Eliza => 78




■KeyValuePair の List でのソート (IComparer)

 ICompare(Comparer) も使えるのでやってみよう。このような比較関数(クラス)は他の言語でもよくあるので、移植もしやすい。ちなみに以前書いた Java での連想配列(Map)でのソートと全く同じ構成となる。

●値(or キー)でのソート (KeyValuePair の List と IComparer)
using System;
using System.Collections.Generic;

public static void Main()
{
Dictionary<string, int> dic = new Dictionary<string, int>() {
{ "Becky", 85 },
{ "Daisy", 72 },
{ "Alice", 95 },
{ "Eliza", 78 },
{ "Cindy", 100 },
};

List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(dic);
list.Sort(new MyComparer<string, int>());

foreach (var v in list)
{
Console.WriteLine(v.Key + " => " + v.Value);
}
}

//比較クラス(関数)
public class MyComparer<K,V> : IComparer<KeyValuePair<K,V>>
where K : IComparable
where V : IComparable
{
public int Compare(KeyValuePair<K,V> a, KeyValuePair<K,V> b) {
return a.Value.CompareTo(b.Value); //値 昇順
//return b.Value.CompareTo(a.Value); //値 降順
//return a.Key.CompareTo(b.Key); //キー 昇順
//return b.Key.CompareTo(a.Key); //キー 降順
}
}

Daisy => 72
Eliza => 78
Becky => 85
Alice => 95
Cindy => 100

 上記の例では1つのクラスでまとめてしまったため、型パラメーターの制約を複数書いてしまっているが、値のみ(V)・キーのみ(K)とわかっているなら、制約はどちらかだけでも良い。同じようなことは delegate 版でもできると思うが、1つのクラスにまとめた利点としては、プロパティなどを追加して、いつでもソート方法を変更できる・同じソート方法を共有できること等にある。用途に合わせて使い分けるのも良いだろう。




■キーと値の2つの配列でのソート

 もう1つ、今回の連想配列のテーマからは少し外れるのだが、C# には「ソート対象となる1次元配列と、その項目に対応する別の1次元配列」をソートできる Array.Sort(Array, Array) がオーバーロードされている。まるで連想配列をソートするような感じになる。

●キーと値の2つの配列でのソート
using System;
using System.Collections.Generic;

public static void Main()
{
string[] name = {"Alice", "Becky", "Cindy", "Daisy", "Eliza"};
int[] value = {95, 85, 100, 72, 78};

Array.Sort(value, name); //昇順
//Array.Sort(value, name, new RevComparer<int>()); //降順

for (int i = 0; i < value.Length; i++)
{
Console.WriteLine(name[i] + " => " + value[i]);
}
}

//逆順(降順)
public class RevComparer<T> : IComparer<T> where T : IComparable
{
public int Compare(T a, T b) {
return b.CompareTo(a); //降順
}
}

Daisy => 72
Eliza => 78
Becky => 85
Alice => 95
Cindy => 100

 引数は Array.Sort(ソート対象の配列, 対応する配列, [IComparer]) となっている。

 ちなみに、コメントアウトされている「RevComparer」(逆順用 Comparer) で降順ソートすると結果は以下のようになる。

Cindy => 100
Alice => 95
Becky => 85
Eliza => 78
Daisy => 72

 ソートのキーとなる配列と値の配列(項目に対応する配列)は別々に作るので、型に自由が利くのが特徴だ。値の配列は何でも良いので、クラスや構造体の配列などをソートすることもできる。普段はクラスや構造体でデータを保持しておいて、ソートしたいときだけキーとなる配列を生成する等もできるだろう。

 Comparer(RevComparer)などもあらかじめ static で作っておけば(int など利用頻度が高い型を別に作っておけば)、毎回 new しなくて済むので実行速度がはやくなる。色々工夫してみると良いだろう。


(関連記事)
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など


category: C#

thread: プログラミング

janre: コンピュータ

tag: C#  連想配列  配列操作  ソート 
tb: 0   cm: --

【C#】2次元配列(ジャグ配列・多次元配列)のソート  


 C#には「ジャグ配列」と「多次元配列」があるのだが、結論を先に言ってしまうと、ソートするならジャグ配列の方が簡単である。というのは初めから用意されている方法が色々あるからだ。では多次元配列はどうかというと、特に用意はされていないので自作することになる。なので、標準的な機能と一般的なソートアルゴリズムとを使って、いくつかの方法でソートをやってみよう。




■ジャグ配列でのソート

 まずジャグ配列の動作を確認しておこう。ジャグ配列とは配列の入れ子状態で、「親の次元は子の次元のオブジェクトの参照を持っている」と考えれば良い。今回の場合2次元配列なので、一番親の配列はそれぞれの配列の参照を格納にしている感じになる。つまりその参照を交換すれば1行分のデータ(配列)を交換したことと同じことになる。図のように考えた場合、縦方向のソート(列をキーに行をソート)となることがわかる。これは他の言語でもだいたい同じなので、覚えておくと色々と応用が利く。





●ジャグ配列でのソート (ラムダ式)
using System;
using System.Text;

//ジャグ配列
int[][] jag = {
new int[] {0, 1, 200},
new int[] {1, 3, 100},
new int[] {2, 4, 100},
new int[] {3, 2, 300},
new int[] {4, 0, 200},
};

Array.Sort(jag, (a, b) => a[1] - b[1]); //昇順(index=1)

Console.WriteLine(ToString(jag)); //デバッグ用関数(※下記を参照)

[[4, 0, 200],
[0, 1, 200],
[3, 2, 300],
[1, 3, 100],
[2, 4, 100]]

(※) ToString(T[][]) は前回作った関数を参照(配列の内容を文字列化→表示するだけのもの)

 ラムダ式の書き方だといまいちアルゴリズムがわかりずらいが、やっていることは上の図と同じと考えて良いだろう。もっとコードの意味を知りたいなら、以下に示す「Comparer」での書き方の方がわかりやすい。実行速度などの違いはあるかもしれないが、ラムダ式だと簡潔に書けることが利点だ。

 ちなみに、ソート第2キーを加えたいなら、ソート関数部分を次のように書くこともできる。これも「Comparer」での書き方と比較すると意味がわかりやすい。用途によって書き分けると良いだろう。

Array.Sort(jag, (a, b) => a[2] == b[2] ? a[1] - b[1] : b[2] - a[2]);  //降順(index=2), 昇順(index=1)

[[3, 2, 300],
[4, 0, 200],
[0, 1, 200],
[1, 3, 100],
[2, 4, 100]]

 これはインデクス[2]の列は降順に、インデクス[1]の列は昇順にしてソートしている(ソート第1キー:[2]が同じなら、ソート第2キー:[1]でソートする)。



●ジャグ配列でのソート (LINQ)
using System;
using System.Linq;

//ジャグ配列
var jag = new [] {
new [] {0, 1, 200},
new [] {1, 3, 100},
new [] {2, 4, 100},
new [] {3, 2, 300},
new [] {4, 0, 200},
};

var sorted = jag.OrderBy(e => e[1]); //昇順(index=1)

foreach (var s in sorted) {
foreach (var e in s) {
Console.Write(e + ", ");
}
Console.WriteLine();
}

4, 0, 200,
0, 1, 200,
3, 2, 300,
1, 3, 100,
2, 4, 100,

 これは先の例のラムダ式と同じ結果となる。型を必要としないなら、この方法でも良いだろう。「using System.Linq」が必要なので忘れずに。

 ちなみに、降順なら「OrderByDescending」を使う。また、「OrderBy~」で同じ値になる場合は「ThenBy/ThenByDescending」で順をつけることができる。つまり第2キーを加えたいなら、以下のようにする。

var sorted = jag.OrderByDescending(e => e[2]).ThenBy(e => e[1]);  //降順(index=2), 昇順(index=1)

3, 2, 300,
4, 0, 200,
0, 1, 200,
1, 3, 100,
2, 4, 100,

 これはインデクス[2]の列は降順に、インデクス[1]の列は昇順にしてソートしている(ソート第1キー:[2]が同じなら、ソート第2キー:[1]でソートする)。



●ジャグ配列でのソート (IComparer, IComparable)
using System;
using System.Collections.Generic;
using System.Text;

//比較クラス(比較関数)
public class MyComparer<T> : IComparer<T[]> where T : IComparable
{
public int Compare(T[] a, T[] b) {
return a[1].CompareTo(b[1]); //昇順
//return b[1].CompareTo(a[1]); //降順
}
}

public static void Main()
{
//ジャグ配列
int[][] jag = {
new int[] {0, 1, 200},
new int[] {1, 3, 100},
new int[] {2, 4, 100},
new int[] {3, 2, 300},
new int[] {4, 0, 200},
};

Array.Sort(jag, new MyComparer<int>());

Console.WriteLine(ToString(jag)); //デバッグ用関数(※下記を参照)
}

[[4, 0, 200],
[0, 1, 200],
[3, 2, 300],
[1, 3, 100],
[2, 4, 100]]

(※) ToString(T[][]) は前回作った関数を参照(配列の内容を文字列化→表示するだけのもの)

 「Comparer」「IComparer」「IComparable」と同じようなものが並んでいてややこしいが、「Comparer」は IComparer などを実装しているクラス、「IComparer」は「Compare()」(比較関数)を使うためのインターフェース、「IComparable」は「CompareTo()」(大小関係を返す関数)を使うためのインターフェースと覚えておけば良いだろう。比較関数は基本的に戻値が正の値だと a > b、負の値だと a < b、0だと a == b を表すと考えれば良い(要するに a - b)。ただこれは昇順の場合であって、降順(逆順)にしたければ、大小を反転(b - a)すれば良いことになる。これも他の言語でも使えることが多いので、覚えておくと役に立つ。

 ついでにこれもソート第2キーを加えてみよう。この場合は比較クラス(関数)の MyComparer にだけ変更を加えれば良い。プロジェクト共有で使用している等なら、一度にすべてのソート方法を変更できるメリットもある。ラムダ式版と合わせて使い分けをすると良いだろう。

●比較クラス(関数)に第2キーを加える
//比較クラス(比較関数)
public class MyComparer<T> : IComparer<T[]> where T : IComparable
{
public int Compare(T[] a, T[] b) {
if (a[2].CompareTo(b[2]) == 0) //第1キーが同じなら
{
return a[1].CompareTo(b[1]); //第2キー:昇順
}
return b[2].CompareTo(a[2]); //第1キー:降順
}
}

[[3, 2, 300],
[4, 0, 200],
[0, 1, 200],
[1, 3, 100],
[2, 4, 100]]

 内容的にはラムダ式版の第2キーを加えたものと同じものになる。Compare() 関数内は if 文で書いているが、ラムダ式のときのように三項演算子(条件演算子)で return しても良い。ただ if 文なら第3キーや特殊な条件を加えたくなったとき、非常に簡単に書ける利点がある。

 どちらの書き方が良いというわけでなく、保守性・拡張性を考えるか、一発簡潔に書くか等、用途によって使い分けるのが良いだろう。




■多次元配列でのソート (クイックソート)

 多次元配列でのソートは初めから用意されてはいないので、ここからは一般的なソートアルゴリズムを用いた方法になる。多次元配列は内部的にはひとつづきなので、要素ごとに交換しなくてはならないことに注意しよう。

 また、2次元配列の場合は縦方向(列をキーに行をソート)と横方向(行をキーに列をソート)の2つがあるが、とりあえずはジャグ配列版と比較のためにも縦方向のソートで書いてみよう。

●2次元配列(多次元配列)のクイックソート(縦方向)
using System;
using System.Collections.Generic;
using System.Text;

//2次元配列のクイックソート(縦方向)
public static void QuickSortRow<T>(T[,] arr, int sortIndex, int first, int last, IComparer<T> comp)
{
int i = first; //前方検索インデクス
int j = last; //後方検索インデクス
T p = Median(arr[i, sortIndex], arr[(j - i) / 2 + i, sortIndex], arr[j, sortIndex], comp);
int col = arr.GetLength(1);

while (true)
{
while (comp.Compare(arr[i, sortIndex], p) < 0) { //基準値以上の位置まで前方検索
i++;
}
while (comp.Compare(arr[j, sortIndex], p) > 0) { //基準値以下の位置まで後方検索
j--;
}
if (i >= j) { //交差したら終了
break;
}

//スワップ
for (int k = 0; k < col; k++) {
T tmp = arr[i, k];
arr[i, k] = arr[j, k];
arr[j, k] = tmp;
}

i++;
j--;
}

//基準値より2分割して再帰呼び出し(2分割できないときは再帰停止)
if (first < i - 1) {
QuickSortRow(arr, sortIndex, first, i - 1, comp);
}
if (j + 1 < last) {
QuickSortRow(arr, sortIndex, j + 1, last, comp);
}
}

//独自の Comparer(IComparer)用
public static void QuickSortRow<T>(T[,] arr, int sortIndex, IComparer<T> comp)
{
QuickSortRow(arr, sortIndex, 0, arr.GetLength(0) - 1, comp);
}

//既定の Comparer(IComparer)用
public static void QuickSortRow<T>(T[,] arr, int sortIndex) where T : IComparable
{
QuickSortRow(arr, sortIndex, 0, arr.GetLength(0) - 1, Comparer<T>.Default);
}

//x, y, z の中央値を返す
public static T Median<T>(T x, T y, T z, IComparer<T> comp)
{
if (comp.Compare(x, y) < 0)
{
if (comp.Compare(y, z) < 0) return y;
else if (comp.Compare(z, x) < 0) return x;
else return z;
}
else
{
if (comp.Compare(z, y) < 0) return y;
else if (comp.Compare(x, z) < 0) return x;
else return z;
}
}


public static void Main()
{
//多次元配列
int[,] grid = {
{0, 1, 200},
{1, 3, 100},
{2, 4, 100},
{3, 2, 300},
{4, 0, 200},
};

QuickSortRow(grid, 1);
Console.WriteLine(ToString(grid)); //デバッグ用関数(※下記を参照)
}

[[4, 0, 200],
[0, 1, 200],
[3, 2, 300],
[1, 3, 100],
[2, 4, 100]]

(※) ToString(T[,]) は前回作った関数を参照(配列の内容を文字列化→表示するだけのもの)

 クイックソートのアルゴリズムは、Wikipedia でも見て欲しい。コードも実装例を2次元配列用に改造したものである。実は以前に書いた Java 版(横方向)の焼き直しだったりもする。

 なお、基準値(p : Pivot) は簡略のために、検索範囲の最初と最後の値を加算して2で割る方法(平均値)でも良い。ここではジェネリック型で作っているので、Wikipedia に載っている med3 関数(中間値[中央値])を使っている(ジェネリック型で演算はできないので)。このピボットの値をどう取るかで効率が変わるので色々研究されているらしい。実際には元の並びにもよるのでやりやすいものを使えば良いだろう。

 また注意点としては、このクイックソートは安定ソートではないので、同値の場合は、他の列の並びは不定になる。他の列の順序も考慮に入れたいのなら、安定ソートのものでソート関数内に第2ソートキー比較などを入れる工夫が必要になるだろう。挿入ソートを2回使う手もある。ユニーク値や連番なら問題はない。

 逆順(降順)にしたいなら、以下の Comparer を使えば良い。

●逆順(降順)の Comparer を定義して使う
//逆順(降順)
public class RevComparer<T> : IComparer<T> where T : IComparable
{
public int Compare(T a, T b) {
return b.CompareTo(a); //降順
}
}

//メインでは...
QuickSortRow(grid, 1, new RevComparer<int>());

[[2, 4, 100],
[1, 3, 100],
[3, 2, 300],
[0, 1, 200],
[4, 0, 200]]




■多次元配列でのソート (挿入ソート)

 もう1つ、安定ソートを使いたいときのために、挿入ソートも書いておこう。といってもこれも以前 Java で書いた「2次元配列の挿入ソート(横方向)」の焼き直しである。縦と横の方向を入れ替えただけなので、難しくはないだろう。

●2次元配列(多次元配列)の挿入ソート(縦方向)
using System;
using System.Collections.Generic;
using System.Text;

//2次元配列の挿入ソート(縦方向)
public static void InsertionSortRow<T>(T[,] arr, int sortIndex, IComparer<T> comp)
{
int row = arr.GetLength(0);
int col = arr.GetLength(1);
T[] tmp = new T[col];

for (int i = 1; i < row; i++)
{
T t = arr[i, sortIndex];
if (comp.Compare(arr[i - 1, sortIndex], t) > 0)
{
//列の退避
for (int k = 0; k < col; k++)
{
tmp[k] = arr[i, k];
}

//上へずらす
int j = i;
do {
for (int k = 0; k < col; k++)
{
arr[j, k] = arr[j - 1, k];
}
j--;
} while (j > 0 && comp.Compare(arr[j - 1, sortIndex], t) > 0);

//列のストア
for (int k = 0; k < col; k++)
{
arr[j, k] = tmp[k];
}
}
}
}

//既定の Comparer(IComparer)用
public static void InsertionSortRow<T>(T[,] arr, int sortIndex) where T : IComparable
{
InsertionSortRow(arr, sortIndex, Comparer<T>.Default);
}


public static void Main()
{
//多次元配列
int[,] grid = {
{0, 1, 200},
{1, 3, 100},
{2, 4, 100},
{3, 2, 300},
{4, 0, 200},
};

InsertionSortRow(grid, 1); //昇順
//InsertionSortRow(grid, 1, new RevComparer<int>()); //降順
Console.WriteLine(ToString(grid)); //デバッグ用関数(※下記を参照)
}

[[4, 0, 200],
[0, 1, 200],
[3, 2, 300],
[1, 3, 100],
[2, 4, 100]]

(※) ToString(T[,]) は前回作った関数を参照(配列の内容を文字列化→表示するだけのもの)

 逆順(降順)にしたい場合は、RevComparer を使う。安定ソートなので、2回以上のソート(別々のキー[列]で)しても、並びは保持されるので、第2, 3,…ソートキーを使うのと同じ効果になる(※ただし、効率は良くない)。




■多次元配列→ジャグ配列 変換してソート

 これは、多次元配列では内部的にはひとつづきのため、要素ごとに処理する必要があるので、いっそのことジャグ配列に変換してしまおうという方法だ。

 「多次元配列→ジャグ配列 変換」は以前に作ったものをそのまま使う。あとは「ジャグ配列でのソート」と同じだ。

●多次元配列→ジャグ配列 変換(→前回作ったもの)してからソート
using System;
using System.Collections.Generic;
using System.Text;

/**
* 多次元配列をジャグ配列に変換
*/
public static T[][] ToJaggedArray<T>(T[,] arr)
{
int row = arr.GetLength(0);
int col = arr.GetLength(1);

T[][] jag = new T[row][];

for (int i = 0; i < row; i++)
{
jag[i] = new T[col];
for (int j = 0; j < col; j++)
{
jag[i][j] = arr[i, j];
}
}

return jag;
}


public static void Main()
{
//多次元配列
int[,] grid = {
{0, 1, 200},
{1, 3, 100},
{2, 4, 100},
{3, 2, 300},
{4, 0, 200},
};

int[][] jag = ToJaggedArray(grid); //多次元配列→ジャグ配列 変換
Array.Sort(jag, (a, b) => a[1] - b[1]); //昇順(index=1)
//Array.Sort(jag, (a, b) => b[1] - a[1]); //降順(index=1)
Console.WriteLine(ToString(jag)); //デバッグ用関数(※下記を参照)
}

[[2, 4, 100],
[1, 3, 100],
[3, 2, 300],
[0, 1, 200],
[4, 0, 200]]

(※) ToString(T[][]) は前回作った関数を参照(配列の内容を文字列化→表示するだけのもの)

 まぁ、はじめからジャグ配列を使うのが一番楽だが、負荷が許せるなら、変換するのもアリだろう。




■キーと値の2つの配列でのソート

 もう1つ、今回の2次元配列のテーマからは少し外れるのだが、C# には「ソート対象となる1次元配列と、その項目に対応する別の1次元配列」をソートできる Array.Sort(Array, Array) がオーバーロードされている。まるで連想配列をソートするような感じになる。

using System;
using System.Collections.Generic;

string[] name = {"Alice", "Becky", "Cindy", "Daisy", "Eliza"};
int[] value = {95, 85, 100, 72, 78};

Array.Sort(value, name); //昇順
//Array.Sort(value, name, new RevComparer<int>()); //降順

for (int i = 0; i < name.Length; i++)
{
Console.WriteLine(name[i] + " => " + value[i]);
}

Daisy => 72
Eliza => 78
Becky => 85
Alice => 95
Cindy => 100

 引数は Array.Sort(ソート対象の配列, 対応する配列, [IComparer]) となっている。

 ちなみに、コメントアウトされている「RevComparer」(逆順用 Comparer) で降順ソートすると結果は以下のようになる。

Cindy => 100
Alice => 95
Becky => 85
Eliza => 78
Daisy => 72

 ソートのキーとなる配列と値の配列(項目に対応する配列)は別々に作るので、型に自由が利くのが特徴だ。値の配列は何でも良いので、クラスや構造体の配列などをソートすることもできる。普段はクラスや構造体でデータを保持しておいて、ソートしたいときだけキーとなる配列を生成する等もできるだろう。

 Comparer(RevComparer)などもあらかじめ static で作っておけば(int など利用頻度が高い型を別に作っておけば)、毎回 new しなくて済むので実行速度がはやくなる。色々工夫してみると良いだろう。


(関連記事)
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など
【C#】連想配列(Dictionary)を値 or キーでソート


■参考になる書籍


category: C#

thread: プログラミング

janre: コンピュータ

tag: C#  配列操作  ソート 
tb: 0   cm: --

【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など  


 Java では2次元配列はジャグ配列になるのだが、C#には「多次元配列」と「ジャグ配列」の選択肢がある。使う時は微妙に概念やプロパティなどが違うので、簡単な図解で理解し、相互変換などをやってみよう。




■概念・サイズなどのプロパティ

 「多次元配列とジャグ配列の違いは?」を超簡単に説明すると「多次元配列はデータを格子状に並べた配列」「ジャグ配列は個々の配列を更に並べた配列(入れ子の配列)」でいいと思う。そんな感じで理解してしまえば、「Length」や「GetLength()」の使い方がよくわかる。以下にそれぞれの違いを図解してみよう。



■多次元配列とは

 とりあえず「データを格子状に並べた配列」と考えれば良い。図解にすると以下のようになる。


●多次元配列
using System;
using System.Collections.Generic;
using System.Text;

public static void Main(){
//多次元配列
int[,] grid = {
{ 0, 1, 2, 3, 4},
{ 5, 6, 7, 8, 9},
{10, 11, 12, 13, 14},
};

Console.WriteLine(ToString(grid)); //デバッグ用関数(※下記を参照)
Console.WriteLine("Length = " + grid.Length);
Console.WriteLine("GetLength(0) = " + grid.GetLength(0));
Console.WriteLine("GetLength(1) = " + grid.GetLength(1));
Console.WriteLine("grid[2, 3] = " + grid[2, 3]);
}

[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]
Length = 15
GetLength(0) = 3
GetLength(1) = 5
grid[2, 3] = 13

(※) ToString(T[,]) は下記を参照

 各次元のサイズは、

 GetLength(次元数)

になる。例では2次元配列になっているが、3次元配列なら GetLength(0~2) のようになる。

 そして「Lengthは全体の要素数」になるので注意しよう。ひとつづきのデータが格子状に並んでいるだけと考えればわかりやすいだろう。3次元以上になったときも同じように考えれば良い。ちなみに「foreach」で列挙すれば図のような並びで出てくる。

foreach (int i in grid) Console.Write(i + " ");

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

 また、実際にメモリ上でもひとつづきに領域が確保されており、ポインタで連続処理するのに利用することもできる。そういった処理の場合はジャグ配列より多次元配列の方が良いだろう。以下に参考URLを載せておこう。

(参考)
多次元配列の速度比較
C#のジャグ配列・多次元配列をpin_ptrでC++に渡せるか?




■ジャグ配列とは

 とりあえず「個々の配列を更に並べた配列(入れ子の配列)」と考えれば良い。図解にすると以下のようになる。


●ジャグ配列
using System;
using System.Collections.Generic;
using System.Text;

public static void Main(){
//ジャグ配列
int[][] jag = {
new int[] { 0, 1, 2, 3, 4},
new int[] { 5, 6, 7},
new int[] {10, 11, 12, 13},
};

Console.WriteLine(ToString(jag)); //デバッグ用関数(※下記を参照)
Console.WriteLine("Length = " + jag.Length);
Console.WriteLine("GetLength(0) = " + jag.GetLength(0)); //= jag.Length
Console.WriteLine("jag[0].Length = " + jag[0].Length);
Console.WriteLine("jag[1].Length = " + jag[1].Length);
Console.WriteLine("jag[2].Length = " + jag[2].Length);
Console.WriteLine("jag[2][3] = " + jag[2][3]);
}

[[0, 1, 2, 3, 4],
[5, 6, 7],
[10, 11, 12, 13]]
Length = 3
GetLength(0) = 3
jag[0].Length = 5
jag[1].Length = 3
jag[2].Length = 4
jag[2][3] = 13

(※) ToString(T[][]) は下記を参照

 次にジャグ配列だが、図のように個々の配列が、更にまとめられて1つの配列にされていると考えれば良い。なので多次元配列と違って個々の配列の長さは自由になる(Length はそれぞれのサイズになる)。なので例えば「隣接リスト」などに使えばメモリの節約にもなる(隣接行列ならどちらでも良い)。メモリ上でも配列ごとに違う領域に割り当てられると考えて良いだろう(システムが勝手に割り当てる)。例では1次元配列が入れ子になっているが、親の次元はオブジェクトの参照を要素としている(この例の場合は各配列の頭[0])ので、中身は何でも良い(型が邪魔なら object 配列などにすれば良い→ボックス化のようになる)。




■多次元配列→ジャグ配列 変換

 LINQ を使っても良いのかも知れないが、とりあえず今回は概念の図をそのままコーディングするという感じで、ついでにジェネリクスで書いておこう。こういう書き方はダサいかも知れないが(笑)、用途に合わせて改造しやすい利点がある。引数などを追加したバージョンも作りやすいしね。他の言語にも移植しやすい。ちなみに Java ではジェネリクスで new はできないので移植する際には注意しよう。

using System;
using System.Collections.Generic;
using System.Text;

/**
* 多次元配列をジャグ配列に変換
*/
public static T[][] ToJaggedArray<T>(T[,] arr)
{
int row = arr.GetLength(0);
int col = arr.GetLength(1);

T[][] jag = new T[row][];

for (int i = 0; i < row; i++)
{
jag[i] = new T[col];
for (int j = 0; j < col; j++)
{
jag[i][j] = arr[i, j];
}
}

return jag;
}


//メインでは...
int[,] grid = {
{ 0, 1, 2, 3, 4},
{ 5, 6, 7, 8, 9},
{10, 11, 12, 13, 14},
};

int[][] jag = ToJaggedArray(grid);
Console.WriteLine(ToString(jag)); //デバッグ用関数(※下記を参照)
Console.WriteLine("Length = " + jag.Length);
Console.WriteLine("GetLength(0) = " + jag.GetLength(0)); //= jag.Length
Console.WriteLine("jag[0].Length = " + jag[0].Length);
Console.WriteLine("jag[1].Length = " + jag[1].Length);
Console.WriteLine("jag[2].Length = " + jag[2].Length);

[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]
Length = 3
GetLength(0) = 3
jag[0].Length = 5
jag[1].Length = 5
jag[2].Length = 5

(※) ToString(T[][]) は下記を参照




■ジャグ配列→多次元配列 変換

using System;
using System.Collections.Generic;
using System.Text;

/**
* ジャグ配列を多次元配列に変換
* col : 列数
*/
public static T[,] ToDimensionalArray<T>(T[][] arr, int col)
{
int row = arr.Length;

T[,] dim = new T[row, col];

for (int i = 0; i < row; i++)
{
for (int j = 0; j < arr[i].Length && j < col; j++)
{
dim[i, j] = arr[i][j];
}
}

return dim;
}

//最大の col(列数)で多次元配列に変換
public static T[,] ToDimensionalArray<T>(T[][] arr)
{
int row = arr.Length;
int col = 0;

//最大を取得
for (int i = 0; i < row; i++)
{
col = Math.Max(col, arr[i].Length);
}

return ToDimensionalArray(arr, col);
}


//メインでは...
int[][] jag = {
new int[] { 0, 1, 2, 3, 4},
new int[] { 5, 6, 7, 8, 9},
new int[] {10, 11, 12, 13, 14},
};

int[,] grid = ToDimensionalArray(jag);
Console.WriteLine(ToString(grid)); //デバッグ用関数(※下記を参照)
Console.WriteLine("Length = " + grid.Length);
Console.WriteLine("GetLength(0) = " + grid.GetLength(0));
Console.WriteLine("GetLength(1) = " + grid.GetLength(1));

[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]]
Length = 15
GetLength(0) = 3
GetLength(1) = 5

(※) ToString(T[,]) は下記を参照

 引数を1つとる関数の方はオーバーロード用で、ジャグ配列は個々の長さが違う可能性があるため、列の最大を取得している。長さが違う場合、列の最大に満たない要素は型の既定値で初期化される。作りたい多次元配列の列数がわかっている場合は、引数を2つとる関数の方の col に具体的な値を入れた方が速い。列の長さが同じとわかってるなら以下のようにしても良い。

int[,] grid = ToDimensionalArray(jag, jag[0].Length);





■文字列化

 これはどちらかというとデバッグ用なのだが、よく使うので書いておこう。ただ配列の中身を確認するためのものだ。

using System;
using System.Collections.Generic;
using System.Text;

//文字列変換用
static StringBuilder sb = new StringBuilder(1024);

/**
* 2次元配列の文字列化(多次元配列)
*/
public static string ToString<T>(T[,] arr, string sep = ", ", string rowSep = "\n",
string start_bracket = "[", string end_bracket = "]")
{
sb.Length = 0;
sb.Append(start_bracket);
for (int i = 0; i < arr.GetLength(0); i++) {
if (i > 0) {
sb.Append(sep);
sb.Append(rowSep);
}

sb.Append(start_bracket);

for (int j = 0; j < arr.GetLength(1); j++)
{
if (j > 0)
{
sb.Append(sep);
}
sb.Append(arr[i, j]);
}
sb.Append(end_bracket);
}
sb.Append(end_bracket);

return sb.ToString();
}

/**
* 2次元配列の文字列化(ジャグ配列)
* ※Totring<T>(T[])と曖昧エラーとなるので注意
*/
public static string ToString<T>(T[][] arr, string sep = ", ", string rowSep = "\n",
string start_bracket = "[", string end_bracket = "]")
{
sb.Length = 0;
sb.Append(start_bracket);
for (int i = 0; i < arr.Length; i++) {
if (i > 0) {
sb.Append(sep);
sb.Append(rowSep);
}

sb.Append(start_bracket);

for (int j = 0; j < arr[i].Length; j++)
{
if (j > 0)
{
sb.Append(sep);
}
sb.Append(arr[i][j]);
}
sb.Append(end_bracket);
}
sb.Append(end_bracket);

return sb.ToString();
}

 1つだけ注意点は、1次元配列用に「ToString<T>(T[] arr, ~)」を新たに作った場合、ジャグ配列の「ToString<T>(T[][] arr, ~)」と曖昧エラー(「The call is ambiguous between the following methods or properties: ~」)が出るかも知れない。これは T を int として int[][] と解釈、または、T を int[] として int[][] とも解釈できるからだ。その場合は関数をリネームなどして別々に使えば良い。





(関連記事)
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】連想配列(Dictionary)を値 or キーでソート


category: C#

thread: プログラミング

janre: コンピュータ

tag: C#  配列操作 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR