【问题标题】:C# Data structure AlgorithmC# 数据结构算法
【发布时间】:2009-01-07 08:56:04
【问题描述】:

我最近接受了一家*软件公司的采访。我完全被面试官问我的一个问题所困扰,那就是

问。我有一台 512 mb / 1 GB RAM 的机器,我必须对 4 GB 大小的文件(XML 或任何文件)进行排序。我将如何进行?数据结构是什么,我将使用哪种排序算法以及如何使用?

你认为它可以实现吗?如果是的话,你能解释一下吗?

提前致谢!

【问题讨论】:

    标签: language-agnostic data-structures sorting


    【解决方案1】:

    面试官可能想要的答案可能是你如何有效地对超出系统内存的数据集进行排序。以下部分摘自Wikipedia

    内存使用模式和索引 排序

    当数组的大小为 排序接近或超过 可用的主内存,因此 (慢得多)磁盘或交换空间必须 被采用,内存使用模式 排序算法变为 重要的,以及可能的算法 当 很容易放入 RAM 中的数组可能会变成 不切实际的。在这种情况下, 比较总数变为 (相对)不太重要,并且 记忆片段的次数 必须相互复制或交换 磁盘可以支配性能 算法的特征。因此, 传球次数和 比较的本地化可以是 比原始数量更重要 比较,因为比较 附近的元素彼此发生 以系统总线速度(或者,使用缓存, 即使在 CPU 速度下),与 磁盘速度,实际上是 瞬间。

    例如,流行的递归 快速排序算法提供了相当 有足够的合理表现 RAM,但由于递归方式 它复制数组的一部分 当 数组不适合 RAM,因为它 可能会导致一些慢速复制或 将操作移入和移出磁盘。在 在这种情况下,另一种算法可能 更可取,即使它需要更多 总比较。

    解决此问题的一种方法, 当记录复杂时效果很好 (例如在关系数据库中)是 被一个相对较小的键排序 字段,是创建一个索引到 数组,然后对索引进行排序,而不是 比整个数组。 (一个排序的 然后整个数组的版本可以 一口气产生,阅读 从索引,但通常即使是 不必要的,因为已排序 索引就足够了。)因为索引 远小于整个数组, 它可能很容易放入内存中 整个阵列不会,有效地 消除磁盘交换问题。 这个过程有时被称为 “标签排序”。[5]

    另一种克服 内存大小问题是结合两个 算法以某种方式 各有实力优势 提高整体性能。为了 例如,数组可能是 细分为大小为 将很容易放入 RAM 中(例如,一些 千个元素),已排序的块 使用有效的算法(例如 快速排序或堆排序),以及 结果按照合并排序合并。这 比做事效率低 首先是mergesort,但它 需要更少的物理 RAM(将 实用)而不是完整的快速排序 整个数组。

    技术也可以组合。为了 对非常大的数据集进行排序 大大超过系统内存,甚至 索引可能需要使用 算法或算法组合 旨在合理执行 虚拟内存,即减少 所需的交换量。

    【讨论】:

      【解决方案2】:

      使用Divide and Conquer

      这是伪代码:

      function sortFile(file)
          if fileTooBigForMemory(file)
             pair<firstHalfOfFile, secondHalfOfFile> = breakIntoTwoHalves()
             sortFile(firstHalfOfFile)
             sortFile(secondHalfOfFile)
          else
             sortCharactersInFile(file)
          endif
      
          MergeTwoHalvesInOrder(firstHalfOfFile, secondHalfOfFile)
      end
      

      两个著名的分治算法是merge sortquick sort 算法。所以你可以用它们来实现。

      对于数据结构,一个包含文件中字符的字符数组就可以了。如果您想更加面向对象,请将其包装在一个名为 File 的类中:

      class File {
          private char[] characters;
          //methods to access and mutate 'characters'
      }
      

      【讨论】:

      • 这在一些教科书中也称为多路合并。
      【解决方案3】:

      Guido van Rossum blog 上有一篇不错的帖子,有一些建议。请注意,代码是 Python 中的。

      【讨论】:

      • 您好 Fabrizio,感谢您的回复,Guido van Rossum 博客非常接近我的问题,但如果您能提供更多参考,仍然会很棒。谢谢!!
      • 恐怕我不能做更多的事情了。无论如何,我认为您甚至可以从其他答案中获得“理由”:恕我直言,他们都在说同样的话。
      【解决方案4】:

      将文件拆分为适合内存的块。 使用快速排序对每个块进行排序并将其保存到单独的文件中。 然后合并结果文件,你得到你的结果。

      【讨论】:

      • 我知道它只是颠倒过来,我只是向他展示 2 如何使文件系统“出现”为内存 2 程序的 概念证明,因此他可以 无缝插入任何他喜欢的数组排序。撤消您的投票,这是未调用的 4。也许 dis 会有所帮助:letmegooglethatforyou.com/?q=c%23+array+quicksort
      【解决方案5】:

      我会使用多路合并。有一本名为Managing Gigabytes 的优秀书籍展示了几种不同的方法。他们还对大于物理内存的文件进行基于排序的反转。查看第 240 页,了解关于磁盘上的块排序的非常详细的算法。

      上面的帖子是正确的,因为您拆分了文件并对每个部分进行了排序。

      假设您有 4GB 的文件,并且只想加载最大 512MB。这意味着您需要将文件分成至少 8 个块。如果您不确定您的排序将使用多少额外开销,您甚至可以将该数字加倍以确保 16 个块的安全。

      然后将 16 个文件一次排序一个,以保证顺序。所以现在你有块 0-15 作为排序文件。

      现在您打开这些文件的 16 个文件句柄并一次读取一个条目,将最低的一个写入最终输出。由于您知道每个文件都已排序,因此从每个文件中取最低值意味着您将按照正确的顺序将它们写入最终输出。

      我在 C# 中使用了这样一个系统来对电子邮件中的大量垃圾邮件词进行分类。原始系统要求所有这些都加载到 RAM 中,以便对它们进行排序并为垃圾邮件计数建立字典。一旦文件增长超过 2 GB,内存结构就需要 6+GB 的 RAM,并且由于分页和 VM 的原因需要超过 24 小时才能进行排序。使用上述分块的新系统在 40 分钟内对整个文件进行了排序。对于如此简单的更改,这是一个令人印象深刻的加速。

      我使用了各种加载选项(每个块 1/4 系统内存等)。事实证明,对于我们的情况,最好的选择是大约 1/10 系统内存。然后 Windows 有足够的内存留给体面的文件 I/O 缓冲来抵消增加的文件流量。并且机器对运行在其上的其他进程非常敏感。

      是的,我也经常喜欢在面试中问这些类型的问题。只是看看人们是否可以跳出框框思考。当您不能只在列表上使用 .Sort() 时,您会怎么做?

      【讨论】:

        【解决方案6】:

        只是模拟一个虚拟内存,重载数组索引运算符,[]

        在 C++ 或 C# 中查找对数组进行排序的快速排序实现。重载索引器运算符 [] ,它将读取并保存到文件中。这样,您可以插入现有的排序算法,您只需更改那些 []

        的幕后发生的事情

        【讨论】:

        • 感谢 Michael.. 的回复,我们可以在不打开文件的情况下读取文件吗?如果我们打开它,那么它的大小可以超过 RAM 大小??你能澄清一下吗?
        • 如果使用 ReadLine,打开文件不会将其全部内容加载到内存中。在实现部分,ReadLines 将 XML/文本文件转换为二进制文件。使用您喜欢的数组排序算法,通过重载 [] 运算符对二进制文件进行读/排序/写操作。然后将其转换回 XML
        • 如果使用 ReadLine,打开文件不会将其全部内容加载到内存中。在实现部分,ReadLines 将 XML/文本文件转换为二进制文件。使用您喜欢的数组排序算法,通过重载 [] 运算符对二进制文件进行读/排序/写操作。然后将其转换回 XM
        【解决方案7】:

        这是在 C# 上模拟虚拟内存的一个示例

        来源:http://msdn.microsoft.com/en-us/library/aa288465(VS.71).aspx

        // indexer.cs
        // arguments: indexer.txt
        using System;
        using System.IO;
        
        // Class to provide access to a large file
        // as if it were a byte array.
        public class FileByteArray
        {
            Stream stream;      // Holds the underlying stream
                                // used to access the file.
        // Create a new FileByteArray encapsulating a particular file.
            public FileByteArray(string fileName)
            {
                stream = new FileStream(fileName, FileMode.Open);
            }
        
            // Close the stream. This should be the last thing done
            // when you are finished.
            public void Close()
            {
                stream.Close();
                stream = null;
            }
        
            // Indexer to provide read/write access to the file.
            public byte this[long index]   // long is a 64-bit integer
            {
                // Read one byte at offset index and return it.
                get 
                {
                    byte[] buffer = new byte[1];
                    stream.Seek(index, SeekOrigin.Begin);
                    stream.Read(buffer, 0, 1);
                    return buffer[0];
                }
                // Write one byte at offset index and return it.
                set 
                {
                    byte[] buffer = new byte[1] {value};
                    stream.Seek(index, SeekOrigin.Begin);
                    stream.Write(buffer, 0, 1);
                }
            }
        
            // Get the total length of the file.
            public long Length 
            {
                get 
                {
                    return stream.Seek(0, SeekOrigin.End);
                }
            }
        }
        
        // Demonstrate the FileByteArray class.
        // Reverses the bytes in a file.
        public class Reverse 
        {
            public static void Main(String[] args) 
            {
                // Check for arguments.
                if (args.Length == 0)
                {
                    Console.WriteLine("indexer <filename>");
                    return;
                }
        
                FileByteArray file = new FileByteArray(args[0]);
                long len = file.Length;
        
                // Swap bytes in the file to reverse it.
                for (long i = 0; i < len / 2; ++i) 
                {
                    byte t;
        
                    // Note that indexing the "file" variable invokes the
                    // indexer on the FileByteStream class, which reads
                    // and writes the bytes in the file.
                    t = file[i];
                    file[i] = file[len - i - 1];
                    file[len - i - 1] = t;
                }
        
                file.Close();
            } 
        }
        

        使用上面的代码来滚动你自己的数组类。然后只需使用任何数组排序算法。

        【讨论】:

        • 谢谢迈克尔,我会试试你的解决方案。但我会将您的回答标记为正确答案。欢呼:)
        • 这段代码只是反转文件。它不能解决你的问题,而且在速度方面效率极低。
        • 我知道它只是颠倒过来,我只是向他展示 2 如何使文件系统“出现”为内存 2 程序的 概念证明,所以他可以 无缝插入任何他喜欢的数组排序。撤消您的投票,这是未调用的 4。也许 dis 会有所帮助:letmegooglethatforyou.com/?q=c%23+array+quicksort
        • 迈克尔,我无法撤消投票 - 所以说投票太旧了,只有在帖子更改时才能更改。无论如何,您提出的解决方案是不合理的,任何“*软件公司”都会开除以这种方式解决所提供问题的人。
        • 这是合理的,如果你在上面使用某种形式的缓存,任何有自尊的程序员都会优化他们认为慢的代码。举个例子,我发现VirtualMode DataGridView最适合大数据,但我不会傻傻地使用它,我会为CellValueNeeded放一些缓存。