查找"键"在8GB +文本文件中

时间:2016-09-25 10:20:29

标签: algorithm sorting delphi search

我有一些'小'包含大约500000个条目/行的文本文件。每行还有一个'键'柱。我需要在一个大文件中找到这个密钥(8GB,至少2.19亿条目)。找到后,我需要追加“价值”。从大文件到小文件,在行的末尾作为新列。

像这样的大文件:

KEY                 VALUE
"WP_000000298.1"    "abc"
"WP_000000304.1"    "xyz"
"WP_000000307.1"    "random"
"WP_000000307.1"    "text"
"WP_000000308.1"    "stuff"
"WP_000000400.1"    "stuffy"

简单地说,我需要查找关键词'在大文件中。

显然我需要在RAM中加载整个表(但这不是我有32GB可用的问题)。大文件似乎已经排序了。我得检查一下。
问题是我无法使用类似TDictionary的快速查找,因为正如您所看到的,密钥不是唯一的

注意:这可能是一次性计算。我将使用该程序一次,然后扔掉它。因此,它不必是最佳算法(难以实现)。它只需要在适当的时间内完成(如1-2天)。 PS:我更喜欢没有DB这样做。

我正在考虑这个可能的解决方案:TList.BinarySearch。但似乎TList仅限于134,217,727(MaxInt div 16)项目。所以TList不会工作。

结论:
我选择了Arnaud Bouchez解决方案。他的TDynArray令人印象深刻!如果你需要处理大文件,我完全推荐它 AlekseyKharlanov提供了另一个不错的解决方案,但TDynArray已经实施。

7 个答案:

答案 0 :(得分:17)

尝试使用现有的实现,而不是重新发明二进制搜索或B-Tree的轮子。

将内容提供给SQLite3内存数据库(使用正确的索引,并且每10,000次INSERT执行一次事务),您就完成了。确保您定位Win64,以便在RAM中有足够的空间。您甚至可以使用基于文件的存储:创建速度稍慢,但使用索引时,按键查询将是即时的。如果您的Delphi版本中没有SQlite3支持(通过最新的FireDAC),您可以使用我们的OpenSource unit及其associated documentation

使用SQlite3将明确更快,并且使用的资源比常规客户端 - 服务器SQL数据库少 - BTW“免费”版本的MS SQL无法处理您需要的大量数据,AFAIR。

更新:我已经编写了一些示例代码,以说明如何将SQLite3与我们的ORM图层一起用于您的问题 - 请参阅this source code file in github。< / p>

以下是一些基准信息:

  with index defined before insertion:
    INSERT 1000000 rows in 6.71s
    SELECT 1000000 rows per Key index in 1.15s

  with index created after insertion:
    INSERT 1000000 rows in 2.91s
    CREATE INDEX 1000000 in 1.28s
    SELECT 1000000 rows per Key index in 1.15s

  without the index:
    INSERT 1000000 rows in 2.94s
    SELECT 1000000 rows per Key index in 129.27s

因此,对于庞大的数据集,索引是值得的,并且在数据插入之后创建索引会减少使用的资源!即使插入速度较慢,选择每个键时索引的增益也很大。您可以尝试对MS SQL执行相同的操作,或使用其他ORM,我猜您会哭。 ;)

答案 1 :(得分:10)

另一个答案,因为它是另一种解决方案。

我没有使用SQLite3数据库,而是使用了我们的TDynArray wrapper及其排序和二进制搜索方法。

type
  TEntry = record
    Key: RawUTF8;
    Value: RawUTF8;
  end;
  TEntryDynArray = array of TEntry;

const
  // used to create some fake data, with some multiple occurences of Key
  COUNT = 1000000; // million rows insertion !
  UNIQUE_KEY = 1024; // should be a power of two

procedure Process;

var
  entry: TEntryDynArray;
  entrycount: integer;
  entries: TDynArray;

  procedure DoInsert;
  var i: integer;
      rec: TEntry;
  begin
    for i := 0 to COUNT-1 do begin
      // here we fill with some data
      rec.Key := FormatUTF8('KEY%',[i and pred(UNIQUE_KEY)]);
      rec.Value := FormatUTF8('VALUE%',[i]);
      entries.Add(rec);
    end;
  end;

  procedure DoSelect;
  var i,j, first,last, total: integer;
      key: RawUTF8;
  begin
    total := 0;
    for i := 0 to pred(UNIQUE_KEY) do begin
      key := FormatUTF8('KEY%',[i]);
      assert(entries.FindAllSorted(key,first,last));
      for j := first to last do
        assert(entry[j].Key=key);
      inc(total,last-first+1);
    end;
    assert(total=COUNT);
  end;

以下是时间安排结果:

one million rows benchmark:
INSERT 1000000 rows in 215.49ms
SORT ARRAY 1000000 in 192.64ms
SELECT 1000000 rows per Key index in 26.15ms

ten million rows benchmark:
INSERT 10000000 rows in 2.10s
SORT ARRAY 10000000 in 3.06s
SELECT 10000000 rows per Key index in 357.72ms

它比SQLite3内存解决方案快10倍以上。 1000万行保留在Win32进程的内存中没有问题。

有关TDynArray包装器在实践中如何工作以及如何its SSE4.2 optimized string comparison functions give good results的良好示例。

完整的源代码可用in our github repository

编辑,在Win64下拥有100,000,000行(1亿行),在此过程中使用超过10GB的RAM:

INSERT 100000000 rows in 27.36s
SORT ARRAY 100000000 in 43.14s
SELECT 100000000 rows per Key index in 4.14s

答案 2 :(得分:7)

因为这是一次性任务。最快的方法是将整个文件加载到内存中,逐行扫描内存,解析密钥并将其与搜索键(键)进行比较并打印(保存)找到的位置。

UPD:如果您在源文件中已排序列表。并假设您有411000个键来查找。您可以使用此技巧:按源文件的顺序对搜索键进行排序。从两个列表中读取第一个密钥并进行比较。如果它们不同,请从源头读取,直到它们相等。保存位置,如果源中的下一个键也相等,也保存它。等等。如果下一个键不同,请从搜索键列表中读取下一个键。继续直到EOF。

答案 3 :(得分:3)

使用内存映射文件。只是认为你的文件已经完全读入内存,并在内存中进行你想要的二进制搜索。让Windows在您进行内存搜索时关心读取文件的部分。

您可以将这些来源中的任何一个用于开始,只是不要忘记为Win64更新它们

http://torry.net/quicksearchd.php?String=memory+mapped+files&Title=No

答案 4 :(得分:1)

需要对文件进行排序但完全避免数据结构的方法:

你只关心一行,所以为什么要阅读大部分文件?

打开文件并移动&#34; get指针&#34; (在谈论C的道歉)文件的中途。你必须弄清楚你是在一个数字还是一个单词,但是一个数字应该在附近。一旦知道最接近的数字,就知道它是否高于或低于您想要的数字,并继续二进制搜索。

答案 5 :(得分:0)

基于Aleksey Kharlanov回答的想法。我接受了他的回答 我只是在这里复制他的想法,因为他没有详细说明(没有伪代码或更深入的算法分析)。我希望在实施之前确认它有效。

我们对两个文件进行排序(一次) 我们在内存中加载大文件(一次) 我们从磁盘读取小文件(一次)。

代码:
在下面的代码中,sKey是Small文件中的当前键。 bKey是Big文件中的当前键:

LastPos:= 0
for sKey in SmallFile do 
 for CurPos:= LastPos to BigFile.Count do 
  if sKey = bKey 
  then 
    begin 
     SearchNext  // search (down) next entries for possible duplicate keys
     LastPos:= CurPos
    end
  else 
    if sKey < bKey 
    then break

它有效,因为我知道最后一个键的最后位置(在Big文件中)。下一个键只能位于最后位置的某个位置;在AVERAGE它应该在接下来的440个条目中。但是,我甚至不必总是在LastPos下面读取440个条目,因为如果我的sKey不存在于大文件中,它将小于bKey,所以我很快打破了内循环并继续前进。

思想?

答案 6 :(得分:0)

如果我这样做是一次性的话,我会创建一个包含我需要查找的所有键的集合。然后逐行读取文件,检查该组中是否存在该键,如果是则输出该值。

简而言之,算法是:

mySet = dictionary of keys to look up
for each line in the file
    key = parse key from line
    if key in mySet
        output key and value
end for

由于Delphi没有通用集,我使用TDictionary并忽略该值。

字典查找是O(1),所以应该非常快。您的限制因素是文件I / O时间。

我认为编码需要大约10分钟,运行时间不到10分钟。