问题:
我有一个大约 120,000 用户(字符串)的文本文件,我希望将其存储在一个集合中,然后再对该集合执行搜索。
每次用户更改TextBox
的文字时都会发生搜索方法,结果应该是{strong> 包含 TextBox
中文字的字符串1}}。
我不必更改列表,只需提取结果并将其放入ListBox
。
到目前为止我尝试了什么:
我尝试了两个不同的集合/容器,我正在从外部文本文件中转储字符串条目(当然是一次):
List<string> allUsers;
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秒)。
问题:
您认为我的瓶颈在哪里?我用过的系列?搜索方法?既?
如何才能获得更好的性能和更流畅的功能?
答案 0 :(得分: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
实例。这意味着您应该打开并修改Form
的{{1}}方法(在Dispose
文件中),看起来像这样:
YourForm.Designer.cs
在我的机器上,它运行得非常快,因此在进行更复杂的解决方案之前,您应该对其进行测试和分析。
话虽这么说,“更复杂的解决方案”可能是将最后几个结果存储在字典中,然后只有在新条目仅与最后一个字符的第一个不同时才过滤它们。 / p>
答案 1 :(得分: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.Text
500,000次需要多长时间,它只用了0.7秒,远远少于OP中提到的1-3秒。不过,这是值得进行的优化。
答案 2 :(得分:28)
使用Suffix tree作为索引。或者更确切地说,只需构建一个排序字典,将每个名称的每个后缀与相应名称列表相关联。
输入:
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”。
此类树设计用于快速搜索子串。它的性能接近于O(log n)。我相信这种方法可以快速工作,直接由GUI线程使用。此外,由于缺少同步开销,它将比线程解决方案更快地工作。
答案 3 :(得分:15)
您需要文本搜索引擎(如Lucene.Net)或数据库(您可以考虑嵌入式搜索引擎,如SQL CE,SQLite等)。换句话说,您需要一个索引搜索。基于散列的搜索在这里不适用,因为您搜索子字符串,而基于散列的搜索很适合搜索精确值。
否则,它将是循环遍历集合的迭代搜索。
答案 4 :(得分:12)
拥有“去抖动”类型的事件可能也很有用。这与限制不同之处在于它会在触发事件之前等待一段时间(例如,200 ms)以完成更改。
有关去抖动的详情,请参阅 Debounce and Throttle: a visual explanation 。我感谢这篇文章以JavaScript为重点,而不是C#,但原则适用。
这样做的好处是,当您仍在输入查询时,它不会进行搜索。然后它应该停止尝试一次执行两次搜索。
答案 5 :(得分:11)
我做了一些分析。
(更新3)
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 。
另外两个重要的瓶颈现在是对string.Contains
的调用(约占执行时间的45%)以及set_Datasource
(30%)中列表框元素的更新。
我们可以通过创建一个后缀树来在速度和内存使用之间进行权衡,因为Basilevs建议减少必要的比较数量,并在按键后加载一些处理时间到加载名称来自文件,可能对用户更有利。
为了提高将元素加载到列表框中的性能,我建议只加载前几个元素,并向用户表明还有其他元素可用。通过这种方式,您可以向用户提供可用结果的反馈,以便他们可以通过输入更多字母或按一下按钮加载完整列表来优化搜索。
使用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();
}
我希望这会有所帮助。
答案 6 :(得分: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);
}
答案 7 :(得分:9)
假设您只是通过前缀匹配,您要查找的数据结构称为trie,也称为“前缀树”。您现在使用的IEnumerable.Where
方法必须在每次访问时遍历字典中的所有项目。
This thread显示了如何在C#中创建一个trie。
答案 8 :(得分:8)
WinForms ListBox控件确实是你的敌人。加载记录的速度很慢,ScrollBar会对你展示所有120,000条记录。
尝试使用传统的DataGridView数据来源DataTable,使用单个列[UserName]来保存您的数据:
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;
}
答案 9 :(得分:7)
首先,我会更改ListControl
查看数据来源的方式,您将结果IEnumerable<string>
转换为List<string>
。特别是当你输入几个字符时,这可能是低效的(并且不需要)。 不要制作数据的大量副本。
.Where()
结果包装到一个只实现IList
(搜索)所需内容的集合中。这将节省您为键入的每个字符创建一个新的大列表。第二步是当小的足够时不要在大列表中搜索。当用户开始键入“ab”并添加“c”时,您不需要在大列表中进行研究,在过滤列表中搜索就足够了(并且更快)。每次都可以优化搜索,每次都不要执行完整搜索。
第三步可能更难:保持数据的快速搜索。现在,您必须更改用于存储数据的结构。想象一下这样的树:
A B C Add Better Ceil Above Bone Contour
这可以简单地用数组实现(如果你使用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()
(但为了简洁,我省略了第二条建议,如果你选择正确的字典大小,你可以保持每个列表的简短和快速 - 也许 - 避免其他任何)。此外请注意,您可以尝试使用前两个字符作为字典(更多列表和更短)。如果你扩展这个你将有一棵树(但我不认为你有这么大的项目)。
有many different algorithms用于字符串搜索(带有相关数据结构),仅举几例:
关于并行搜索的几句话。这是可能的,但它很少是微不足道的,因为使其并行的开销可以比搜索本身高得多。我不会并行执行搜索(分区和同步将很快变得过于庞大而且可能很复杂)但我会将搜索移动到单独的线程。如果主线程不是忙,您的用户在打字时不会感到任何延迟(他们不会注意列表是否会在200毫秒后出现但是如果他们不得不这样做会感到不舒服他们打字后等待50毫秒)。当然,搜索本身必须足够快,在这种情况下,您不使用线程来加速搜索,而是保持您的UI响应。请注意,单独的线程不会使您的查询更快,它不会挂起UI但如果您的查询速度很慢,它在单独的线程中仍然会很慢(此外,您还必须处理多个顺序请求。)
答案 10 :(得分:4)
我怀疑你能否加快速度,但肯定你应该:
a)使用AsParallel LINQ扩展方法
a)使用某种计时器来延迟过滤
b)在另一个线程上放置一个过滤方法
在某处保留某种string previousTextBoxValue
。做一个延迟的计时器
如果previousTextBoxValue
与您的textbox.Text
值相同,则会触发查询。如果不是 - 将previousTextBoxValue
重新分配给当前值并重置计时器。将计时器开始设置为文本框已更改事件,它将使您的应用程序更流畅。在1-3秒内过滤120,000条记录是可以的,但您的用户界面必须保持响应。
答案 11 :(得分:4)
您可以尝试使用PLINQ(并行LINQ)。 虽然这并不能保证提速,但您需要通过反复试验找出答案。
答案 12 :(得分:3)
您也可以尝试使用BindingSource.Filter功能。我已经使用它,它就像一个魅力来过滤一堆记录,每次都用搜索文本更新这个属性。另一种选择是使用AutoCompleteSource进行TextBox控制。
希望它有所帮助!
答案 13 :(得分:2)
我会尝试对集合进行排序,搜索仅匹配起始部分并限制搜索某个数字。
关于ininialization
allUsers.Sort();
并搜索
allUsers.Where(item => item.StartWith(textBox_search.Text))
也许你可以添加一些缓存。
答案 14 :(得分:1)
使用并行 LINQ
。 PLINQ
是LINQ to Objects的并行实现。 PLINQ实现了一整套LINQ标准查询运算符作为T:System.Linq命名空间的扩展方法,并具有用于并行操作的附加运算符。 PLINQ将LINQ语法的简单性和可读性与并行编程的强大功能相结合。就像针对任务并行库的代码一样,PLINQ查询根据主机的功能在并发度上进行扩展。
Understanding Speedup in PLINQ
您也可以使用Lucene.Net
Lucene.Net是Lucene搜索引擎库的一个端口,用于编写 C#并针对.NET运行时用户。 Lucene搜索库是 基于倒排索引。 Lucene.Net有三个主要目标:
答案 15 :(得分:1)
根据我所看到的情况,我同意对列表进行排序的事实。
但是,对列表构造时的排序将非常缓慢,在构建时排序,您将有更好的执行时间。
否则,如果您不需要显示列表或保留订单,请使用散列图。
hashmap将散列您的字符串并搜索确切的偏移量。我想它应该更快。
答案 16 :(得分:1)
尝试使用BinarySearch方法,它应该比Contains方法更快。
包含将是O(n) BinarySearch是O(lg(n))
我认为排序集合在搜索时应该更快,在添加新元素时会更慢,但据我所知,你只有搜索性能问题。