C++ 面试难题:用有限的内存对一百万个输入进行排序

C++ 面试难题:用有限的内存对一百万个输入进行排序,c++,c,algorithm,sorting,data-structures,C++,C,Algorithm,Sorting,Data Structures,我试着用外部排序来回答这个问题,但面试官回答说复杂度太高了。 还有更好的选择吗 简化问题: 假设我们有1000个元素要排序,空间只分配给100个元素。什么是比外部排序花费更少时间的最佳算法。我不知道你(或面试官)指的是哪种外部排序,但是 我的建议是10种方式(在您的情况下)合并: 将文件拆分为最大内存大小的块(100个元素) 这是O(1) 对内存中的每个块进行排序,并作为单独的文件存储 这是O((n/max\u mem)*(max\u mem)log(max\u mem))=O(n lo

我试着用外部排序来回答这个问题,但面试官回答说复杂度太高了。 还有更好的选择吗

简化问题:
假设我们有1000个元素要排序,空间只分配给100个元素。什么是比外部排序花费更少时间的最佳算法。

我不知道你(或面试官)指的是哪种外部排序,但是

我的建议是10种方式(在您的情况下)合并:

  • 将文件拆分为最大内存大小的块(100个元素)
    • 这是
      O(1)
  • 对内存中的每个块进行排序,并作为单独的文件存储
    • 这是
      O((n/max\u mem)*(max\u mem)log(max\u mem))
      =
      O(n log(max\u mem))
  • 将所有块作为元素流打开
  • 通过在每个步骤中选择最低的元素来合并所有流。
    • 这是
      O(n log(n/max\u mem))
      使用最小堆或
      O(n^2/max\u mem)
      非常简单(实际上可能更快)
  • 删除块
关于计算,这是
O(n(log(max_mem)+log(n/max_mem))
=
O(nlog(n))

关于磁盘I/O,如果所有合并都是在一次过程中完成的,那么这只是
2*n
读取和
2*n
写入。 更一般地说,它是
(1+[合并树的深度])*n

所有写入都是连续的。 第一次读取是顺序的,第二次读取是顺序的,从10个文件交叉读取

如果有更多的数据,则需要重复或递归合并(每个块100个,然后重复选取N个块)。在这一点上,用@amit的答案中描述的替换/选择替换拆分+排序步骤是值得的,特别是当数据已经几乎排序时(您可能完全回避合并步骤)


请注意,较高的N可能会增加计算量(如果使用正确的结构,可能会略微增加计算量),但会显著减少磁盘I/O量(达到一定量;如果一次合并太多块,则可能会耗尽读取缓冲区的内存,导致不必要的读取)。磁盘I/O很贵,CPU周期也不贵。

我不知道你(或面试官)指的是哪种外部类型,但是

我的建议是10种方式(在您的情况下)合并:

  • 将文件拆分为最大内存大小的块(100个元素)
    • 这是
      O(1)
  • 对内存中的每个块进行排序,并作为单独的文件存储
    • 这是
      O((n/max\u mem)*(max\u mem)log(max\u mem))
      =
      O(n log(max\u mem))
  • 将所有块作为元素流打开
  • 通过在每个步骤中选择最低的元素来合并所有流。
    • 这是
      O(n log(n/max\u mem))
      使用最小堆或
      O(n^2/max\u mem)
      非常简单(实际上可能更快)
  • 删除块
关于计算,这是
O(n(log(max_mem)+log(n/max_mem))
=
O(nlog(n))

关于磁盘I/O,如果所有合并都是在一次过程中完成的,那么这只是
2*n
读取和
2*n
写入。 更一般地说,它是
(1+[合并树的深度])*n

所有写入都是连续的。 第一次读取是顺序的,第二次读取是顺序的,从10个文件交叉读取

如果有更多的数据,则需要重复或递归合并(每个块100个,然后重复选取N个块)。在这一点上,用@amit的答案中描述的替换/选择替换拆分+排序步骤是值得的,特别是当数据已经几乎排序时(您可能完全回避合并步骤)


请注意,较高的N可能会增加计算量(如果使用正确的结构,可能会略微增加计算量),但会显著减少磁盘I/O量(达到一定量;如果一次合并太多块,则可能会耗尽读取缓冲区的内存,导致不必要的读取)。磁盘I/O成本很高,CPU周期也不高。

标准的做法是使用磁盘

在外部排序中,不仅要有
O(nlogn)
comlexity,而且要尽可能减少磁盘读/写,并使大多数读/写按顺序进行(而不是随机进行),因为按顺序进行磁盘访问效率更高

正如@JanDvorak所建议的那样,这样做的标准方法确实是k路合并排序,但我要纠正的建议中存在一些错误和补充:

  • 首先,对输入执行一次排序会减少初始“运行”的次数(增加序列的次数),因此通常会减少后期合并排序所需的总迭代次数
  • 我们需要内存进行缓冲(读取和写入输入)-因此,对于内存大小M和文件大小M*10,我们无法进行10路合并-这将导致大量读取磁盘(读取每个元素,而不是块)。
    k
    的标准公式-合并的“顺序”是
    M/(2b)
    (其中M是内存大小,
    b
    是每个“缓冲区”(通常是磁盘块)的大小
  • 每个合并排序步骤都是通过从上一次迭代中生成的每次“运行”中读取
    b
    条目来完成的-将
    M/2
    填充到内存中。内存的其余部分用于“预测”(这允许以最少的IO等待进行连续工作)-从运行中请求更多元素,并为输出缓冲区请求更多元素-以保证块中的顺序正确
  • 此方法的总迭代次数为
    log_k(N/(2M))
    ,其中
    k
    是运行次数(先前计算),
    M
    是内存大小,
    N
    是文件大小。每次迭代需要对整个文件进行1次顺序读取和1次顺序写入

  • 也就是说,文件大小/内存大小的比率是u