【c#】linqで値が近いデータを一件だけ抽出したい
- 評価
- クリップ 0
- VIEW 5,178
やりたいこと
linqで順序を保持したままフィルターしたいです。
例えば、リストの中にソートされていない1から10000まで数字があって、100が入力されたら50から150までを、300が入力されたら250から350までを、ソートされたコレクションとして返したいです。
このとき、まずは1から10000までの入ったリストをorderbyでソートし、その後各入力に対してwhereでフィルタした結果を返したいのですが、whereを使うと順序が保持されないとドキュメントにありました。
このような場合にどういった処理をすればよいか教えてください。
補足
さきにorderbyしたいのは、実際には入力というのがクライアントからの膨大な数になるからです。
それでも毎回フィルタしてからorderbyしたほうがいいでしょうか?
補足2
試してみたところ、最初にorderbyをするだけで何秒もかかってしまうので、最初にorderbyするという選択肢はなくなりました。
本当にやりたいことは、{時間、性別}というデータのコレクションがあり(これが1から10000まで)、{5時、男}という入力に対して、まず4時半から5時半の間で最も遅い時間のその性別のデータをとってくるということです。
最初にこのデータのコレクションをソートするというのはやめるとして、どのような方法で取得するのがよいでしょうか?
{最遅5時半、最早4時半、男}で絞り込んだ後、orderbydescendingしてfirstordefaultをとってくるのがよいでしょうか?
これだとやはり1入力で1orderbyになりますが…
そもそも1件しか必要ないのですが、ソートせずにとってきたりする方法もありますか?
時間についてminをつかおうとおもったのですが、データのインスタンスではなく時間がとれてしまいました。
※順序を保持したまま〜というのはなしになったため、タイトルを変えました。
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
checkベストアンサー
+1
こんにちは。
Whereメソッドはそのソースコードからして、元のコレクションの順序を崩すということは「絶対に」ありません。
もしOrderedなコレクションをWhereに渡した場合、その結果もまたOrderedであることは確実です。
こちらでLinqのソースコードを閲覧することができます。
referencesource/Enumerable.cs
Linq以外にも、.NET Frameworkの基本ライブラリはソースコードが公開されているので、今のように不安なときは目を通すと良いですよ。
補足について。
結論から言うと、「必ずフィルタしてからOrderByするべき」です。
ソート処理というのは、要素数が増えるほどに「爆発的に計算量が増えます」。
OrderByメソッドは、最初にコレクションの全ての要素を読み込んで、それをソートする処理になります。
よって、100件のソートと10000件のソートではその負荷が段違いです。
ソースのコレクションが膨大な量であるということですが、それこそ先にWhereを通すことで、ソート対象のコレクションを小さくすることが重要なのです。
補足2について。
{最遅5時半、最早4時半、男}で絞り込んだ後、orderbydescendingしてfirstordefaultをとってくるのがよいでしょうか?
Linqを使う場合はこれで正解だと思います。
コレクションの種類次第では、もう少し効率の良い方法があるかもしれませんが。
追記:
1件だけ抽出したい場合、Aggregateを使う方法があります。
// 先にフィルタする
var filterdCollection = originalCollection
.Where(x => x.性別 == 男)
.Where(x => 四時半 < x.時間 && x.時間 <= 五時半);
// 二つの要素を比較して、{ 時間 } が遅いほうを選択
var result = filteredCollection.Aggregate((x, y) => (x.時間 < y.時間) ? y : x);
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
+1
試してみましたか?基本的にリストならLinqはソート順序をキープしてくれますよ。
コレクションによって実装が異なったりするので一概には言えないんですけどね。
しかも安定したソートなので同値であっても順序の入れ替えとかも起きません。
Where使った後OrderByする方が自然ではあるんですが、仮に逆順でもよっぽどのことをしなければ大丈夫です。
// 50,51,52,... 148,149,150
foreach (var number in Enumerable.Range(1, 10000)
.OrderBy(number => number)
.Where(number => 50 <= number && number <= 150))
{
Console.WriteLine($"{number}");
}
// 150,149,148,... 52,51,50
foreach (var number in Enumerable.Range(1, 10000)
.OrderByDescending(number => number)
.Where(number => 50 <= number && number <= 150))
{
Console.WriteLine($"{number}");
}
補足への回答
先にコレクションをソートしておけば大丈夫ですよ。
最初に言った通りWhere句で並び順が崩れるなんていうことは普通ありません。
必要に応じてWhereメソッドを呼び出すというのを何度もやったらいいです。
もしも現在の実装で並び順が崩れるため、パフォーマンスを考えてWhere句の後にOrderByしない解決方法を必要としている場合はコードを添付してください。
// コレクションを最初に作る時にソートしておく
MyCollection = new Collection<MyItem>(DataSource.OrderBy(item => item.Id));
// 入力されたら前後50の範囲を返す関数を作っとく。
public static class MyItemExtension
{
public static IEnumerable<MyItem> Sampling(this IEnumerable<MyItem> items, int threshold)
{
return items.Where(item => (threshold - 50) <= item.Id && item.Id <= (threshold + 50));
}
}
// 50~150受け取りたい処理にパス
SomeFunction(MyCollection.Sampling(100));
// 250~350受け取りたい処理にパス
SomeFunction(MyCollection.Sampling(300));
パフォーマンス上のことで更に追記
もう一つ。1から10000まで隙間なくデータが埋まっているならWhere句よりGetRangeを使うこともできます。
リストはコピーされるものの、こちらはリスト全体を舐めないですむので場合によっては高速になります。
public static class ListExtension
{
public static List<T> Sampling(this List<T> items, int threshold)
{
// startIndexを計算で求められる場合にはそうする。
// 1から10000のリストなので追加で1引いた数にすれば良い。
int begin = threshold - 51;
return items.GetRange(begin, 101); // 50 ~ 150は101要素だから。
}
}
※コレクションによっては最善の方法でキー探索とかしてくれるかもしれませんが、わかりませんしね。。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
0
効率の点からも先にWhereで絞り込んでからソートすればよいと思います。
List<int> list = ...;
int sentinel = 100;
var sorted = list
.Where(i => i >= sentinel - 50 && i <= sentinel + 50)
.OrderBy(i => i)
.ToList();
Whereが順序を保証しないというのはMSDNにはないようでしたがどこに記載されていましたか?
LINQやJavaのStreamやScalaのSeqなどどれも似たようなものだと思っているのですが、複数のスレッドで並行して処理するようにしない限り要素の順序はパイプラインの前段のまま保存されるのが普通だと思ってました。本当はどうなんでしょう・・・
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
-1
聞きたいことがころころ変わりすぎじゃんね!(笑)
次からは質問する時に自信がなければ変にやりたいことを自分で要約しすぎないようにしましょう。
そもそも1から10000がミクロも関係ない話題ですねこれは…。
もしもコレクションのソースがデータベースで、キャッシュしておけずに毎回SQL問い合わせが発生するようなケース(Webアプリの場合とか)だったらそもそもLinq使うのやめて全部SQL文で対処しましょう。SQL側にも1件だけデータを取ってくるための構文が大体用意されてます。(SQL Serverだったら TOP 1とかね。)
コレクションが固定のマスタでデータをキャッシュしておけるなら、Linqを使えばいいでしょう。
データベースにアクセスするレイテンシを排除することもできますしね。
キャッシュした場合は実現したい検索処理によってどうしたら高速になるのかは変わってきます。
一番簡単な方法であればコレクションをソート済みで用意しておき、Whereメソッドを必要に応じて繋げていき、最も遅い時間を取ってくるのはLastOrDefault
を、最も早い時間はFirstOrDefault
を使えばいいです。
ソートの必要がないように最初にコレクションを用意しておくのはここでもやはり重要です。
まずは時間順でソートしておきましょう。これはSQLならデータを取ってくる時にしておけばいいです。
コレクションをOrderByしたら時間がかかっても、SQLでのソートはインデックスがあれば一瞬です。
SELECT * FROM DATA ORDR BY 時間, 性別;
要素数1万ぐらいならWhereメソッドでフィルターしても全然OKです。
あまりに頻繁に呼ばれるなら色々工夫しなきゃいけなくなりますが、気になるぐらい遅いことはないです。
IEnumerable<T> collection = MyCollection;
if (/*性別を制限するなら*/)
{
collection = collection.Where(/*性別の制限*/);
}
if (/*最小時間を設定するなら*/)
{
collection = collection.Where(/*最小時間以上*/);
}
if (/*最大時間を設定するなら*/)
{
collection = collection.Where(/*最大時間以下*/);
}
if (/* 最も遅い時間をとってくるなら */)
{
return collection.LastOrDefault();
}
else
{
// 最も早い時間をとってくるなら
return collection.FirstOrDefault();
}
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.21%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
2016/12/17 11:32
ありがとうございました。
2016/12/17 12:42
要素が1件の場合はその1件をそのまま返してくれます。
2016/12/17 16:05
1件の場合も別に判定したほうがよいのでしょうか?
2016/12/17 17:31
2016/12/17 21:33
ありがとうございました。