문자열 컬렉션에서 검색하는 가장 빠른 방법


80

문제:

컬렉션에 저장하고 나중에 해당 컬렉션에 대해 검색을 수행하려는 약 120,000 명의 사용자 (문자열) 의 텍스트 파일 이 있습니다.

검색 방법은 사용자가 a의 텍스트를 변경할 때마다 발생 TextBox하며 결과는 의 텍스트 를 포함 하는 문자열이어야합니다 TextBox.

목록을 변경할 필요가 없습니다. 결과를 가져 와서 ListBox.

지금까지 시도한 것 :

두 개의 다른 컬렉션 / 컨테이너로 시도했는데, 외부 텍스트 파일에서 문자열 항목을 덤프합니다 (물론 한 번).

  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>문자열 의 일부 를 검색하기 때문에 여기서는 도움이되지 않습니다 .
Dennis

8

66
"가장 빠른 방법"을 묻지 마십시오. 문자 그대로 몇 주에서 몇 년의 연구가 필요하기 때문입니다. 대신 "30ms 미만으로 실행되는 솔루션이 필요합니다."라고 말하거나 성능 목표가 무엇이든 말하십시오. 가장 빠른 장치 가 필요하지 않고 충분히 빠른 장치가 필요합니다 .
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();
    }
}

또한 _filter부모 Form가 삭제 될 때 실제로 인스턴스 를 삭제해야합니다 . 즉, 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);
}

내 컴퓨터에서는 매우 빠르게 작동하므로 더 복잡한 솔루션을 찾기 전에이를 테스트하고 프로파일 링해야합니다.

즉, "더 복잡한 솔루션"은 마지막 두 개의 결과를 사전에 저장 한 다음 새 항목이 마지막 문자의 첫 번째 문자 만 다른 것으로 밝혀진 경우에만 필터링하는 것입니다.


방금 귀하의 솔루션을 테스트했으며 완벽하게 작동합니다! 잘하셨습니다. 내가 가진 유일한 문제는 _signal.Dispose();컴파일 할 수 없다는 것입니다 (보호 수준에 대한 오류).
etaiso 2014.01.27

@etaiso : 그게 이상 해요, 당신이 부르는 exaclty _signal.Dispose()BackgroundWordFilter수업 밖의 어딘가에 있습니까?
Groo

1
@Groo 명시 적 구현이므로 직접 호출 할 수 없습니다. 당신은 using블록 을 사용 하거나 호출해야합니다WaitHandle.Close()
Matthew Watson

1
자, 이제이 방법이 .NET 4에서 공개되었습니다. .NET 4 용 MSDN 페이지는 공개 방법 아래에 나열 되고 .NET 3.5 페이지는 보호 된 방법 아래에 표시 됩니다. 또한 WaitHandleMono 소스에 조건부 정의가있는 이유도 설명합니다 .
Groo 2014 년

1
@Groo 죄송합니다. 이전 버전의 .Net에 대해 언급 했어야했습니다. 혼동을 드려 죄송합니다! 그러나 그는 캐스팅 할 필요가 없습니다 . .Close()대신 호출 할 수 있습니다 .Dispose().
Matthew Watson

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.Text50 만 번 액세스하는 데 걸리는 시간을 측정 했으며 OP에 언급 된 1-3 초보다 훨씬 적은 0.7 초 밖에 걸리지 않았습니다. 그래도 이것은 가치있는 최적화입니다.


1
고마워요 매튜. 나는 당신의 해결책을 시도했지만 문제가 ListBox의 인구에 있다고 생각하지 않습니다. 이런 종류의 필터링이 매우 순진하기 때문에 더 나은 접근 방식이 필요하다고 생각합니다 (예 : "abc"를 검색하면 0 개의 결과가 반환되고 "abcX"등도 검색하지 않아야합니다.).)
etaiso

@etaiso 맞습니다 (모든 일치 항목을 미리 설정할 필요가없는 경우 Matthew의 솔루션이 훌륭하게 작동하더라도) 그래서 매번 전체 검색을 수행하는 대신 검색 을 구체화 하는 두 번째 단계로 제안 했습니다 .
Adriano Repetti 2014 년

5
@etaiso 글쎄, 내가 말했듯이 검색 시간은 무시할 수 있습니다. 120,000 개의 문자열로 시도해 보았고 일치하지 않는 매우 긴 문자열과 많은 일치를 제공하는 매우 짧은 문자열을 검색하는 데 모두 1/50 초 미만이 걸렸습니다.
Matthew Watson

3
textBox_search.Text시간에 측정 가능한 금액을 기여 합니까 ? Text120k 문자열 각각에 대해 한 번씩 텍스트 상자 에서 속성을 가져 오면 편집 제어 창에 120k 메시지를 보낼 수 있습니다.
Gabe

@Gabe 네 그렇습니다. 자세한 내용은 내 대답을 참조하십시오.
Andris

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. 사용자 입력이 더 이상 일치하지 않을 때까지 찾은 키부터 계속 반복합니다. 이것은 "bram"-> Abram 및 "braham"-> Abraham을 줄 것입니다.
  3. 반복 결과 (Abram, Abraham)를 연결하고 출력합니다.

이러한 트리는 하위 문자열을 빠르게 검색하도록 설계되었습니다. 성능은 O (log n)에 가깝습니다. 이 접근 방식은 GUI 스레드에서 직접 사용할 수있을만큼 빠르게 작동 할 것이라고 생각합니다. 또한 동기화 오버 헤드가 없기 때문에 스레드 솔루션보다 빠르게 작동합니다.


내가 아는 접미사 배열은 일반적으로 접미사 트리보다 더 나은 선택입니다. 구현하기 쉽고 메모리 사용량을 줄입니다.
CodesInChaos 2014 년

목록 용량을 제공하여 최소화 할 수있는 메모리 오버 헤드 비용으로 구축 및 유지 관리가 매우 쉬운 SortedList를 제안합니다.
Basilevs 2014 년

또한 배열 (및 원본 ST)은 큰 텍스트를 처리하도록 설계되었지만 여기에는 다른 작업 인 많은 양의 짧은 청크가 있습니다.
Basilevs 2014 년

좋은 접근 방식을 위해 +1하지만 수동으로 목록을 검색하는 대신 해시 맵이나 실제 검색 트리를 사용합니다.
OrangeDog

접두사 트리 대신 접미사 트리를 사용하면 어떤 이점이 있습니까?
jnovacho 2014 년

15

텍스트 검색 엔진 (예 : Lucene.Net ) 또는 데이터베이스 (예 : SQL CE , SQLite 등 의 임베디드 엔진을 고려할 수 있음 )가 필요합니다. 즉, 색인화 된 검색이 필요합니다. 해시 기반 검색은 하위 문자열을 검색하기 때문에 여기서 적용 할 수 없지만 해시 기반 검색은 정확한 값을 검색하는 데 적합합니다.

그렇지 않으면 컬렉션을 반복하는 반복 검색이됩니다.


인덱싱 해시 기반 검색입니다. 값 대신 모든 하위 문자열을 키로 추가하기 만하면됩니다.
OrangeDog

3
@OrangeDog : 동의하지 않습니다. 인덱싱 된 검색 인덱스 키에 의한 해시 기반 검색으로 구현 수 있지만 필요하지 않으며 문자열 값 자체에 의한 해시 기반 검색이 아닙니다.
Dennis

@Dennis 동의합니다. 유령을 취소하려면 +1 -1.
사용자

+1 텍스트 검색 엔진과 같은 구현에는 string.Contains. 즉. bain bcaaaabaa을 검색 하면 (인덱싱 된) 건너 뛰기 목록이 생성됩니다. 첫 번째 b는 고려되지만 다음은이므로 일치하지 않으므로 다음으로 c건너 뜁니다 b.
Caramiriel 2014 년

12

"디 바운스"유형의 이벤트를 갖는 것도 유용 할 수 있습니다. 이벤트를 시작하기 전에 변경이 완료 될 때까지 일정 시간 (예 : 200ms)을 기다린다는 점에서 스로틀 링과 다릅니다.

참조 디 바운스 및 스로틀 : 시각적 설명 디 바운싱에 대한 자세한 정보를 얻을 수 있습니다. 이 기사는 C # 대신 JavaScript에 초점을 맞추고 있지만 원칙이 적용됩니다.

이것의 장점은 여전히 ​​쿼리를 입력 할 때 검색하지 않는다는 것입니다. 그런 다음 한 번에 두 개의 검색을 수행하려는 시도를 중지해야합니다.


Algorithmia 라이브러리의 EventThrotler 클래스 throttler 이벤트의 C #을 구현 참조 : github.com/SolutionsDesign/Algorithmia/blob/master/...
프랑스어 보우마

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

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.000ms가 소요되었습니다.

가장 큰 범인은 textBox_search.Text내부에 대한 호출 Contains입니다. 이렇게하면 get_WindowText텍스트 상자 의 값 비싼 메서드에 대한 각 요소가 호출됩니다 . 코드를 다음과 같이 변경하기 만하면됩니다.

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

실행 시간을 1.858ms줄였습니다 .

업데이트 2 :

다른 두 가지 중요한 병목 현상은 이제 호출 string.Contains(실행 시간의 약 45 %)과 목록 상자 요소의 업데이트 set_Datasource(30 %)입니다.

Basilevs가 필요한 비교 횟수를 줄이고 키를 누른 후 검색에서 처리 시간을 파일에서 이름을로드 할 때까지 푸시하도록 제안했듯이 Suffix 트리를 생성하여 속도와 메모리 사용량 사이의 균형을 맞출 수 있습니다. 사용자에게 바람직 할 수 있습니다.

목록 상자에 요소를로드하는 성능을 높이려면 처음 몇 개의 요소 만로드하고 사용 가능한 추가 요소가 있음을 사용자에게 표시하는 것이 좋습니다. 이렇게하면 사용자에게 사용 가능한 결과가 있다는 피드백을 제공하여 더 많은 문자를 입력하여 검색을 구체화하거나 버튼을 눌러 전체 목록을로드 할 수 있습니다.

를 사용 BeginUpdate하고 EndUpdate의 실행 시간을 변경하지 않았습니다 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(); 
}

이게 도움이 되길 바란다.


나는이이 같은 아무것도 개선 할 생각하지 않습니다 BeginUpdateEndUpdate개별적으로 항목을 추가 할 때 또는 사용할 때 사용하기위한 것입니다 AddRange().
etaiso 2014 년

DataSource속성이 구현 되는 방식에 따라 다릅니다 . 시도해 볼 가치가 있습니다.
Andris

귀하의 프로파일 링 결과는 저와 매우 다릅니다. 30ms 안에 120k 문자열을 검색 할 수 있었지만 목록 상자에 추가하는 데 4500ms가 걸렸습니다. 600ms 이내에 목록 상자에 2.5M 문자열을 추가하는 것 같습니다. 어떻게 가능합니까?
Gabe 2014 년

@Gabe 프로파일 링하는 동안 필터 텍스트가 원래 목록의 큰 부분을 제거하는 입력을 사용했습니다. 필터 텍스트가 목록에서 아무것도 제거하지 않는 입력을 사용하면 비슷한 결과가 나타납니다. 제가 측정 한 내용을 명확히하기 위해 답변을 업데이트하겠습니다.
안드리

9

접두사로만 일치한다고 가정하면 찾고있는 데이터 구조를 "접두사 트리"라고도 하는 trie 라고합니다 . 그만큼IEnumerable.Where지금 사용하고 있는지 방법은 각 액세스에 대한 당신의 사전에있는 모든 항목을 반복하는 것입니다.

이 스레드 는 C #에서 트라이를 만드는 방법을 보여줍니다.


1
그가 접두사로 그의 레코드를 필터링한다고 가정합니다.
Tarec 2014 년

1
그는 String.StartsWith () 대신 String.Contains () 메서드를 사용하고 있으므로 정확히 우리가 찾고있는 것이 아닐 수도 있습니다. 여전히-당신의 아이디어는 접두사 시나리오에서 StartsWith () 확장을 사용한 일반적인 필터링보다 의심 할 여지없이 낫습니다.
Tarec 2014-01-27

그가 의미하는 것이 시작한다면, Trie는 성능 향상을 위해 백그라운드 작업자 접근 방식과 결합 될 수 있습니다
Lyndon White

8

WinForms ListBox 컨트롤은 실제로 여기에서 적입니다. 레코드를로드하는 속도가 느리며 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 다른 사람들이 30ms 밖에 걸리지 않는 검색을 최적화하려고했지만 실제로 목록 상자를 채우는 데 문제가 있음을 인식 한 사람은 당신뿐입니다.
Gabe

7

먼저 ListControl데이터 소스를 보는 방법을 변경 하고 결과 IEnumerable<string>List<string>. 특히 몇 개의 문자를 입력 한 경우 비효율적 일 수 있습니다 (필요하지 않음). 데이터의 광범위한 사본을 만들지 마십시오 .

  • .Where()결과를 IList(검색) 에서 필요한 것만 구현하는 컬렉션으로 래핑 합니다 . 이렇게하면 입력 된 각 문자에 대한 새로운 큰 목록을 만들 수 있습니다.
  • 대안으로 LINQ를 피하고 더 구체적이고 최적화 된 것을 작성합니다. 목록을 메모리에 유지하고 일치하는 인덱스 배열을 만들고 배열을 재사용하여 각 검색에 대해 다시 할당 할 필요가 없습니다.

두 번째 단계는 작은 목록으로 충분할 때 큰 목록에서 검색하지 않는 것입니다. 사용자가 "ab"를 입력하기 시작하고 "c"를 추가하면 큰 목록에서 조사 할 필요가 없습니다. 필터링 된 목록에서 검색하면 충분합니다 (더 빠릅니다). 상세 검색매번 이 가능하며 매번 전체 검색을 수행하지 마십시오.

세 번째 단계는 더 어려울 수 있습니다 . 빠르게 검색 할 수 있도록 데이터를 정리하십시오 . 이제 데이터를 저장하는 데 사용하는 구조를 변경해야합니다. 다음과 같은 나무를 상상해보십시오.

알파벳
 더 나은 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()첫 번째 단계에서 제안한대로 사용 했습니다 (하지만 간결성을 위해 두 번째 제안에서 생략했습니다. 사전 키에 맞는 크기를 선택하면 각 목록을 짧고 빠르게 유지하여 다른 것은 피할 수 있습니다). 또한 사전에 처음 두 문자를 사용하려고 할 수 있습니다 (목록이 많고 짧음). 이것을 확장하면 나무가 생깁니다 (하지만 그렇게 많은 수의 항목이 있다고 생각하지 않습니다).

문자열 검색을위한 다양한 알고리즘 이 있습니다 (관련 데이터 구조 포함).

  • 유한 상태 오토 마톤 기반 검색 :이 접근 방식에서는 저장된 검색 문자열을 인식하는 결정 론적 유한 오토 마톤 (DFA)을 구성하여 역 추적을 방지합니다. 이들은 구성하는 데 비용이 많이 들지만 일반적으로 powerset 구성을 사용하여 생성되지만 사용이 매우 빠릅니다.
  • 스텁 : Knuth–Morris–Pratt는 검색 할 문자열이있는 입력을 접미사로 인식하는 DFA를 계산합니다. Boyer–Moore는 바늘 끝부터 검색을 시작하므로 일반적으로 각 단계에서 전체 바늘 길이만큼 앞으로 이동할 수 있습니다. Baeza–Yates는 이전 j 문자가 검색 문자열의 접두사 였는지 여부를 추적하므로 퍼지 문자열 검색에 적용 할 수 있습니다. bitap 알고리즘은 Baeza–Yates의 접근 방식을 적용한 것입니다.
  • 색인 방법 : 더 빠른 검색 알고리즘은 텍스트의 전처리를 기반으로합니다. 예를 들어 접미사 트리 또는 접미사 배열과 같은 하위 문자열 인덱스를 빌드 한 후 패턴 발생을 빠르게 찾을 수 있습니다.
  • 기타 변형 : 트라이 그램 검색과 같은 일부 검색 방법은 "일치 / 불일치"보다는 검색 문자열과 텍스트 사이의 "가까움"점수를 찾기위한 것입니다. 이를 "퍼지"검색이라고도합니다.

병렬 검색에 대한 몇 마디. 가능하지만 병렬로 만들기위한 오버 헤드가 검색 자체보다 훨씬 높을 수 있기 때문에 사소한 일이 아닙니다. 검색 자체를 병렬로 수행하지는 않지만 (파티션 및 동기화가 곧 너무 확장되고 복잡해질 수 있음) 검색을 별도의 스레드로 이동합니다 . 주 스레드가 바쁘지 않으면 사용자가 입력하는 동안 지연을 느끼지 않을 것입니다 (200ms 후에 목록이 표시되는지 여부는 기록하지 않지만 입력 한 후 50ms를 기다려야하는 경우 불편 함을 느낄 것입니다) . 물론 검색 자체가 충분히 빨라야합니다.이 경우 검색 속도를 높이기 위해 스레드를 사용하지 않고 UI 응답 성유지합니다 . 쿼리를 UI가 중단되지 않지만 쿼리가 느린 경우 별도의 스레드에서 여전히 느립니다 (또한 여러 개의 순차적 요청도 처리해야 함).


1
일부가 이미 지적했듯이 OP는 결과를 접두사로만 제한하고 싶지 않습니다 (예 : 그는 사용 Contains하지 않고 를 사용합니다 StartsWith). 참고로, 일반적으로 ContainsKey복싱을 피하기 위해 키를 검색 할 때 일반적인 방법 을 사용하는 것이 더 좋으며 TryGetValue두 번 조회를 피하는 데 사용 하는 것이 더 좋습니다 .
Groo

2
@Groo 당신 말이 맞아요. 이 코드의 요점은 작동하는 솔루션이 아니라 힌트입니다. 복사를 피하고 검색을 구체화하고 다른 스레드로 이동하는 등 다른 모든 작업을 시도한 경우 충분하지 않으면 사용중인 데이터 구조를 변경해야합니다. . 예는 단순하게 유지하기 위해 문자열의 시작입니다.
Adriano Repetti 2014 년

@Adriano 명확하고 상세한 답변에 감사드립니다! 나는 당신이 언급 한 대부분의 것에 동의하지만 Groo가 말했듯이 데이터를 정리하는 마지막 부분은 제 경우에는 적용되지 않습니다. 하지만 포함 된 문자와 유사한 사전을 보유하고있을 수 있다고 생각합니다 (여전히 중복이있을 수 있음)
etaiso

빠른 확인과 계산 후에 "포함 된 문자"아이디어는 한 문자에만 적합하지 않습니다 (두 개 이상의 조합을 사용하면 매우 큰 해시 테이블이 생성됩니다)
etaiso

@etaiso 예, 두 글자의 목록을 유지할 수 있지만 (하위 목록을 빠르게 줄이기 위해) 진정한 트리가 더 잘 작동 할 수 있습니다 (문자열 내부의 위치에 관계없이 각 글자가 후속 문자에 연결되므로 "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어딘가에 보관하십시오 . 1000ms의 지연으로 타이머를 만드십시오 .이 값이 값 previousTextBoxValue과 같으면 틱에서 검색을 시작 textbox.Text합니다. 그렇지 않은 경우- previousTextBoxValue현재 값으로 다시 할당 하고 타이머를 재설정합니다. 타이머 시작을 텍스트 상자 변경 이벤트로 설정하면 응용 프로그램이 더 부드러워집니다. 1-3 초에 120,000 개의 레코드를 필터링하는 것은 괜찮지 만 UI는 응답 성을 유지해야합니다.


1
나는 그것을 평행하게 만드는 데 동의하지 않지만 다른 두 가지 점에 절대적으로 동의합니다. UI 요구 사항을 충족하는 것으로도 충분할 수 있습니다.
Adriano Repetti 2014 년

언급하는 것을 잊었지만 .NET 3.5를 사용하고 있으므로 AsParallel은 옵션이 아닙니다.
etaiso

3

BindingSource.Filter 함수를 사용해 볼 수도 있습니다 . 나는 그것을 사용했고 그것은 검색되는 텍스트 로이 속성을 업데이트 할 때마다 많은 레코드에서 필터링하는 매력처럼 작동합니다. 또 다른 옵션은 TextBox 컨트롤에 AutoCompleteSource 를 사용하는 것 입니다.

도움이 되었기를 바랍니다.


2

컬렉션을 정렬하고 시작 부분 만 일치하도록 검색하고 일부 수로 검색을 제한하려고합니다.

그래서 초기화

allUsers.Sort();

및 검색

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

캐시를 추가 할 수 있습니다.


1
그는 문자열의 시작 부분으로 작업하지 않습니다 (그것이 String.Contains ()를 사용하는 이유입니다). Contains ()를 사용하면 정렬 된 목록이 성능을 변경하지 않습니다.
Adriano Repetti 2014 년

예, '포함'을 사용하면 쓸모가 없습니다. 나는 접미사 트리를 사용한 제안을 좋아합니다. stackoverflow.com/a/21383731/994849 스레드에는 흥미로운 답변이 많이 있지만이 작업에 얼마나 많은 시간을 할애 할 수 있는지에 따라 다릅니다.
hardsky 2014 년

1

병렬을 사용하십시오 LINQ. PLINQLINQ to Objects의 병렬 구현입니다. PLINQ는 T : System.Linq 네임 스페이스에 대한 확장 메서드로 LINQ 표준 쿼리 연산자의 전체 집합을 구현하고 병렬 작업을위한 추가 연산자를 가지고 있습니다. PLINQ는 LINQ 구문의 단순성과 가독성을 병렬 프로그래밍의 힘과 결합합니다. 작업 병렬 라이브러리를 대상으로하는 코드와 마찬가지로 PLINQ 쿼리는 호스트 컴퓨터의 기능에 따라 동시성 정도가 확장됩니다.

PLINQ 소개

PLINQ의 속도 향상 이해

또한 Lucene.Net 을 사용할 수 있습니다 .

Lucene.Net은 C #으로 작성되고 .NET 런타임 사용자를 대상으로하는 Lucene 검색 엔진 라이브러리의 포트입니다. Lucene 검색 라이브러리는 반전 된 색인을 기반으로합니다. Lucene.Net에는 세 가지 주요 목표가 있습니다.


1

내가 본 것에 따르면 나는 목록을 정렬한다는 사실에 동의합니다.

그러나 목록이 구조 일 때 정렬하는 것은 매우 느릴 것이며 빌드 할 때 정렬하면 더 나은 실행 시간을 갖게됩니다.

그렇지 않으면 목록을 표시하거나 순서를 유지할 필요가없는 경우 해시 맵을 사용하십시오.

해시 맵은 문자열을 해시하고 정확한 오프셋에서 검색합니다. 더 빨라야한다고 생각합니다.


어떤 키가있는 해시 맵? 문자열에 포함 된 키워드를 찾고 싶습니다.
etaiso

키의 경우 목록에 번호를 넣을 수 있으며, 더 많은 정보를 원하면 번호와 이름을 추가 할 수 있습니다.
dada 2014 년

나머지를 위해 나는 모든 것을 읽지 않았거나 나쁜 설명이 있었거나 (아마 둘 다;)) [따옴표] 컬렉션에 저장하고 나중에 수행하고 싶은 약 120,000 명의 사용자 (문자열)의 텍스트 파일이 있습니다. 그 컬렉션을 검색합니다. 나는 그것이 단지 문자열 검색이라고 생각했다.
dada 2014 년

1

BinarySearch 메서드를 사용하면 Contains 메서드보다 빠르게 작동합니다.

포함은 O (n)입니다. BinarySearch는 O (lg (n))입니다.

정렬 된 컬렉션은 검색에서 더 빨리 작동하고 새 요소를 추가 할 때는 더 느리게 작동해야한다고 생각하지만 검색 성능 문제 만 있다는 것을 이해했습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.