質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

4回答

10148閲覧

【c#】linqで値が近いデータを一件だけ抽出したい

syogakusya

総合スコア67

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

1グッド

0クリップ

投稿2016/12/16 05:29

編集2016/12/16 07:30

###やりたいこと

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をつかおうとおもったのですが、データのインスタンスではなく時間がとれてしまいました。
※順序を保持したまま〜というのはなしになったため、タイトルを変えました。

teraotailnosuke👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

ベストアンサー

こんにちは。
Whereメソッドはそのソースコードからして、元のコレクションの順序を崩すということは「絶対に」ありません。
もしOrderedなコレクションをWhereに渡した場合、その結果もまたOrderedであることは確実です。

こちらでLinqのソースコードを閲覧することができます。
referencesource/Enumerable.cs
Linq以外にも、.NET Frameworkの基本ライブラリはソースコードが公開されているので、今のように不安なときは目を通すと良いですよ。

補足について。
結論から言うと、「必ずフィルタしてからOrderByするべき」です。
ソート処理というのは、要素数が増えるほどに「爆発的に計算量が増えます」。
OrderByメソッドは、最初にコレクションの全ての要素を読み込んで、それをソートする処理になります。
よって、100件のソートと10000件のソートではその負荷が段違いです。
ソースのコレクションが膨大な量であるということですが、それこそ先にWhereを通すことで、ソート対象のコレクションを小さくすることが重要なのです。

補足2について。

{最遅5時半、最早4時半、男}で絞り込んだ後、orderbydescendingしてfirstordefaultをとってくるのがよいでしょうか?

Linqを使う場合はこれで正解だと思います。
コレクションの種類次第では、もう少し効率の良い方法があるかもしれませんが。


追記:
1件だけ抽出したい場合、Aggregateを使う方法があります。

csharp

1// 先にフィルタする 2var filterdCollection = originalCollection 3 .Where(x => x.性別 ==) 4 .Where(x => 四時半 < x.時間 && x.時間 <= 五時半); 5 6// 二つの要素を比較して、{ 時間 } が遅いほうを選択 7var result = filteredCollection.Aggregate((x, y) => (x.時間 < y.時間) ? y : x);

投稿2016/12/16 07:29

編集2016/12/16 07:54
tamoto

総合スコア4103

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

syogakusya

2016/12/17 02:32

なるほど、aggregateを試してみます。 ありがとうございました。
tamoto

2016/12/17 03:42

一つ、Aggregateは「コレクションの要素数が0の場合、例外を吐く」ので、フィルタ後のコレクションの要素数に注意して下さい。 要素が1件の場合はその1件をそのまま返してくれます。
syogakusya

2016/12/17 07:05

anyで調べてないときはnullを返すことにします。 1件の場合も別に判定したほうがよいのでしょうか?
tamoto

2016/12/17 08:31

例外になる条件は要素数0の時だけなので、1件以上の場合はそのままAggregateに放り込んで問題ないですよ。
syogakusya

2016/12/17 12:33

よくわかりました。 ありがとうございました。
guest

0

試してみましたか?基本的にリストなら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しない解決方法を必要としている場合はコードを添付してください。

C#

1// コレクションを最初に作る時にソートしておく 2MyCollection = new Collection<MyItem>(DataSource.OrderBy(item => item.Id)); 3 4// 入力されたら前後50の範囲を返す関数を作っとく。 5public static class MyItemExtension 6{ 7 public static IEnumerable<MyItem> Sampling(this IEnumerable<MyItem> items, int threshold) 8 { 9 return items.Where(item => (threshold - 50) <= item.Id && item.Id <= (threshold + 50)); 10 } 11} 12 13// 50~150受け取りたい処理にパス 14SomeFunction(MyCollection.Sampling(100)); 15// 250~350受け取りたい処理にパス 16SomeFunction(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要素だから。 } }

※コレクションによっては最善の方法でキー探索とかしてくれるかもしれませんが、わかりませんしね。。

投稿2016/12/16 06:07

編集2016/12/16 07:26
haru666

総合スコア1591

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

syogakusya

2016/12/16 07:28

回答ありがとうございます。 趣旨がずれてしまい申し訳ございません。 いただいた情報もせっかくなので読み込んで今後に活かしていこうと思います。
guest

0

聞きたいことがころころ変わりすぎじゃんね!(笑)
次からは質問する時に自信がなければ変にやりたいことを自分で要約しすぎないようにしましょう。
そもそも1から10000がミクロも関係ない話題ですねこれは…。

もしもコレクションのソースがデータベースで、キャッシュしておけずに毎回SQL問い合わせが発生するようなケース(Webアプリの場合とか)だったらそもそもLinq使うのやめて全部SQL文で対処しましょう。SQL側にも1件だけデータを取ってくるための構文が大体用意されてます。(SQL Serverだったら TOP 1とかね。)

コレクションが固定のマスタでデータをキャッシュしておけるなら、Linqを使えばいいでしょう。
データベースにアクセスするレイテンシを排除することもできますしね。

キャッシュした場合は実現したい検索処理によってどうしたら高速になるのかは変わってきます。
一番簡単な方法であればコレクションをソート済みで用意しておき、Whereメソッドを必要に応じて繋げていき、最も遅い時間を取ってくるのはLastOrDefaultを、最も早い時間はFirstOrDefaultを使えばいいです。

ソートの必要がないように最初にコレクションを用意しておくのはここでもやはり重要です。
まずは時間順でソートしておきましょう。これはSQLならデータを取ってくる時にしておけばいいです。
コレクションをOrderByしたら時間がかかっても、SQLでのソートはインデックスがあれば一瞬です。

SQL

1SELECT * FROM DATA ORDR BY 時間, 性別;

要素数1万ぐらいならWhereメソッドでフィルターしても全然OKです。
あまりに頻繁に呼ばれるなら色々工夫しなきゃいけなくなりますが、気になるぐらい遅いことはないです。

C#

1IEnumerable<T> collection = MyCollection; 2 3if (/*性別を制限するなら*/) 4{ 5 collection = collection.Where(/*性別の制限*/); 6} 7 8if (/*最小時間を設定するなら*/) 9{ 10 collection = collection.Where(/*最小時間以上*/); 11} 12 13if (/*最大時間を設定するなら*/) 14{ 15 collection = collection.Where(/*最大時間以下*/); 16} 17 18if (/* 最も遅い時間をとってくるなら */) 19{ 20 return collection.LastOrDefault(); 21} 22else 23{ 24 // 最も早い時間をとってくるなら 25 return collection.FirstOrDefault(); 26}

投稿2016/12/16 07:55

編集2016/12/16 07:57
haru666

総合スコア1591

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

効率の点からも先にWhereで絞り込んでからソートすればよいと思います。

C#

1List<int> list = ...; 2int sentinel = 100; 3var sorted = list 4 .Where(i => i >= sentinel - 50 && i <= sentinel + 50) 5 .OrderBy(i => i) 6 .ToList();

Whereが順序を保証しないというのはMSDNにはないようでしたがどこに記載されていましたか?
LINQやJavaのStreamやScalaのSeqなどどれも似たようなものだと思っているのですが、複数のスレッドで並行して処理するようにしない限り要素の順序はパイプラインの前段のまま保存されるのが普通だと思ってました。本当はどうなんでしょう・・・

投稿2016/12/16 06:16

編集2016/12/16 06:21
KSwordOfHaste

総合スコア18392

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問