Cách nhanh nhất để tìm kiếm trong tập hợp chuỗi


80

Vấn đề:

Tôi có một tệp văn bản của khoảng 120.000 người dùng (chuỗi) mà tôi muốn lưu trữ trong một bộ sưu tập và sau đó để thực hiện tìm kiếm trên bộ sưu tập đó.

Phương thức tìm kiếm sẽ xảy ra mỗi khi người dùng thay đổi văn bản của a TextBoxvà kết quả phải là các chuỗi có chứa văn bản trong đó TextBox.

Tôi không phải thay đổi danh sách, chỉ cần kéo kết quả và đưa chúng vào a ListBox.

Những gì tôi đã thử cho đến nay:

Tôi đã thử với hai bộ sưu tập / vùng chứa khác nhau, tôi đang kết xuất các mục nhập chuỗi từ tệp văn bản bên ngoài (tất nhiên là một lần):

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

Với truy vấn LINQ sau :

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

Sự kiện tìm kiếm của tôi (kích hoạt khi người dùng thay đổi văn bản tìm kiếm):

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;
    }
}

Các kết quả:

Cả hai đều cho tôi một thời gian phản hồi kém (khoảng 1-3 giây giữa mỗi lần nhấn phím).

Câu hỏi:

Bạn nghĩ nút thắt cổ chai của tôi nằm ở đâu? Bộ sưu tập tôi đã sử dụng? Các phương pháp tìm kiếm? Cả hai?

Làm cách nào để có được hiệu suất tốt hơn và chức năng trôi chảy hơn?


10
HashSet<T>sẽ không giúp bạn ở đây, vì bạn đang tìm kiếm một phần của chuỗi.
Dennis


66
Đừng hỏi "cách nhanh nhất để đến là gì", bởi vì điều đó sẽ mất hàng tuần đến hàng năm trời để nghiên cứu. Thay vào đó, hãy nói "Tôi cần một giải pháp chạy trong vòng chưa đầy 30 mili giây" hoặc bất kể mục tiêu hiệu suất của bạn là gì. Bạn không cần thiết bị nhanh nhất , bạn cần một thiết bị đủ nhanh .
Eric Lippert

44
Ngoài ra, hãy lấy một hồ sơ cá nhân . Đừng đoán về vị trí của phần chậm; những phỏng đoán như vậy thường sai. Điểm nghẽn có thể nằm ở đâu đó đáng ngạc nhiên.
Eric Lippert

4
@Basilevs: Tôi đã từng viết một bảng băm O (1) rất đáng yêu trong thực tế. Tôi đã phân tích hồ sơ để tìm hiểu lý do và phát hiện ra rằng trong mỗi lần tìm kiếm, nó gọi một phương thức - không đùa - cuối cùng đã hỏi cơ quan đăng ký "hiện tại chúng tôi đang ở Thái Lan phải không?". Không lưu vào bộ nhớ đệm cho dù người dùng có ở Thái Lan hay không là điểm nghẽn trong mã O (1) đó. Vị trí của nút cổ chai có thể phản trực giác sâu sắc . Sử dụng một bộ hồ sơ.
Eric Lippert

Câu trả lời:


48

Bạn có thể xem xét thực hiện tác vụ lọc trên một chuỗi nền sẽ gọi phương thức gọi lại khi nó hoàn tất hoặc chỉ cần khởi động lại quá trình lọc nếu đầu vào bị thay đổi.

Ý tưởng chung là có thể sử dụng nó như thế này:

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);
    }
}

Một bản phác thảo thô sẽ giống như:

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();
    }
}

Ngoài ra, bạn thực sự nên loại bỏ _filtercá thể khi bố mẹ Formđược xử lý. Điều này có nghĩa là bạn nên mở và chỉnh sửa phương thức Formcủa mình Dispose(bên trong YourForm.Designer.cstệp) để trông giống như:

// 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);
}

Trên máy của tôi, nó hoạt động khá nhanh, vì vậy bạn nên kiểm tra và lập hồ sơ trước khi tìm một giải pháp phức tạp hơn.

Điều đó đang được nói, một "giải pháp phức tạp hơn" có thể là lưu trữ một vài kết quả cuối cùng trong từ điển và sau đó chỉ lọc chúng nếu hóa ra mục nhập mới chỉ khác ký tự đầu tiên của ký tự cuối cùng.


Tôi vừa thử nghiệm giải pháp của bạn và nó hoạt động hoàn hảo! Tốt lắm. Vấn đề duy nhất tôi gặp phải là tôi không thể _signal.Dispose();biên dịch (lỗi về cấp độ bảo vệ).
etaiso

@etaiso: lạ thật, bạn đang gọi _signal.Dispose()ở đâu vậy Có phải ở đâu đó ngoài BackgroundWordFilterlớp không?
Groo

1
@Groo Nó được triển khai rõ ràng, có nghĩa là bạn không thể gọi nó trực tiếp. Bạn đang phải sử dụng hoặc là một usingkhối, hoặc gọiWaitHandle.Close()
Matthew Watson

1
Ok, bây giờ có lý, phương thức đã được đặt công khai trong .NET 4. Trang MSDN cho .NET 4 liệt kê nó dưới các phương thức công khai , trong khi trang cho .NET 3.5 hiển thị nó dưới các phương thức được bảo vệ . Điều đó cũng giải thích tại sao có một định nghĩa có điều kiện trong nguồn Mono cho WaitHandle .
Groo

1
@Groo Xin lỗi, lẽ ra tôi phải đề cập rằng tôi đang nói về phiên bản cũ hơn của .Net - xin lỗi về sự nhầm lẫn! Tuy nhiên, lưu ý rằng anh ta không cần truyền - .Close()thay vào đó anh ta có thể gọi , chính nó sẽ gọi .Dispose().
Matthew Watson

36

Tôi đã thực hiện một số thử nghiệm và việc tìm kiếm danh sách 120.000 mục và điền vào danh sách mới với các mục nhập mất một khoảng thời gian không đáng kể (khoảng 1/5 giây ngay cả khi tất cả các chuỗi đều khớp).

Do đó, vấn đề bạn đang gặp phải đến từ việc điền nguồn dữ liệu, ở đây:

listBox_choices.DataSource = ...

Tôi nghi ngờ bạn chỉ đơn giản là đưa quá nhiều mục vào hộp danh sách.

Có lẽ bạn nên thử giới hạn nó trong 20 mục đầu tiên, như sau:

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

Cũng lưu ý (như những người khác đã chỉ ra) rằng bạn đang truy cập thuộc TextBox.Texttính cho từng mục trong allUsers. Điều này có thể dễ dàng được sửa chữa như sau:

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

Tuy nhiên, tôi đã tính thời gian để truy cập TextBox.Text500.000 lần và nó chỉ mất 0,7 giây, ít hơn nhiều so với 1 - 3 giây được đề cập trong OP. Tuy nhiên, đây là một sự tối ưu hóa đáng giá.


1
Cảm ơn Matthew. Tôi đã thử giải pháp của bạn nhưng tôi không nghĩ vấn đề là với dân số của ListBox. Tôi nghĩ rằng tôi cần một cách tiếp cận tốt hơn vì loại lọc này rất ngây thơ (ví dụ: tìm kiếm "abc" trả về 0 kết quả, sau đó tôi thậm chí không nên tìm kiếm "abcX", v.v.)
etaiso

@etaiso đúng (ngay cả khi giải pháp của Matthew có thể hoạt động tốt nếu bạn không thực sự cần đặt trước tất cả các kết quả phù hợp), đó là lý do tại sao tôi đề xuất bước thứ hai để tinh chỉnh tìm kiếm thay vì thực hiện toàn bộ tìm kiếm mỗi lần.
Adriano Repetti

5
@etaiso Chà, thời gian tìm kiếm không đáng kể như tôi đã nói. Tôi đã thử nó với 120.000 chuỗi và tìm kiếm một chuỗi rất dài nhưng không có kết quả phù hợp nào và một chuỗi rất ngắn có nhiều kết quả phù hợp cả hai đều mất dưới 1/50 giây.
Matthew Watson

3
textBox_search.Textđóng góp một lượng có thể đo lường được vào thời gian không? Lấy thuộc Texttính trên hộp văn bản một lần cho mỗi 120 nghìn chuỗi có thể gửi 120 nghìn thông báo đến cửa sổ điều khiển chỉnh sửa.
Gabe

@Gabe Có nó. Xem câu trả lời của tôi để biết chi tiết.
Andris

28

Sử dụng cây Hậu tố làm chỉ mục. Hay đúng hơn chỉ cần xây dựng một từ điển được sắp xếp liên kết mọi hậu tố của mọi tên với danh sách các tên tương ứng.

Đối với đầu vào:

Abraham
Barbara
Abram

Cấu trúc sẽ giống như sau:

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

Thuật toán tìm kiếm

Giả sử người dùng nhập "áo ngực".

  1. Chia nhỏ từ điển khi người dùng nhập để tìm thông tin nhập của người dùng hoặc vị trí mà nó có thể đi đến. Bằng cách này, chúng tôi tìm thấy "barbara" - phím cuối cùng thấp hơn "áo ngực". Nó được gọi là giới hạn dưới cho "áo ngực". Tìm kiếm sẽ theo thời gian logarit.
  2. Lặp lại từ khóa tìm thấy trở đi cho đến khi đầu vào của người dùng không còn khớp nữa. Điều này sẽ cho "bram" -> Abram và "braham" -> Abraham.
  3. Kết hợp kết quả lặp lại (Áp-ram, Áp-ra-ham) và xuất ra.

Những cây như vậy được thiết kế để tìm kiếm nhanh các chuỗi con. Hiệu suất của nó gần với O (log n). Tôi tin rằng cách tiếp cận này sẽ hoạt động đủ nhanh để được sử dụng trực tiếp bởi luồng GUI. Hơn nữa nó sẽ hoạt động nhanh hơn giải pháp phân luồng do không có chi phí đồng bộ hóa.


Theo những gì tôi biết, mảng hậu tố thường là lựa chọn tốt hơn cây hậu tố. Dễ triển khai hơn và sử dụng bộ nhớ thấp hơn.
CodesInChaos

Tôi đề xuất SortedList rất dễ xây dựng và duy trì với chi phí sử dụng bộ nhớ có thể được giảm thiểu bằng cách cung cấp dung lượng danh sách.
Basilevs

Ngoài ra, có vẻ như các mảng (và ST gốc) được thiết kế để xử lý các văn bản lớn, trong khi ở đây chúng ta có một lượng lớn các đoạn ngắn là nhiệm vụ khác nhau.
Basilevs

+1 cho cách tiếp cận tốt, nhưng tôi muốn sử dụng bản đồ băm hoặc cây tìm kiếm thực tế hơn là tìm kiếm danh sách theo cách thủ công.
OrangeDog

Có lợi thế nào khi sử dụng cây hậu tố thay vì cây tiền tố?
jnovacho

15

Bạn cần một công cụ tìm kiếm văn bản (như Lucene.Net ) hoặc cơ sở dữ liệu (bạn có thể xem xét một công cụ được nhúng như SQL CE , SQLite , v.v.). Nói cách khác, bạn cần một tìm kiếm được lập chỉ mục. Tìm kiếm dựa trên băm không áp dụng ở đây vì bạn đang tìm kiếm chuỗi con, trong khi tìm kiếm dựa trên băm rất tốt để tìm kiếm giá trị chính xác.

Nếu không, nó sẽ là một tìm kiếm lặp đi lặp lại với việc lặp qua bộ sưu tập.


Lập chỉ mục một tìm kiếm dựa trên băm. Bạn chỉ cần thêm tất cả các chuỗi con làm khóa thay vì chỉ giá trị.
OrangeDog

3
@OrangeDog: không đồng ý. tìm kiếm được lập chỉ mục có thể được triển khai dưới dạng tìm kiếm dựa trên băm bằng các khóa chỉ mục, nhưng nó không cần thiết và nó không phải là tìm kiếm dựa trên băm theo giá trị chuỗi.
Dennis

@Dennis Đồng ý. +1 để hủy ma -1.
người dùng

+1 vì các triển khai như công cụ tìm kiếm văn bản có các tối ưu hóa (er) thông minh hơn string.Contains. I E. tìm kiếm batrong bcaaaabaasẽ dẫn đến một danh sách bỏ qua (được lập chỉ mục). Đầu tiên bđược xem xét, nhưng không khớp vì tiếp theo là a c, vì vậy nó sẽ bỏ qua tiếp theo b.
Caramiriel

12

Nó cũng có thể hữu ích khi có một loại sự kiện "debounce". Điều này khác với điều chỉnh ở chỗ nó đợi một khoảng thời gian (ví dụ: 200 ms) để các thay đổi kết thúc trước khi kích hoạt sự kiện.

Xem Debounce and Throttle: giải thích trực quan để biết thêm thông tin về gỡ lỗi. Tôi đánh giá cao rằng bài viết này tập trung vào JavaScript, thay vì C #, nhưng nguyên tắc áp dụng.

Ưu điểm của điều này là nó không tìm kiếm khi bạn vẫn đang nhập truy vấn của mình. Sau đó, nó sẽ ngừng cố gắng thực hiện hai tìm kiếm cùng một lúc.


Xem cách triển khai C # của bộ điều chỉnh sự kiện lớp EventThrotler trong thư viện Algorithmia: github.com/SolutionsDesign/Algorithmia/blob/master/…
Frans Bouma

11

Chạy tìm kiếm trên một chuỗi khác và hiển thị một số hoạt ảnh đang tải hoặc thanh tiến trình trong khi chuỗi đó đang chạy.

Bạn cũng có thể thử song song hóa truy vấn LINQ .

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

Dưới đây là một điểm chuẩn thể hiện lợi thế hiệu suất của 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
Tôi biết đó là một khả năng. Nhưng câu hỏi của tôi ở đây là nếu và làm thế nào tôi có thể rút ngắn quá trình này?
etaiso

1
@etaiso nó nên không thực sự là một vấn đề trừ khi bạn đang phát triển trên một số phần cứng cuối thực sự thấp, hãy chắc chắn rằng bạn không chạy trình gỡ lỗi, CTRL + F5
animaonline

1
Đây không phải là một ứng cử viên sáng giá cho PLINQ vì phương pháp String.Containsnày không đắt. msdn.microsoft.com/en-us/library/dd997399.aspx
Tim Schmelter

1
@TimSchmelter khi chúng ta đang nói về hàng tấn chuỗi, đúng là như vậy!
animaonline

4
@TimSchmelter Tôi không biết bạn đang cố chứng minh điều gì, sử dụng mã tôi cung cấp rất có thể sẽ tăng hiệu suất cho OP và đây là điểm chuẩn chứng minh cách hoạt động của nó: pastebin.com/ATYa2BGt --- Kỳ - -
animaonline

11

Cập nhật:

Tôi đã làm một số hồ sơ.

(Cập nhật 3)

  • Nội dung danh sách: Các số được tạo từ 0 đến 2.499.999
  • Lọc văn bản: 123 (20.477 kết quả)
  • Core i5-2500, Win7 64bit, RAM 8GB
  • VS2012 + JetBrains dotTrace

Lần chạy thử nghiệm đầu tiên cho 2.500.000 bản ghi, tôi mất 20.000ms.

Thủ phạm số một là cuộc gọi vào textBox_search.Textbên trong Contains. Điều này thực hiện một cuộc gọi cho mỗi phần tử đến get_WindowTextphương thức đắt tiền của hộp văn bản. Chỉ cần thay đổi mã thành:

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

giảm thời gian thực hiện xuống 1.858ms .

Cập nhật 2:

Hai cổ chai quan trọng khác hiện là lệnh gọi đến string.Contains(khoảng 45% thời gian thực thi) và cập nhật các phần tử hộp danh sách trong set_Datasource(30%).

Chúng tôi có thể cân bằng giữa tốc độ và việc sử dụng bộ nhớ bằng cách tạo cây Hậu tố như Basilevs đã đề xuất để giảm số lượng so sánh cần thiết và đẩy một số thời gian xử lý từ tìm kiếm sau khi nhấn phím để tải tên từ tệp. có thể thích hợp cho người dùng.

Để tăng hiệu suất tải các phần tử vào hộp danh sách, tôi khuyên bạn chỉ nên tải một số phần tử đầu tiên và cho người dùng biết rằng có các phần tử khác có sẵn. Bằng cách này, bạn đưa ra phản hồi cho người dùng rằng có sẵn các kết quả để họ có thể tinh chỉnh tìm kiếm của mình bằng cách nhập thêm các chữ cái hoặc tải danh sách đầy đủ chỉ bằng một lần nhấn nút.

Sử dụng BeginUpdateEndUpdatekhông thay đổi thời gian thực hiện set_Datasource.

Như những người khác đã lưu ý ở đây, bản thân truy vấn LINQ chạy khá nhanh. Tôi tin rằng cổ chai của bạn là sự cập nhật của chính hộp danh sách. Bạn có thể thử một cái gì đó như:

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

Tôi hi vọng cái này giúp được.


Tôi không nghĩ rằng điều này sẽ cải thiện bất cứ điều gì như BeginUpdateEndUpdateđược dự định sử dụng khi thêm các mục riêng lẻ hoặc khi sử dụng AddRange().
etaiso

Nó phụ thuộc vào cách DataSourcetài sản được thực hiện. Nó đáng để thử.
Andris

Kết quả hồ sơ của bạn rất khác với của tôi. Tôi có thể tìm kiếm 120k chuỗi trong 30ms, nhưng việc thêm chúng vào hộp danh sách mất 4500ms. Có vẻ như bạn đang thêm 2,5 triệu chuỗi vào hộp danh sách trong thời gian dưới 600 mili giây. Làm thế nào là có thể?
Gabe

@Gabe Trong khi lập hồ sơ, tôi đã sử dụng đầu vào mà văn bản bộ lọc đã loại bỏ một phần lớn danh sách ban đầu. Nếu tôi sử dụng đầu vào trong đó văn bản bộ lọc không xóa gì khỏi danh sách, tôi sẽ nhận được kết quả tương tự với kết quả của bạn. Tôi sẽ cập nhật phản hồi của mình để làm rõ những gì tôi đã đo được.
Andris

9

Giả sử bạn chỉ phù hợp bởi các tiền tố, cấu trúc dữ liệu bạn đang tìm kiếm được gọi là một Trie , còn được gọi là "cây tiền tố". Các IEnumerable.Wherephương pháp mà bạn đang sử dụng bây giờ sẽ phải lặp qua tất cả các mục trong từ điển của bạn trên mỗi truy cập.

Chủ đề này chỉ ra cách tạo một trie trong C #.


1
Giả sử anh ta đang lọc hồ sơ của mình bằng một tiền tố.
Tarec

1
Lưu ý rằng anh ấy đang sử dụng phương thức String.Contains () thay vì String.StartsWith (), vì vậy nó có thể không phải là chính xác những gì chúng ta đang tìm kiếm. Tuy nhiên - ý tưởng của bạn chắc chắn là tốt hơn so với việc lọc thông thường với phần mở rộng StartsWith () trong kịch bản tiền tố.
Tarec

Nếu anh ta bắt đầu có ý nghĩa, sau đó các Trie thể được kết hợp với cách tiếp cận nền công nhân để cải thiện hiệu suất
Lyndon Trắng

8

Điều khiển WinForms ListBox thực sự là kẻ thù của bạn ở đây. Nó sẽ chậm để tải các bản ghi và ScrollBar sẽ chống lại bạn để hiển thị tất cả 120.000 bản ghi.

Thử sử dụng dữ liệu DataGridView kiểu cũ có nguồn gốc từ DataTable với một cột duy nhất [Tên người dùng] để giữ dữ liệu của bạn:

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;
}

Sau đó, sử dụng DataView trong sự kiện TextChanged của TextBox của bạn để lọc dữ liệu:

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 trong khi mọi người khác đang cố gắng tối ưu hóa tìm kiếm chỉ mất 30 mili giây, bạn là người duy nhất nhận ra rằng vấn đề thực sự nằm ở việc điền vào hộp danh sách.
Gabe

7

Đầu tiên, tôi sẽ thay đổi cách ListControlxem nguồn dữ liệu của bạn, bạn đang chuyển đổi kết quả IEnumerable<string>thành List<string>. Đặc biệt là khi bạn chỉ gõ một vài ký tự, điều này có thể không hiệu quả (và không cần thiết). Không tạo bản sao mở rộng dữ liệu của bạn .

  • Tôi sẽ gói .Where()kết quả vào một bộ sưu tập chỉ triển khai những gì được yêu cầu từ IList(tìm kiếm). Điều này sẽ giúp bạn tiết kiệm để tạo một danh sách lớn mới cho mỗi ký tự được nhập.
  • Thay vào đó, tôi sẽ tránh LINQ và tôi sẽ viết một cái gì đó cụ thể hơn (và được tối ưu hóa). Giữ danh sách của bạn trong bộ nhớ và xây dựng một mảng các chỉ số phù hợp, sử dụng lại mảng để bạn không phải phân bổ lại nó cho mỗi lần tìm kiếm.

Bước thứ hai là không tìm kiếm trong danh sách lớn khi danh sách nhỏ là đủ. Khi người dùng bắt đầu gõ "ab" và thêm "c" thì bạn không cần phải nghiên cứu trong danh sách lớn, tìm kiếm trong danh sách đã lọc là đủ (và nhanh hơn). Tinh chỉnh tìm kiếm mọi lúc có thể, không thực hiện tìm kiếm đầy đủ mỗi lần.

Bước thứ ba có thể khó hơn: giữ cho dữ liệu có tổ chức để được tìm kiếm nhanh chóng . Bây giờ bạn phải thay đổi cấu trúc bạn sử dụng để lưu trữ dữ liệu của mình. tưởng tượng một cái cây như thế này:

ABC
 Thêm Ceil tốt hơn
 Đường viền trên xương

Điều này có thể đơn giản được triển khai với một mảng (nếu bạn đang làm việc với các tên ANSI nếu không thì một từ điển sẽ tốt hơn). Tạo danh sách như thế này (mục đích minh họa, nó khớp với phần đầu của chuỗi):

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);
    }
}

Sau đó, tìm kiếm sẽ được thực hiện bằng cách sử dụng ký tự đầu tiên:

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

Xin lưu ý rằng tôi đã sử dụng MyListWrapper()như đề xuất ở bước đầu tiên (nhưng tôi đã bỏ qua gợi ý thứ 2 cho ngắn gọn, nếu bạn chọn đúng kích thước cho khóa từ điển, bạn có thể giữ cho mỗi danh sách ngắn gọn và nhanh chóng - có thể - tránh bất cứ điều gì khác). Hơn nữa, lưu ý rằng bạn có thể cố gắng sử dụng hai ký tự đầu tiên cho từ điển của mình (nhiều danh sách hơn và ngắn hơn). Nếu bạn mở rộng điều này, bạn sẽ có một cái cây (nhưng tôi không nghĩ rằng bạn có số lượng lớn như vậy).

nhiều thuật toán khác nhau để tìm kiếm chuỗi (với các cấu trúc dữ liệu liên quan), chỉ đề cập đến một số:

  • Tìm kiếm dựa trên automaton trạng thái hữu hạn : trong cách tiếp cận này, chúng tôi tránh bẻ khóa ngược bằng cách xây dựng một Automaton hữu hạn xác định (DFA) nhận dạng chuỗi tìm kiếm được lưu trữ. Những thứ này rất tốn kém để xây dựng — chúng thường được tạo ra bằng cách sử dụng cấu trúc tập hợp quyền hạn — nhưng rất nhanh chóng để sử dụng.
  • Stubs : Knuth – Morris – Pratt tính toán một DFA nhận dạng đầu vào với chuỗi để tìm kiếm dưới dạng hậu tố, Boyer – Moore bắt đầu tìm kiếm từ cuối kim, vì vậy nó thường có thể nhảy lên trước cả chiều dài kim ở mỗi bước. Baeza – Yates theo dõi xem các ký tự j trước đó có phải là tiền tố của chuỗi tìm kiếm hay không và do đó có thể thích ứng với tìm kiếm chuỗi mờ. Thuật toán bitap là một ứng dụng của cách tiếp cận Baeza – Yates.
  • Phương pháp lập chỉ mục : các thuật toán tìm kiếm nhanh hơn dựa trên việc xử lý trước văn bản. Sau khi xây dựng một chỉ mục chuỗi con, ví dụ cây hậu tố hoặc mảng hậu tố, có thể nhanh chóng tìm thấy các lần xuất hiện của một mẫu.
  • Các biến thể khác : một số phương pháp tìm kiếm, chẳng hạn như tìm kiếm bằng trigram, nhằm mục đích tìm điểm "độ gần gũi" giữa chuỗi tìm kiếm và văn bản hơn là "khớp / không khớp". Đây đôi khi được gọi là tìm kiếm "mờ".

Vài lời về tìm kiếm song song. Điều đó có thể xảy ra nhưng nó hiếm khi nhỏ vì chi phí để làm cho nó song song có thể dễ dàng cao hơn nhiều so với bản thân việc tìm kiếm. Tôi sẽ không thực hiện tìm kiếm song song (phân vùng và đồng bộ hóa sẽ sớm trở nên quá rộng và có thể phức tạp) nhưng tôi sẽ chuyển tìm kiếm sang một chuỗi riêng biệt . Nếu luồng chính không bận, người dùng của bạn sẽ không cảm thấy bất kỳ sự chậm trễ nào khi họ đang nhập (họ sẽ không lưu ý liệu danh sách có xuất hiện sau 200 mili giây hay không nhưng họ sẽ cảm thấy khó chịu nếu phải đợi 50 mili giây sau khi nhập) . Tất nhiên bản thân tìm kiếm phải đủ nhanh, trong trường hợp này, bạn không sử dụng các chuỗi để tăng tốc tìm kiếm mà để giữ cho giao diện người dùng của bạn phản hồi . Xin lưu ý rằng một chuỗi riêng biệt sẽ không thực hiện truy vấn của bạnnhanh hơn , nó sẽ không treo giao diện người dùng nhưng nếu truy vấn của bạn chậm, nó sẽ vẫn chậm trong một chuỗi riêng (hơn nữa bạn cũng phải xử lý nhiều yêu cầu tuần tự).


1
Như một số đã chỉ ra, OP không muốn giới hạn kết quả chỉ ở các tiền tố (tức là anh ta sử dụng Containschứ không phải StartsWith). Một lưu ý nhỏ, thường tốt hơn là sử dụng ContainsKeyphương pháp chung khi tìm kiếm khóa để tránh quyền anh và thậm chí tốt hơn nên sử dụng TryGetValueđể tránh hai lần tra cứu.
Groo

2
@Groo bạn nói đúng, như tôi đã nói nó chỉ mang tính chất minh họa. Điểm của mã đó không phải là một giải pháp hiệu quả mà là một gợi ý: nếu bạn đã thử mọi thứ khác - tránh sao chép, tinh chỉnh tìm kiếm, chuyển nó sang một chuỗi khác - và nó không đủ thì bạn phải thay đổi cấu trúc dữ liệu mà bạn đang sử dụng . Ví dụ là đầu của chuỗi đơn giản.
Adriano Repetti

@Adriano cảm ơn vì câu trả lời rõ ràng và chi tiết! Tôi đồng ý với hầu hết những điều bạn đã đề cập nhưng như Groo đã nói, phần cuối cùng của việc giữ dữ liệu được ngăn nắp không áp dụng trong trường hợp của tôi. Nhưng tôi nghĩ rằng có lẽ để giữ một cuốn từ điển tương tự với các phím như lá thư chứa (mặc dù vẫn sẽ được sao chép)
etaiso

sau khi kiểm tra và tính toán của "bức thư chứa" ý tưởng là không tốt cho chỉ một ký tự (và nếu chúng ta đi cho sự kết hợp của hai hay nhiều chúng ta sẽ kết thúc với một bảng băm rất lớn)
etaiso

@etaiso vâng, bạn có thể giữ một danh sách gồm hai chữ cái (để nhanh chóng giảm bớt danh sách con) nhưng cây true có thể hoạt động tốt hơn (mỗi chữ cái được liên kết với các chữ cái kế tiếp của nó, không quan trọng vị trí của nó bên trong chuỗi vì vậy đối với "HOME", bạn có "H-> O", "O-> M" và "M-> E". Nếu bạn đang tìm kiếm "om", bạn sẽ nhanh chóng tìm thấy nó. Vấn đề là nó trở nên khá phức tạp và có thể quá nhiều cho bạn kịch bản (IMO).
Adriano Repetti

4

Bạn có thể thử sử dụng PLINQ (LINQ song song). Mặc dù điều này không đảm bảo tăng tốc độ, nhưng điều này bạn cần phải tìm ra bằng cách thử và sai.


4

Tôi nghi ngờ bạn sẽ có thể làm cho nó nhanh hơn, nhưng chắc chắn bạn nên:

a) Sử dụng phương thức mở rộng AsParallel LINQ

a) Sử dụng một số loại bộ đếm thời gian để trì hoãn lọc

b) Đặt một phương pháp lọc trên một chuỗi khác

Giữ một số loại string previousTextBoxValueở đâu đó. Tạo bộ hẹn giờ có độ trễ 1000 mili giây để kích hoạt tìm kiếm khi đánh dấu nếu previousTextBoxValuegiống với textbox.Textgiá trị của bạn . Nếu không - gán lại previousTextBoxValuecho giá trị hiện tại và đặt lại bộ đếm thời gian. Đặt bộ hẹn giờ bắt đầu cho sự kiện thay đổi hộp văn bản và nó sẽ giúp ứng dụng của bạn hoạt động trơn tru hơn. Lọc 120.000 bản ghi trong 1-3 giây là được, nhưng giao diện người dùng của bạn phải vẫn đáp ứng.


1
Tôi không đồng ý làm cho nó song song nhưng tôi hoàn toàn đồng ý với hai điểm còn lại. Nó thậm chí có thể đủ để đáp ứng các yêu cầu về giao diện người dùng.
Adriano Repetti

Quên đề cập đến điều đó nhưng tôi đang sử dụng .NET 3.5 nên AsParallel không phải là một tùy chọn.
etaiso

3

Bạn cũng có thể thử sử dụng hàm BindingSource.Filter . Tôi đã sử dụng nó và nó hoạt động giống như một chiếc bùa để lọc từ nhiều bản ghi, mỗi khi cập nhật thuộc tính này với văn bản đang được tìm kiếm. Một tùy chọn khác sẽ là sử dụng AutoCompleteSource để kiểm soát TextBox.

Hy vọng nó giúp!


2

Tôi sẽ cố gắng sắp xếp bộ sưu tập, tìm kiếm để chỉ khớp với phần bắt đầu và giới hạn tìm kiếm theo một số.

cứ như vậy từ chối

allUsers.Sort();

và tìm kiếm

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

Có lẽ bạn có thể thêm một số bộ nhớ cache.


1
Anh ấy không làm việc với đầu chuỗi (đó là lý do tại sao anh ấy đang sử dụng String.Contains ()). Với Chứa (), một danh sách được sắp xếp không thay đổi hiệu suất.
Adriano Repetti

Có, với 'Chứa' thì vô dụng. Tôi thích đề xuất với sufix tree stackoverflow.com/a/21383731/994849 Có rất nhiều câu trả lời thú vị trong chuỗi, nhưng nó phụ thuộc vào lượng thời gian anh ấy có thể dành cho nhiệm vụ này.
cứng rắn,

1

Sử dụng song song LINQ. PLINQlà một triển khai song song của LINQ đến các đối tượng. PLINQ triển khai tập hợp đầy đủ các toán tử truy vấn chuẩn LINQ làm phương thức mở rộng cho không gian tên T: System.Linq và có các toán tử bổ sung cho các hoạt động song song. PLINQ kết hợp sự đơn giản và dễ đọc của cú pháp LINQ với sức mạnh của lập trình song song. Cũng giống như mã nhắm mục tiêu Thư viện song song Tác vụ, các truy vấn PLINQ mở rộng theo mức độ đồng thời dựa trên khả năng của máy tính chủ.

Giới thiệu về PLINQ

Hiểu tốc độ trong PLINQ

Ngoài ra bạn có thể sử dụng Lucene.Net

Lucene.Net là một cổng của thư viện công cụ tìm kiếm Lucene, được viết bằng C # và nhắm mục tiêu đến người dùng .NET runtime. Thư viện tìm kiếm Lucene dựa trên một chỉ mục đảo ngược. Lucene.Net có ba mục tiêu chính:


1

Theo những gì tôi đã thấy, tôi đồng ý với thực tế để sắp xếp danh sách.

Tuy nhiên để sắp xếp khi xây dựng danh sách sẽ rất chậm, hãy sắp xếp khi xây dựng, bạn sẽ có thời gian thực hiện tốt hơn.

Ngược lại, nếu bạn không cần hiển thị danh sách hoặc để giữ thứ tự, hãy sử dụng một bản đồ băm.

Bản đồ băm sẽ băm chuỗi của bạn và tìm kiếm ở độ lệch chính xác. Tôi nghĩ nó sẽ nhanh hơn.


Hashmap bằng phím gì? Tôi muốn có thể tìm các từ khóa có trong các chuỗi.
etaiso

đối với khóa anh ấy, bạn có thể đưa số vào danh sách, nếu bạn muốn phàn nàn nhiều hơn, bạn có thể thêm số cộng với tên tùy chọn là của bạn.
dada

phần còn lại, tôi đã không đọc mọi thứ hoặc có một lời giải thích không tốt (có thể là cả hai;)) [quote] có một tệp văn bản của khoảng 120.000 người dùng (chuỗi) mà tôi muốn lưu trữ trong một bộ sưu tập và sau đó để thực hiện tìm kiếm trên bộ sưu tập đó. [/ quote] Tôi nghĩ đó chỉ là một chuỗi tìm kiếm.
dada

1

Hãy thử sử dụng phương pháp Tìm kiếm nhị phân, nó sẽ hoạt động nhanh hơn rồi đến phương thức Chứa.

Các vùng chứa sẽ là O (n) Tìm kiếm nhị phân là O (lg (n))

Tôi nghĩ rằng bộ sưu tập được sắp xếp sẽ hoạt động nhanh hơn khi tìm kiếm và chậm hơn khi thêm các phần tử mới, nhưng như tôi hiểu, bạn chỉ gặp vấn đề về hiệu suất tìm kiếm.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.