如何在巨大的文本文件中对整数进行排序?

时间:2018-07-09 17:28:10

标签: c++ sorting

问题陈述

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

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

这是一场比赛,因此存在内存限制(最高20 MB)时间限制(最高5秒)

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

23
8
16
42
15
4
108

对数字进行排序后:

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

中位数”将为16;

因此,我在Internet上进行了搜索,但找不到任何通过这些限制的答案。


方法

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


想法

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

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

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

请求

因此,我想优化这些想法之一以通过极限,或者通过任何新想法通过这些极限


首选项

我更喜欢使用第二个主意,因为与其他两个主意不同,它通过了 limits (限制),但是我不能这样做,因为我不知道如何在文本文件的中间插入一行。因此,如果我了解了这一点,其余的过程将非常容易。


尝试的解决方案

此功能可以接收数字,并通过读取文件找到最佳位置并将其放置在该位置。
实际上,它代表了我的第三个​​想法
这样就可以了(我通过大量输入对其进行了测试),但是前面提到的问题是时间限制

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 ();
}

4 个答案:

答案 0 :(得分:2)

假设:

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

还不清楚该文件的OS磁盘缓存是否会计入内存限制。我会假设不是,因为多次从磁盘中读取1GB的文件会花费超过5秒钟的时间。

解决方案:

因此,让我们从一个简单的示例开始做起(我们稍后将对其进行优化和调整):

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

当然,我们需要对以上内容进行一些调整。

首先,与其使用一百万的范围,不如使用二的幂。这样,我们可以简单地使用带有掩码的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

答案 1 :(得分:1)

您真正需要的是一种解决此类问题的分治算法。 看看External Sorting

中的外部合并排序和分发排序部分

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

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

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

答案 2 :(得分:1)

在我的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的限制)会作弊。

答案 3 :(得分:0)

您可以尝试中位数中位数算法。这是一种就地算法,其时间复杂度为O(n)。
1。Read here
2。 Wikipedia Article