次の形式の浮動小数点値の配列があるとします。
{ 1.34, 1.15, 1.1, 1.2, 1.26, 1.10, 1.20, 1.17 }
それらがユーザー入力(またはその他のメカニズム)によって提供されたと想定します。ユーザーが「1.1」を実際には「1.01」を意味すると見なします。つまり、「1.10」とは別の意味です。
標準のソートアルゴリズム(バブルソート、クイックソート、またはフレームワーク固有のソート)を使用すると、配列は次のようになります。
{ 1.1, 1.10, 1.15, 1.17, 1.2, 1.20, 1.26, 1.34 }
ただし、必要な出力配列は次のようになります。
{ 1.1, 1.2, 1.10, 1.15, 1.17, 1.20, 1.26, 1.34 }
これを行う方法は、並べ替えの前に配列を反復して、次のようにすることだと思います:
これにより、2つの配列が生成されます。1つは小数点以下N桁またはM桁の値(生のユーザー入力)を含み、もう1つは小数点以下M桁のすべての値を含みます(「サニタイズ」ユーザー入力)。これは、小数点以下M桁の値を含む配列をソートすると、必要な結果が得られることを意味します。
これを行う他の方法はありますか?より高速なアルゴリズム、またはオーバーヘッドの少ないアルゴリズムを探しています。
私はこれを一般的な問題の領域であると考えており、これを修正する方法はたくさんあると思います。必要な結果を得るために使用できる他のアルゴリズムにはどのようなものがありますか。
あなたの問題は、あなたの「数」が小数点以下を持たないということです。 .
で区切られた2つの整数で構成される文字列があります。それらを2つの整数として解析し、カスタム比較演算子を記述します。並べ替えアルゴリズムは同じままにすることができ、比較器に渡すだけで済みます。
C#では、このような比較演算子は次のようになります。
void Test()
{
var data=new string[]{ "1.34", "1.15", "1.1", "1.2", "1.26", "1.10", "1.20", "1.17" };
Array.Sort(data, VersionComparer);
data.Dump();
}
static int VersionComparer(string s1, string s2)
{
List<int> parts1 = s1.Split('.').Select(int.Parse).ToList();
List<int> parts2 = s2.Split('.').Select(int.Parse).ToList();
while(parts1.Count < parts2.Count)
parts1.Add(0);
while(parts2.Count < parts1.Count)
parts2.Add(0);
for(int i = 0; i < parts1.Count; i++)
{
if(parts1[i] < parts2[i])
return -1;
if(parts1[i] > parts2[i])
return +1;
}
return 0;
}
少し注意してください:一部の値の桁数が他の桁数よりも大きい場合、現在の値よりもnot floatになります。これはfloatとして解釈されるを意図した文字列ですが、アクセスするたびに解釈する必要があり、それがまさにここでの問題です。
基本的には2つのオプションがあります。沼地標準のソートアルゴリズムを使用し、引数で何かを計算する前にFloat.parseFloat()
を実行するようにコンパレータルーチンをプログラムするか、別のステップで一度だけ解析を実行して数値を保存します。どこかに値があるので、再度行う必要はありません。
明らかに、どちらのソリューションが優れているかは、コンパレータが呼び出される頻度に依存します。リストがランダムな順序である場合、ソート中にほとんどの値がおそらく2回以上処理されるため、実際に比較する値を事前に計算することは意味があります。しかし、リストがほぼソートされる傾向がある場合、またはフロート解析が安すぎるために(変換ステップを追加するプログラミング作業と比較して)心配な場合は、比較のたびにその場で実行する方が適切です。
いつものように、どちらが優れているか一般的には答えられず、どちらが優れているかあなたの場合両方のソリューションをプロファイリングしないと答えられません。決して想定しない-常に測定する。
作業している配列の大きさによっては、関数型プログラミングの「装飾、並べ替え、装飾解除」パターン(これは メモ化 の形式です)を適用できます。
文字列を一定の長さに分割してデータを破棄することは、効率が悪い場合があります。結局のところ、文字列を並べ替えている間、文字列は変更されないため、一度だけ実行する必要があります。
Perlでは、これは Schwartzian変換 として知られています-このためのコードは次のようになります:
#!/usr/bin/Perl
use strict;
my @data = qw ( 1.34 1.15 1.1 1.2 1.26 1.10 1.20 1.17 );
my @sorted =
map { $_->[0] }
sort { $a->[1] <=> $b->[1] or $a->[2] <=> $b->[2] }
map {
my @l = split(/\./);
[$_, length($l[1]), $l[1]] }
@data;
print join(',', @sorted),"\n";
これの秘訣は、 "1.15"から["1.15"、2、15]への変更が1回だけ行われ、それらの値からソートが機能することです。小さなサイズの配列の場合、これはパフォーマンスのマイナーな向上です。大規模な配列の場合、非常に重要になる可能性があります。
「配列内の物を投げて並べ替えるだけ」が欠けている言語では、これは少し複雑になります。元のデータとコンポーネントパーツ、および並べ替え可能なオブジェクトを作成する必要があります。これは、コンストラクターに配置できるため、操作が少し簡単です。
Javaでは、次のアプローチを使用できます。
public class DecimalSortable implements Comparable<DecimalSortable> {
private int len;
private int dec;
private String data;
public DecimalSortable(String data) {
this.data = data;
String[] array = data.split("\\.");
len = array[1].length();
dec = Integer.parseInt(array[1]);
}
@Override
public int compareTo(DecimalSortable o) {
int rv = Integer.compare(len, o.len);
if(rv == 0) {
rv = Integer.compare(dec, o.dec);
}
return rv;
}
public String getData() {
return data;
}
}
スプリットへの単一の呼び出しと、後でソートされる値の抽出に注意してください。はい、それはより動的な言語よりも少しオーバーヘッドがありますが、それがどのように機能するか、そしてsplitへの繰り返しの呼び出しを回避する方法(および各比較のための新しいStringオブジェクトの関連する作成)を理解します。