【问题标题】:How to sort integer numbers in a huge text file?如何在巨大的文本文件中对整数进行排序?
【发布时间】:2018-07-09 17:28:09
【问题描述】:

问题陈述

每次给我一个非常大的数字列表,我需要打印“中位数”。

更清楚地说,可以有“125,000,000”个数字,并且保证每个数字都小于“1.e+18”。

这是一个竞赛,所以有内存限制(最多 20 MB)时间限制(最多 5 秒)。 p>

中位数”是在已排序数字中间的那个。
例如,如果这是数字列表:

23
8
16
42
15
4
108

排序后的数字:

1) 4
2) 8
3) 15
4) 16
5) 23
6) 42
7) 108

中位数”将是 16;

所以我在互联网上搜索了它,但我找不到任何超过这些限制的答案。


方法

我的方法是获取所有数字,将它们保存在文本文件中,对它们进行排序,然后得到“中位数”。


想法

  1. 我知道我可以从文件中读取所有数字并将它们放入向量中 然后轻松地对它们进行排序。
    但这会超出内存限制

  2. 所以我想出了这个想法,在将数字放入文本时对其进行排序 文件。
    这就像有一个循环后得到下一个 来自控制台的数字读取文件(逐行)以及何时读取 到达正确的位置,在那里插入数字并且不接触其他 数字。
    但问题是我不能在中间插入一行 文本文件,因为它会覆盖其他数字。

  3. 所以我创建了两个文件,其中一个文件的编号已经 输入,另一个读取第一个文件并将其复制到 本身直到到达正确的位置然后插入最后一个给定的数字然后 继续复制剩余的数字。
    但是它花费了太多时间,所以它 超过时间限制

请求

所以我想优化这些想法之一以通过限制或任何通过这些限制的新想法。


偏好

我更喜欢使用第二个想法,因为与其他两个不同,它通过了限制,但我不能这样做,因为我不知道如何在文本文件中间插入一行。所以如果我学会了这一点,剩下的过程就会变得如此简单。


尝试的解决方案

这是一个函数,它接收一个数字,并通过读取一个文件,为它找到最佳位置并将其放在那里。
事实上,它代表了我的第三个​​想法
所以它可以工作(我用大量输入对其进行了测试)但我之前提到的问题是 时间限制

void insertNewCombinedNumber ( int combinedNumber )
{
    char combinedNumberCharacterArray[ 20 ];
    bool isInserted = false;

    ofstream combinedNumbersOutputFile;
    ifstream combinedNumbersInputFile;

    // Operate on First File
    if ( isFirstCombinedFileActive )
    {
        combinedNumbersOutputFile.open ( "Combined Numbers - File 01.txt" );
        combinedNumbersInputFile.open ( "Combined Numbers - File 02.txt" );
    }
    // Operate on Second File
    else
    {
        combinedNumbersOutputFile.open ( "Combined Numbers - File 02.txt" );
        combinedNumbersInputFile.open ( "Combined Numbers - File 01.txt" );
    }

    if ( !combinedNumbersInputFile )
    {
        combinedNumbersInputFile.close ();

        ofstream combinedNumbersInputCreateFile ( "Combined Numbers - File 02.txt" );
        combinedNumbersInputCreateFile.close ();

        combinedNumbersInputFile.open ( "Combined Numbers - File 02.txt" );
    }

    combinedNumbersInputFile.getline ( combinedNumberCharacterArray , 20 );

    for ( int i = 0; !combinedNumbersInputFile.eof (); i++ )
    {
        if ( !isInserted && combinedNumber <= characterArrayToDecimal ( combinedNumberCharacterArray ) )
        {
            combinedNumbersOutputFile << combinedNumber << endl;
            isInserted = true;
        }

        combinedNumbersOutputFile << combinedNumberCharacterArray << endl;

        combinedNumbersInputFile.getline ( combinedNumberCharacterArray , 20 );
    }

    if ( !isInserted )
    {
        combinedNumbersOutputFile << combinedNumber << endl;
        isInserted = true;
    }

    isFirstCombinedFileActive = !isFirstCombinedFileActive;

    combinedNumbersOutputFile.close ();
    combinedNumbersInputFile.close ();
}

【问题讨论】:

  • 一次读取 20 MB 的数字,对每个子集进行排序,将每个子集写入不同的文件,合并文件。
  • 如果我遇到这个问题,我会研究不同的排序算法(有很多),看看是否可以使用任何方法来帮助处理这些约束。正如@NathanOliver 指出的那样,这个middle number 可能在没有完全排序的情况下可用......
  • @Bijan 等等……你是直接给了一个文件,还是一次只给一个数字?
  • @Justin 正如我所说,这是竞赛的一部分。用户传递给程序的数字有“500”,然后程序必须使用以下函数生成所有可能的数字,最后它必须打印排序数字中间的数字。 f ( x , y , z ) = z * ( y * ( x + 1 ) + 1 )。
  • @Bijan 那么您甚至必须编写文件,还是只计算中间数?

标签: c++ sorting


【解决方案1】:

假设:

我假设数字列表已经是二进制形式(因为我们需要多次遍历数据,并且每次将文本转换为二进制都需要额外的处理时间)。那将是一个 1GB (125M * 64bit) 的文件。

目前还不清楚该文件的操作系统磁盘缓存是否计入内存限制。我认为不是,因为多次从磁盘读取一个 1GB 的冷文件已经花费了 5 秒以上。

解决方案:

让我们从一个简单的例子开始,说明如何做到这一点(我们稍后会优化和调整):

  • 首先创建一个数字范围的直方图(以 100 万组为例,但这还行不通 - 见下文)
  • 因此,创建一个大小为 max value / 1 million(现在太大)的 uint32 数组,我们将在其中放置存储桶的计数(0-999999、1000000-1999999 等)。
  • 循环遍历数字列表,每次递增数组的第 n 个值(数字所属的存储桶)。
  • 现在我们有了一个包含计数的数组,我们可以轻松计算出中位数将位于哪个桶(或范围)中。
  • 再次遍历列表,现在只将适合该范围的数字存储在数组中。
  • 对数组进行排序并计算哪个项目是中位数(也使用所有桶的计数)。

当然,我们需要在上面稍微调整一下。

首先,与其使用 100 万范围,不如使用 2 的幂。这样我们就可以简单地使用带有掩码的and 来获取桶/计数列表中的位置(而不是使用更昂贵的除法)。

其次,为了使用范围为 100 万的存储桶,我们必须创建一个太大的数组。

所以最好的选择是进行 3 次遍历:首先使用 1e12 的范围,然后对于中位数所在的范围,我们再次循环使用 1e6 的范围(但使用 2 的幂)。

这样,您只需对属于一个小桶的数字进行排序,而不是对整个 1.25 亿个集合进行排序。排序需要O(n log n)


问题中给出的数字示例:

23
8
16
42
15
4
108

使用 16 个桶/范围 - 第一次通过:

array_pos   count
0 (0-15)      3
1 (16-31)     2
2 (32-47)     1
3 (48-63)     0
4 (64-79)     0
5 (80-95)     0
6 (96-111)    1

我们现在可以计算出中位数必须在桶中array_pos 1。

remember/store these values:
Count before bucket 16-31: 3
Count  after bucket 16-31: 2

第二遍 - 读取桶 (16-31) 的值 - (同样,如果桶大小是 2 的幂,我们可以使用一些位掩码来快速检查数字是否在范围内):

23
16

对这个小数组进行排序,并使用 2 个计数(beforeafter)计算中位数的位置。

count
3
    16 -> median
    23
2

【讨论】:

  • 我实际上认为这会起作用。我会试试这个。
  • @Bijan 唯一的问题可能是 I/O 是瓶颈。从 100MB/s 硬盘上的磁盘读取 1GB 冷数据已经需要 10 秒.. - 但是一旦数据被缓存,它应该需要不到一秒的时间才能通过它(假设数字以二进制形式存储)。跨度>
  • 标题确实说巨大的文本文件
  • @jxh 是的,我也为文本文件提供了答案。但看起来 OP 正在生成文件(以保持低于 20GB 的已用内存),因此更好的选择是将这些数字保存为二进制而不是将它们转换为文本(5 秒限制)。
【解决方案2】:

您真正需要的是针对此类问题的分而治之算法。 查看External Sorting中的外部合并排序和分布排序部分

这个想法是将数据分类到多个块中,然后使用分而治之的方法再次合并这些块。

它的时间复杂度为O(n logn),我认为会超过时间限制。

这些算法非常有名,你可以谷歌获取实现细节。

【讨论】:

    【解决方案3】:

    在我的first answer 中,我给出了一个解决方案,可以在一个列表或一组二进制数(有内存限制)中找到中位数,而无需对整个集合进行排序。

    只是为了好玩,让我们看一个解决方案,其中文件包含数字作为由换行符分隔的文本,让我们不将文本转换为二进制数字(这可能很昂贵,而且我们不能将它们保存在内存中) )。

    同样,我们将使用存储桶(或存储桶计数),但我们从按位数分组开始。

    样本集:

    1265
    12
    6548122
    21516
    6548455
    516831213
    2155
    21158699
    54866
    

    第一遍 - 按位数分组(array_pos 这次是位数):

    array_pos  count
    0            0
    1            0
    2            1
    3            0
    4            2
    5            2
    6            0
    7            2
    8            1
    9            1
    

    所以,中位数必须有 5 位数字 (before: 3 - after:4)。

    第二遍 - (假设所有 5 位数字都不适合 20MB),读取所有 5 位数字并按第一个数字(或前 2、3 或 4 个,取决于计数)对它们进行分组(计数) :

    first_digit  count
    1              0
    2              1
    3              0
    4              0
    5              1
    

    (实际上第二遍也可以在第一遍中完成,因为在这种情况下数组会很小(取决于我们分组的位数)。我们只需要为每个“数字”创建一个数组位数')。

    找到包含中位数的组:

    count  first_digit
    3
        1        2
        1        5 -> median
    4
    

    最后一遍 - 读取所有 5 位数字,第一个数字为 5,对它​​们进行排序(可以按字母顺序,仍然不需要转换)并找到中位数(同样,我们只需对一小部分数据进行排序) .

    在上面的小例子中只有一个,但由于内存限制,我们没有存储结果,因此我们仍然必须在文件中获取它。

    出于性能原因,此处应避免使用 readline()streaming 之类的函数 - 而是应以二进制模式打开文件。这样我们就可以直接遍历字节并在遇到换行符时重置数字计数。

    使用memory mapping 会更好,但我想在这种情况下会作弊(20GB 限制)。

    【讨论】:

      【解决方案4】:

      你可以试试median of medians算法。它是一种就地算法,时间复杂度为O(n)。
      1.Read here
      2. Wikipedia Article

      【讨论】:

        猜你喜欢
        • 2021-02-15
        • 2015-09-03
        • 2012-04-21
        • 2017-10-22
        • 2011-11-28
        • 1970-01-01
        • 2020-09-08
        • 2022-01-01
        相关资源
        最近更新 更多