C# N 路合并用于外部排序

Posted

技术标签:

【中文标题】C# N 路合并用于外部排序【英文标题】:C# N way merge for external sort 【发布时间】:2011-01-18 10:20:50 【问题描述】:

为 N 个排序的文件实现 N 路合并的最佳方法是什么?

假设我有 9 个已排序的文件,每个文件有 10 条记录?如何合并这些文件以创建一个包含 90 条已排序记录的大文件?

【问题讨论】:

是否有重复记录? 是什么阻止您进行内存排序和写入文件?换句话说,你的限制是什么? 我很想说,加载或简单地附加所有 9 个文件并重新排序。考虑到文件访问的开销,我想不出有什么好的理由在合并时尝试交错数据文件。如果您要处理的总记录负载大于可用内存,那么 live 会更复杂。 有或没有重复。无法在内存中,因为该文件超过 2 GB。 【参考方案1】:

我假设可能有比您在示例中提供的更多数据。如果您可以同时打开所有文件,则可以使用此算法:

读取每个文件的第一行,因此您在内存中有 10 行,每个文件一个。 按排序顺序将行放入优先队列。 从优先级队列中取出最少的元素(首先排序)并写入输出文件。 从该行所在的相应文件中再读取一行并将其放入优先级队列中。 重复直到所有文件都读完。

请注意,您不必一次将所有文件读入内存,因此如果您有合理数量的大文件,这将很有效,但如果您有很多小文件,则不会。

如果您有很多小文件,您应该将它们合并成组,为每个组制作一个输出文件,然后重复该过程以合并这些新组。

在 C# 中,您可以使用例如 SortedDictionary 来实现优先级队列。

【讨论】:

如果您一次读取一行,在文件扇区之间来回切换不会有很大的磁盘开销吗?读取每个文件的数据缓冲区似乎是一个重要因素 嘿,感谢您的快速响应这是我计划使用的算法。所以这是下一个问题,我有一个列表,其中包含示例 9 文件名中的临时文件名。但是这个数字每次都可能不同,具体取决于原始文件中的数据和用户指定的内存。如何根据我从原始文件创建的排序文件的数量来拥有不同数量的打开流? @user262102:创建一个列表。将流添加到列表中。使用 foreach 循环遍历流列表。完成后不要忘记关闭所有流。 @tbischel:现代磁盘控制器具有大缓存和大量智能。除非实际测试表明这是一个问题,否则我不会担心。 谢谢埃里克,这有帮助。跟踪最低记录来自哪个文件以启动流程的最佳方法是什么?【参考方案2】:

解决另一个答案中的 cmets:

如果您的文件数量不定,我会这样做。这只是一个草图。此代码无法编译,我的方法名称错误,等等。

// initialize the data structures
var priorityQueue = new SortedDictionary<Record, Stream>();
var streams = new List<Stream>();
var outStream = null; 
try

  // open the streams.
  outStream = OpenOutputStream();
  foreach(var filename in filenames)
    streams.Add(GetFileStream(filename));
  // initialize the priority queue
  foreach(var stream in streams)
  
    var record = ReadRecord(stream);
    if (record != null)
      priorityQueue.Add(record, stream);
  // the main loop
  while(!priorityQueue.IsEmpty)
  
     var record = priorityQueue.Smallest;
     var smallestStream = priorityQueue[record];
     WriteRecord(record, outStream);
     priorityQueue.Remove(record);
     var newRecord = ReadRecord(smallestStream);
     if (newRecord != null)
       priorityQueue.Add(newRecord, smallestStream);
  

finally  clean up the streams 

这有意义吗?您只需继续从优先级队列中抓取最小的东西,并将其替换为该流中的下一条记录(如果有的话)。最终队列将是空的,您将完成。

【讨论】:

一个问题是我的记录是一个字符串数组,我不能用它作为字典的键。我需要这样做,因为我解析 csv 文件以保留每个字段中的值,并且根据用户提供的列作为键,我使用快速排序找出最小的记录。希望很清楚,所以我无法使用上面的算法。还有其他想法吗? @user262102:创建一个实现该逻辑的比较器对象,并将其作为排序函数传递给已排序的字典。 这是一个实现起来非常简单的算法,但请注意,使用 SortedDictionary 意味着如果您的输入中有重复数据,它将引发异常。所以要么使用 IPriorityQueue ,或者如果你不想重复,那么在插入之前检查是否存在。【参考方案3】:

策略可能取决于数据量。

    如果数据适合内存,您可以将所有数据读入列表、排序并写出 如果要删除重复项,请使用 HashSet 而不是列表 如果它不适合内存,打开所有文件进行读取,比较每个文件的第一条记录,并写出最低的。然后推进您阅读的文件。循环遍历所有文件,直到它们全部用完并写入新文件。 如果要删除重复项,请执行上述操作,但跳过与最后写入相同的任何记录。

这是一个代码示例,它读取 N 个排序的文本文件并将它们合并。我没有包括重复检查,但它应该很容易实现。

首先是一个辅助类。

class MergeFile : IEnumerator<string>

    private readonly StreamReader _reader;

    public MergeFile(string file)
    
        _reader = File.OpenText(file);
        Current = _reader.ReadLine();
    

    public string Current  get; set; 

    public void Dispose()
    
        _reader.Close();
    

    public bool MoveNext()
    
        Current = _reader.ReadLine();
        return Current != null;
    

    public void Reset()
    
        throw new NotImplementedException();
    

    object IEnumerator.Current
    
        get  return Current; 
    

然后是读取和合并的代码(为了在生产中清晰起见,应该对其进行重构):

// Get the file names and instantiate our helper class
List<IEnumerator<string>> files = Directory.GetFiles(@"C:\temp\files", "*.txt").Select(file => new MergeFile(file)).Cast<IEnumerator<string>>().ToList();
List<string> result = new List<string>();
IEnumerator<string> next = null;
while (true)

    bool done = true;
    // loop over the helpers
    foreach (var mergeFile in files)
    
        done = false;
        if (next == null || string.Compare(mergeFile.Current, next.Current) < 1)
        
            next = mergeFile;
        
    
    if (done) break;
    result.Add(next.Current);
    if (!next.MoveNext())
    
        // file is exhausted, dispose and remove from list
        next.Dispose();
        files.Remove(next);
        next = null;
    

【讨论】:

谢谢,请看我上面的评论。【参考方案4】:

我想说不要使用优先队列,不要使用 IEnumerable。两者都很慢。

这是一种在外部存储器中对已排序文件进行排序或合并的快速方法:

http://www.codeproject.com/KB/recipes/fast_external_sort.aspx

【讨论】:

大家好,感谢您的回复,我确实使用合并排序算法实现了它。对于我的 QA 目的来说,这非常快。它在近 2 分钟内比较了 2 个文件(每个大约 300 MB)和大约 3000 万个单元格。这包括合并排序以及后续比较的时间。谢谢,巴文

以上是关于C# N 路合并用于外部排序的主要内容,如果未能解决你的问题,请参考以下文章

Python 归并排序(递归非递归自然合并排序)

剑指Offer 合并两个排序的链表

归并排序

Leetcode merge-k-sorted-lists(合并k路有序链表 最小堆)

归并排序

排序算法归并排序