文字列コレクションを検索する最速の方法


80

問題:

120,000ユーザー(文字列)のテキストファイルがあり、コレクションに保存して、後でそのコレクションを検索します。

検索方法は、ユーザーがaのテキストを変更するたびに発生TextBoxし、結果のテキストを含む文字列なりますTextBox

リストを変更する必要はありませんListBox。結果をプルして、に配置するだけです。

私がこれまでに試したこと:

2つの異なるコレクション/コンテナを試してみました。これらは外部テキストファイルから文字列エントリをダンプしています(もちろん1回)。

  1. List<string> allUsers;
  2. HashSet<string> allUsers;

次のLINQクエリを使用します。

allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();

私の検索イベント(ユーザーが検索テキストを変更すると発生します):

private void textBox_search_TextChanged(object sender, EventArgs e)
{
    if (textBox_search.Text.Length > 2)
    {
        listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();
    }
    else
    {
        listBox_choices.DataSource = null;
    }
}

結果:

どちらも応答時間が遅くなりました(各キーを押す間隔は約1〜3秒)。

質問:

私のボトルネックはどこにあると思いますか?私が使ったコレクションは?検索方法は?どちらも?

どうすればより良いパフォーマンスとより流暢な機能を得ることができますか?


10
HashSet<T>文字列の一部を検索しているので、ここでは役に立ちません。
デニス

8
接尾辞配列を確認してください。
CodesInChaos 2014年

66
文字通り数週間から数年の研究が必要になるため、「最速の方法は何か」と尋ねないでください。むしろ、「30ミリ秒未満で実行されるソリューションが必要です」、またはパフォーマンスの目標が何であれ、と言います。最速のデバイスは必要ありません。十分に高速なデバイスが必要です。
Eric Lippert 2014年

44
また、プロファイラーを取得します。遅い部分がどこにあるについて推測しないでください。そのような推測はしばしば間違っています。ボトルネックはどこか驚くべきものかもしれません。
Eric Lippert 2014年

4
@Basilevs:私はかつて、実際には非常に遅い素敵なO(1)ハッシュテーブルを作成しました。理由を調べるためにプロファイルを作成したところ、検索のたびに、冗談ではなく、レジストリに「今タイにいますか?」というメソッドが呼び出されていることがわかりました。ユーザーがタイにいるかどうかをキャッシュしないことが、そのO(1)コードのボトルネックでした。ボトルネックの場所は、非常に直感に反する可能性があります。プロファイラーを使用します。
Eric Lippert 2014年

回答:


48

バックグラウンドスレッドでフィルタリングタスクを実行して、完了時にコールバックメソッドを呼び出すか、入力が変更された場合にフィルタリングを再開することを検討できます。

一般的な考え方は、次のように使用できるようにすることです。

public partial class YourForm : Form
{
    private readonly BackgroundWordFilter _filter;

    public YourForm()
    {
        InitializeComponent();

        // setup the background worker to return no more than 10 items,
        // and to set ListBox.DataSource when results are ready

        _filter = new BackgroundWordFilter
        (
            items: GetDictionaryItems(),
            maxItemsToMatch: 10,
            callback: results => 
              this.Invoke(new Action(() => listBox_choices.DataSource = results))
        );
    }

    private void textBox_search_TextChanged(object sender, EventArgs e)
    {
        // this will update the background worker's "current entry"
        _filter.SetCurrentEntry(textBox_search.Text);
    }
}

大まかなスケッチは次のようになります。

public class BackgroundWordFilter : IDisposable
{
    private readonly List<string> _items;
    private readonly AutoResetEvent _signal = new AutoResetEvent(false);
    private readonly Thread _workerThread;
    private readonly int _maxItemsToMatch;
    private readonly Action<List<string>> _callback;

    private volatile bool _shouldRun = true;
    private volatile string _currentEntry = null;

    public BackgroundWordFilter(
        List<string> items,
        int maxItemsToMatch,
        Action<List<string>> callback)
    {
        _items = items;
        _callback = callback;
        _maxItemsToMatch = maxItemsToMatch;

        // start the long-lived backgroud thread
        _workerThread = new Thread(WorkerLoop)
        {
            IsBackground = true,
            Priority = ThreadPriority.BelowNormal
        };

        _workerThread.Start();
    }

    public void SetCurrentEntry(string currentEntry)
    {
        // set the current entry and signal the worker thread
        _currentEntry = currentEntry;
        _signal.Set();
    }

    void WorkerLoop()
    {
        while (_shouldRun)
        {
            // wait here until there is a new entry
            _signal.WaitOne();
            if (!_shouldRun)
                return;

            var entry = _currentEntry;
            var results = new List<string>();

            // if there is nothing to process,
            // return an empty list
            if (string.IsNullOrEmpty(entry))
            {
                _callback(results);
                continue;
            }

            // do the search in a for-loop to 
            // allow early termination when current entry
            // is changed on a different thread
            foreach (var i in _items)
            {
                // if matched, add to the list of results
                if (i.Contains(entry))
                    results.Add(i);

                // check if the current entry was updated in the meantime,
                // or we found enough items
                if (entry != _currentEntry || results.Count >= _maxItemsToMatch)
                    break;
            }

            if (entry == _currentEntry)
                _callback(results);
        }
    }

    public void Dispose()
    {
        // we are using AutoResetEvent and a background thread
        // and therefore must dispose it explicitly
        Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (!disposing)
            return;

        // shutdown the thread
        if (_workerThread.IsAlive)
        {
            _shouldRun = false;
            _currentEntry = null;
            _signal.Set();
            _workerThread.Join();
        }

        // if targetting .NET 3.5 or older, we have to
        // use the explicit IDisposable implementation
        (_signal as IDisposable).Dispose();
    }
}

また、_filterFormが破棄されるときに、実際にインスタンスを破棄する必要があります。この開くべきであることを意味し、編集あなたFormDispose(内部のメソッドYourForm.Designer.csファイル)のようなものを見て:

// inside "xxxxxx.Designer.cs"
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        if (_filter != null)
            _filter.Dispose();

        // this part is added by Visual Studio designer
        if (components != null)
            components.Dispose();
    }

    base.Dispose(disposing);
}

私のマシンでは、非常に高速に動作するため、より複雑なソリューションに進む前に、これをテストしてプロファイルする必要があります。

そうは言っても、「より複雑な解決策」は、最後の2つの結果を辞書に保存し、新しいエントリが最後の文字の最初の文字だけ異なることが判明した場合にのみ、それらをフィルタリングすることです。


私はあなたのソリューションをテストしました、そしてそれは完全に機能します!よくできました。私が抱えている唯一の問題は、_signal.Dispose();をコンパイルできないことです(保護レベルに関するエラー)。
etaiso 2014年

@etaiso:それは奇妙です、あなたが呼んでいるexaclty_signal.Dispose()それはBackgroundWordFilterクラスの外のどこかにありますか?
Groo 2014年

1
@Grooこれは明示的な実装です。つまり、直接呼び出すことはできません。usingブロックを使用するか、電話をかけることになっていますWaitHandle.Close()
Matthew Watson

1
わかりました。メソッドは.NET4で公開されました。.NET4のMSDNページでは、パブリックメソッドの下にリストされていますが、.NET 3.5のページでは、保護されたメソッドの下に表示されてます。これは、WaitHandleのMonoソースに条件付き定義がある理由も説明しています。
Groo 2014年

1
@Groo申し訳ありませんが、古いバージョンの.Netについて話していたと言っておく必要がありました。混乱して申し訳ありません。ただし、キャストする必要はないことに注意してください。.Close()代わりに呼び出すことができ、それ自体がを呼び出します.Dispose()
マシューワトソン

36

私はいくつかのテストを行い、120,000アイテムのリストを検索し、新しいリストにエントリを入力するのにかかる時間はごくわずかです(すべての文字列が一致していても、約1/50秒)。

したがって、発生している問題は、データソースの入力に起因している必要があります。

listBox_choices.DataSource = ...

リストボックスに項目を入れすぎているのではないかと思います。

おそらく、次のように、最初の20エントリに制限してみてください。

listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text))
    .Take(20).ToList();

また、(他の人が指摘しているように)のTextBox.Text各アイテムのプロパティにアクセスしていることに注意してくださいallUsers。これは次のように簡単に修正できます。

string target = textBox_search.Text;
listBox_choices.DataSource = allUsers.Where(item => item.Contains(target))
    .Take(20).ToList();

ただし、アクセスにかかる時間をTextBox.Text500,000回計時したところ、0.7秒しかかからず、OPに記載されている1〜3秒よりはるかに短い時間でした。それでも、これは価値のある最適化です。


1
マシューに感謝します。私はあなたの解決策を試しましたが、問題はリストボックスの人口にあるとは思いません。この種のフィルタリングは非常に単純なので、より良いアプローチが必要だと思います(たとえば、「abc」を検索すると0の結果が返され、「abcX」なども検索しないでください。)
etaiso

@etaiso right(すべての一致を事前に設定する必要がない場合は、Matthewのソリューションがうまく機能する可能性があります)。そのため、毎回完全な検索を実行するのではなく、検索を絞り込むための2番目のステップとして提案しました。
アドリアーノレペッティ2014年

5
@etaisoさて、私が言ったように、検索時間はごくわずかです。120,000の文字列で試してみましたが、一致するものがない非常に長い文字列と、多くの一致する非常に短い文字列を検索すると、どちらも1/50秒未満でした。
マシューワトソン

3
ないtextBox_search.Text時間に測定可能な量を貢献?取得Text一度120K列ごとにテキストボックスのプロパティは、おそらく編集コントロールウィンドウに120Kメッセージを送信します。
Gabe 2014年

@Gabeはい、そうです。詳細については私の答えを参照してください。
アンドリス2014年

28

インデックスとしてサフィックスツリーを使用します。または、すべての名前のすべてのサフィックスを対応する名前のリストに関連付けるソートされた辞書を作成するだけです。

入力用:

Abraham
Barbara
Abram

構造は次のようになります。

a -> Barbara
ab -> Abram
abraham -> Abraham
abram -> Abram
am -> Abraham, Abram
aham -> Abraham
ara -> Barbara
arbara -> Barbara
bara -> Barbara
barbara -> Barbara
bram -> Abram
braham -> Abraham
ham -> Abraham
m -> Abraham, Abram
raham -> Abraham
ram -> Abram
rbara -> Barbara

検索アルゴリズム

ユーザー入力「bra」を想定します。

  1. ユーザー入力で辞書を二分して、ユーザー入力またはそれが移動できる位置を見つけます。このようにして、「barbara」(「bra」より下の最後のキー)を見つけます。それは「ブラ」の下限と呼ばれます。検索には対数時間がかかります。
  2. ユーザー入力が一致しなくなるまで、見つかったキーから繰り返します。これにより、「ブラム」->アブラムと「ブラハム」->アブラハムが得られます。
  3. 反復結果(Abram、Abraham)を連結し、出力します。

このようなツリーは、部分文字列をすばやく検索できるように設計されています。パフォーマンスはO(log n)に近いです。このアプローチは、GUIスレッドで直接使用するのに十分な速度で機能すると思います。さらに、同期のオーバーヘッドがないため、スレッドソリューションよりも高速に動作します。


私が知っていることから、接尾辞配列は通常、接尾辞木よりも良い選択です。実装が簡単で、メモリ使用量が少なくなります。
CodesInChaos 2014年

リストの容量を提供することで最小限に抑えることができるメモリオーバーヘッドを犠牲にして、構築と保守が非常に簡単なSortedListを提案します。
バシレウス2014年

また、配列(および元のST)は大きなテキストを処理するように設計されているようですが、ここでは別のタスクである短いチャンクが大量にあります。
バシレウス2014年

良いアプローチには+1ですが、リストを手動で検索するのではなく、ハッシュマップまたは実際の検索ツリーを使用します。
OrangeDog 2014年

プレフィックスツリーの代わりにサフィックスツリーを使用する利点はありますか?
jnovacho 2014年

15

テキスト検索エンジン(Lucene.Netなど)またはデータベース(SQL CESQLiteなどの組み込みエンジンを検討する場合があります)のいずれかが必要です。つまり、インデックス付き検索が必要です。ハッシュベースの検索は部分文字列を検索するため、ここでは適用できませんが、ハッシュベースの検索は正確な値を検索するのに適しています。

それ以外の場合は、コレクションをループする反復検索になります。


インデックス作成ハッシュベースの検索です。値だけでなく、すべてのサブ文字列をキーとして追加するだけです。
OrangeDog 2014年

3
@OrangeDog:同意しません。インデックス付き検索、インデックスキーによるハッシュベースの検索として実装できますが、必須ではなく、文字列値自体によるハッシュベースの検索ではありません。
デニス

@デニス同意します。ゴースト-1をキャンセルするには+1。
ユーザー

テキスト検索エンジンのような実装には、よりもスマートな(より)最適化があるため、+ 1 string.Contains。つまり、bainbcaaaabaaを検索すると、(インデックス付きの)スキップリストが表示されます。最初のものbが考慮されますが、次のものがであるため一致しません。cしたがって、次のものにスキップしbます。
カラミリエル2014年

12

「デバウンス」タイプのイベントがあると便利な場合もあります。これは、イベントを発生させる前に変更が完了するまで一定時間(たとえば、200ミリ秒)待機するという点でスロットルとは異なります。

デバウンスの詳細については、デバウンスとスロットル:視覚的な説明を参照してください。この記事がC#ではなくJavaScriptに焦点を当てていることを感謝しますが、原則は適用されます。

これの利点は、クエリを入力しているときに検索されないことです。その後、一度に2つの検索を実行しようとするのをやめる必要があります。


イベントスロットルのC#実装については、AlgorithmiaライブラリのEventThrotlerクラスを参照してください:github.com/SolutionsDesign/Algorithmia/blob/master/…–
Frans Bouma

11

別のスレッドで検索を実行し、そのスレッドの実行中にロードアニメーションまたはプログレスバーを表示します。

LINQクエリを並列化することもできます。

var queryResults = strings.AsParallel().Where(item => item.Contains("1")).ToList();

AsParallel()のパフォーマンス上の利点を示すベンチマークは次のとおりです。

{
    IEnumerable<string> queryResults;
    bool useParallel = true;

    var strings = new List<string>();

    for (int i = 0; i < 2500000; i++)
        strings.Add(i.ToString());

    var stp = new Stopwatch();

    stp.Start();

    if (useParallel)
        queryResults = strings.AsParallel().Where(item => item.Contains("1")).ToList();
    else
        queryResults = strings.Where(item => item.Contains("1")).ToList();

    stp.Stop();

    Console.WriteLine("useParallel: {0}\r\nTime Elapsed: {1}", useParallel, stp.ElapsedMilliseconds);
}

1
私はそれが可能性があることを知っています。しかし、ここでの私の質問は、このプロセスを短縮できるかどうか、そしてどのように短縮できるかということです。
etaiso 2014年

1
@etaiso本当にローエンドのハードウェアで開発しているのでない限り、実際には問題にはならないはずです。デバッガーを実行していないことを確認してください。CTRL+ F5
animaonline 2014年

1
この方法String.Containsは高価ではないため、これはPLINQの適切な候補ではありません。msdn.microsoft.com/en-us/library/dd997399.aspx
Tim Schmelter 2014年

1
@TimSchmelterたくさんの文字列について話しているときは、そうです!
animaonline 2014年

4
私はあなたが私が提供されたコードを使用して、証明しようとしているもの見当がつかない@TimSchmelter最も可能性が高いOPのパフォーマンスを向上させ、ここではそれがどのように動作するかを示してベンチマークですします:pastebin.com/ATYa2BGt ---時代- -
animaonline

11

更新:

私はいくつかのプロファイリングを行いました。

(アップデート3)

  • リストの内容:0から2.499.999まで生成された数値
  • フィルタテキスト:123(20.477結果)
  • Core i5-2500、Win7 64ビット、8GB RAM
  • VS2012 + JetBrains dotTrace

2.500.000レコードの最初のテスト実行には、20.000ミリ秒かかりました。

一番の犯人はtextBox_search.Text内部への呼びかけContainsです。これにより、各要素get_WindowTextがテキストボックスの高価なメソッドに呼び出されます。コードを次のように変更するだけです。

    var text = textBox_search.Text;
    listBox_choices.DataSource = allUsers.Where(item => item.Contains(text)).ToList();

実行時間を1.858msに短縮しました

アップデート2:

他の2つの重要なボトルネックは、string.Contains(実行時間の約45%)の呼び出しとset_Datasource(30%)のリストボックス要素の更新です。

Basilevsが必要な比較の数を減らし、キーを押した後の検索からファイルからの名前のロードまでの処理時間をプッシュすることを提案しているため、サフィックスツリーを作成することで、速度とメモリ使用量のトレードオフを行うことができます。ユーザーにとっては望ましいかもしれません。

リストボックスへの要素の読み込みのパフォーマンスを向上させるには、最初のいくつかの要素のみを読み込み、さらに利用可能な要素があることをユーザーに示すことをお勧めします。このようにして、利用可能な結果があることをユーザーにフィードバックし、ユーザーがさらに文字を入力して検索を絞り込んだり、ボタンを押すだけで完全なリストを読み込んだりできるようにします。

の実行時間を使用しBeginUpdateEndUpdate変更しませんでしたset_Datasource

他の人がここで指摘しているように、LINQクエリ自体は非常に高速に実行されます。あなたのボトルネックはリストボックス自体の更新だと思います。次のようなことを試すことができます:

if (textBox_search.Text.Length > 2)
{
    listBox_choices.BeginUpdate(); 
    listBox_choices.DataSource = allUsers.Where(item => item.Contains(textBox_search.Text)).ToList();
    listBox_choices.EndUpdate(); 
}

これがお役に立てば幸いです。


これは、アイテムを個別に追加するとき、またはを使用するときに使用することを目的BeginUpdateとしてEndUpdateいるため、何も改善されないと思いますAddRange()
etaiso 2014年

DataSourceプロパティの実装方法によって異なります。試してみる価値があるかもしれません。
アンドリス2014年

あなたのプロファイリング結果は私のものとは大きく異なります。30ミリ秒で120kの文字列を検索できましたが、リストボックスに追加するには4500ミリ秒かかりました。600ミリ秒以内にリストボックスに250万の文字列を追加しているようです。そんなことがあるものか?
Gabe 2014年

@Gabeプロファイリング中に、フィルターテキストが元のリストの大部分を削除する入力を使用しました。フィルタテキストがリストから何も削除しない入力を使用すると、同様の結果が得られます。測定した内容を明確にするために、応答を更新します。
アンドリス2014年

9

プレフィックスのみで照合していると仮定すると、探しているデータ構造はトライと呼ばれ、「プレフィックスツリー」とも呼ばれます。IEnumerable.Whereあなたが今使用していること方法は、各アクセス上の辞書内のすべての項目を反復処理する必要があります。

このスレッドは、C#でトライを作成する方法を示しています。


1
彼が自分のレコードをプレフィックスでフィルタリングしていると仮定します。
タレック2014年

1
彼がString.StartsWith()の代わりにString.Contains()メソッドを使用していることに注意してください。そのため、私たちが探しているものと正確に一致しない可能性があります。それでも-プレフィックスシナリオでStartsWith()拡張機能を使用した通常のフィルタリングよりも、あなたのアイデアは間違いなく優れています。
タレック2014年

彼が最初から始めるという意味であれば、Trieをバックグラウンドワーカーのアプローチと組み合わせてパフォーマンスを向上させることができます
Lyndon White

8

ここでは、WinFormsListBoxコントロールが本当に敵です。レコードの読み込みに時間がかかり、ScrollBarは120,000件すべてのレコードを表示するためにあなたと戦います。

データを保持するために、単一の列[UserName]を持つDataTableにデータソースされた旧式のDataGridViewを使用してみてください。

private DataTable dt;

public Form1() {
  InitializeComponent();

  dt = new DataTable();
  dt.Columns.Add("UserName");
  for (int i = 0; i < 120000; ++i){
    DataRow dr = dt.NewRow();
    dr[0] = "user" + i.ToString();
    dt.Rows.Add(dr);
  }
  dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
  dgv.AllowUserToAddRows = false;
  dgv.AllowUserToDeleteRows = false;
  dgv.RowHeadersVisible = false;
  dgv.DataSource = dt;
}

次に、TextBoxのTextChangedイベントでDataViewを使用して、データをフィルタリングします。

private void textBox1_TextChanged(object sender, EventArgs e) {
  DataView dv = new DataView(dt);
  dv.RowFilter = string.Format("[UserName] LIKE '%{0}%'", textBox1.Text);
  dgv.DataSource = dv;
}

2
+1は、他のすべての人が30ミリ秒しかかからない検索を最適化しようとしていたときに、問題が実際にリストボックスに入力されていることにあることを認識したのはあなただけです。
Gabe 2014年

7

まずListControl、データソースの表示方法を変更します。結果IEnumerable<string>をに変換しList<string>ます。特に、数文字を入力しただけの場合、これは非効率的(および不要)になる可能性があります。データの拡張コピーを作成しないでください

  • .Where()結果を、必要なものだけを実装するコレクションにラップします。IList(検索)ます。これにより、入力する文字ごとに新しい大きなリストを作成する手間が省けます。
  • 別の方法として、LINQを避け、より具体的な(そして最適化された)ものを作成します。リストをメモリに保持し、一致するインデックスの配列を作成し、配列を再利用して、検索ごとにリストを再割り当てする必要がないようにします。

2番目のステップは、小さいリストで十分な場合は大きいリストを検索しないことです。ユーザーが「ab」と入力し始め、「c」を追加すると、大きなリストを調べる必要はありません。フィルターされたリストで検索するだけで十分です(より高速です)。毎回絞り込み検索が可能です。毎回完全検索を実行しないでください。

3番目のステップは難しいかもしれません:データを整理してすばやく検索できるようにします。次に、データの保存に使用する構造を変更する必要があります。このような木を想像してみてください。

ABC
 より良いCeilを追加する
 骨の輪郭の上

これは単純に配列を使用して実装できます(ANSI名を使用している場合は、辞書の方が適しています)。次のようにリストを作成します(図の目的で、文字列の先頭に一致します)。

var dictionary = new Dictionary<char, List<string>>();
foreach (var user in users)
{
    char letter = user[0];
    if (dictionary.Contains(letter))
        dictionary[letter].Add(user);
    else
    {
        var newList = new List<string>();
        newList.Add(user);
        dictionary.Add(letter, newList);
    }
}

次に、最初の文字を使用して検索が実行されます。

char letter = textBox_search.Text[0];
if (dictionary.Contains(letter))
{
    listBox_choices.DataSource =
        new MyListWrapper(dictionary[letter].Where(x => x.Contains(textBox_search.Text)));
}

MyListWrapper()最初のステップで提案されたとおりに使用したことに注意してください(ただし、簡潔にするために2番目の提案では省略しました。辞書キーに適切なサイズを選択した場合は、各リストを短く高速に保つことができます。他のことは避けてください)。さらに、辞書に最初の2文字を使用しようとする場合があることに注意してください(リストが多く、リストが短い)。これを拡張すると、ツリーが作成されます(ただし、それほど多くのアイテムはないと思います)。

文字列検索(関連するデータ構造を含む)にはさまざまなアルゴリズムがありますが、ほんの数例です。

  • 有限状態オートマトンベースの検索:このアプローチでは、保存された検索文字列を認識する決定性有限オートマトン(DFA)を構築することにより、バックトラックを回避します。これらは構築に費用がかかります(通常はパワーセット構造を使用して作成されます)が、非常にすばやく使用できます。
  • スタブ:Knuth–Morris–Prattは、検索する文字列を接尾辞として入力を認識するDFAを計算します。Boyer–Mooreは針の端から検索を開始するため、通常、各ステップで針の長さ全体をジャンプできます。Baeza–Yatesは、前のj文字が検索文字列のプレフィックスであったかどうかを追跡するため、あいまい文字列検索に適応できます。bitapアルゴリズムは、Baeza–Yatesのアプローチを応用したものです。
  • インデックス作成方法:より高速な検索アルゴリズムは、テキストの前処理に基づいています。接尾辞ツリーや接尾辞配列などの部分文字列インデックスを作成した後、パターンの出現をすばやく見つけることができます。
  • その他のバリエーション:トライグラム検索などの一部の検索方法は、「一致/不一致」ではなく、検索文字列とテキストの間の「近さ」スコアを見つけることを目的としています。これらは「あいまい」検索と呼ばれることもあります。

並列検索についてのいくつかの単語。可能ですが、並列化するためのオーバーヘッドは検索自体よりもはるかに高くなる可能性があるため、簡単なことではありません。検索自体を並行して実行することはしませんが(パーティション分割と同期はすぐに拡張しすぎて複雑になる可能性があります)、検索を別のスレッドに移動します。メインスレッドがビジーでない場合、ユーザーは入力中に遅延を感じることはありません(200ミリ秒後にリストが表示されるかどうかはわかりませんが、入力後50ミリ秒待たなければならない場合は不快に感じます) 。もちろん、検索自体は十分に高速である必要があります。この場合、スレッドを使用して検索を高速化するのではなく、UIの応答性維持します別のスレッドではクエリが作成されないことに注意してくださいより高速で、UIはハングしませんが、クエリが遅い場合でも、別のスレッドでは遅くなります(さらに、複数の順次リクエストも処理する必要があります)。


1
一部の人がすでに指摘しているように、OPは結果をプレフィックスのみに制限することを望んでいません(つまり、彼はContains、ではなくを使用しますStartsWith)。ContainsKeyちなみに、ボックス化を回避するためにキーを検索するときは通常、汎用メソッドを使用TryGetValueする方が適切であり、2回のルックアップを回避するために使用する方がさらに適切です。
Groo 2014年

2
@Grooあなたは正しいです、私が言ったように、それは説明の目的のためだけです。そのコードのポイントは実用的な解決策ではありませんが、ヒントです。他のすべてを試した場合(コピーを避け、検索を絞り込み、別のスレッドに移動します)、それだけでは不十分な場合は、使用しているデータ構造を変更する必要があります。例は、単純にするために文字列を開始する場合です。
アドリアーノレペッティ2014年

@Adriano明確で詳細な回答をありがとう!私はあなたが言及したほとんどのことに同意しますが、Grooが言ったように、データを整理する最後の部分は私の場合には当てはまりません。しかし、含まれている文字と同様のキーを備えた辞書を保持していると思います(ただし、重複はまだあります)
etaiso 2014年

簡単なチェックと計算の後、「含まれている文字」のアイデアは1文字だけには適していません(2つ以上の組み合わせを選択すると、非常に大きなハッシュテーブルになります)
etaiso 2014年

@etaisoはい、(サブリストをすばやく減らすために)2文字のリストを保持できますが、実際のツリーの方がうまくいく場合があります(各文字は、文字列内のどこにあるかに関係なく、後続の文字にリンクされているため、「HOME」の場合は「H-> O」、「O-> M」、「M-> E」。「om」を検索するとすぐに見つかります。問題は、かなり複雑になり、多すぎる可能性があることです。 for youシナリオ(IMO)
Adriano Repetti 2014年

4

PLINQ(Parallel LINQ)を使用してみてください。これはスピードブーストを保証するものではありませんが、これは試行錯誤によって見つける必要があります。


4

私はあなたがそれをより速くすることができるとは思わないが、確かにあなたはすべきである:

A)AsParallel使用LINQの拡張メソッドを

a)フィルタリングを遅らせるためにある種のタイマーを使用する

b)別のスレッドにフィルタリングメソッドを配置します

string previousTextBoxValueどこかに保管してください。1000ミリ秒の遅延でタイマーを作成previousTextBoxValueしますtextbox.Text。これは、値と同じ場合にティックで検索を開始します。そうでない場合-previousTextBoxValue現在の値に再割り当てし、タイマーをリセットします。タイマーの開始をテキストボックスの変更されたイベントに設定すると、アプリケーションがスムーズになります。1〜3秒で120,000レコードをフィルタリングすることは問題ありませんが、UIは応答性を維持する必要があります。


1
私はそれを並行させることに同意しませんが、他の2つの点には絶対に同意します。UI要件を満たすだけでも十分な場合があります。
アドリアーノレペッティ2014年

言及するのを忘れましたが、私は.NET 3.5を使用しているため、AsParallelはオプションではありません。
etaiso 2014年

3

BindingSource.Filter関数を使用してみることもできます。私はそれを使用しました、そしてそれは検索されているテキストでこのプロパティを更新するたびに、たくさんのレコードからフィルタリングするのが魅力のように機能します。もう1つのオプションは、TextBoxコントロールにAutoCompleteSourceを使用することです。

それが役に立てば幸い!


2

コレクションを並べ替え、開始部分のみに一致するように検索し、検索をいくつかの数で制限しようとします。

など初期化

allUsers.Sort();

と検索

allUsers.Where(item => item.StartWith(textBox_search.Text))

たぶん、あなたはいくつかのキャッシュを追加することができます。


1
彼は文字列の先頭で作業していません(そのため、彼はString.Contains()を使用しています)。contains()を使用すると、ソートされたリストはパフォーマンスを変更しません。
アドリアーノレペッティ2014年

はい、「含む」では役に立ちません。私は接尾辞ツリーの提案が好きですstackoverflow.com/a/21383731/994849スレッドには興味深い答えがたくさんありますが、それは彼がこのタスクに費やすことができる時間によって異なります。
hardsky 2014年

1

Parallelを使用しLINQます。PLINQLINQ toObjectsの並列実装です。PLINQは、T:System.Linq名前空間の拡張メソッドとしてLINQ標準クエリ演算子のフルセットを実装し、並列操作用の追加の演算子を備えています。PLINQは、LINQ構文の単純さと読みやすさを並列プログラミングの力と組み合わせています。タスク並列ライブラリを対象とするコードと同様に、PLINQクエリは、ホストコンピューターの機能に基づいて並行性の程度に応じてスケーリングします。

PLINQの紹介

PLINQのスピードアップを理解する

Lucene.Netも使用できます

Lucene.Netは、C#で記述され、.NETランタイムユーザーを対象としたLucene検索エンジンライブラリの移植版です。Lucene検索ライブラリは、転置インデックスに基づいています。Lucene.Netには3つの主要な目標があります。


1

私が見たものによると、私はリストをソートするという事実に同意します。

ただし、リストが構成されているときに並べ替えるのは非常に遅くなります。作成時に並べ替えると、実行時間が短縮されます。

それ以外の場合、リストを表示したり順序を維持したりする必要がない場合は、ハッシュマップを使用してください。

ハッシュマップは文字列をハッシュし、正確なオフセットで検索します。もっと速いはずだと思います。


どのキーのハッシュマップ?文字列に含まれるキーワードを見つけられるようにしたい。
etaiso 2014年

キーの場合は、リストに番号を入力できます。より複雑なものが必要な場合は、番号と名前を追加できます。選択はあなた次第です。
dada 2014年

残りの部分については、すべてを読んでいないか、悪い説明がありました(おそらく両方;))[quote]コレクションに保存して後で実行したい約120,000ユーザー(文字列)のテキストファイルがありますそのコレクションを検索します。[/ quote]単なる文字列検索だと思いました。
dada 2014年

1

BinarySearchメソッドを使用してみてください。Containsメソッドよりも高速に動作するはずです。

含まれるのはO(n)ですBinarySearchはO(lg(n))です

並べ替えられたコレクションは、検索では速く、新しい要素の追加では遅くなるはずだと思いますが、私が理解したように、検索パフォーマンスの問題しかありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.