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

How to sort integer numbers in a huge text file?

本文关键字:文件 文本 整数 排序 巨大 一个      更新时间:2023-10-16

问题陈述

我得到了一个很大的数字列表,一次一个,我需要打印">中值"。

为了更清楚,可以有">125000000"个数字,并保证每个数字都小于"<em+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;

所以我在网上搜索了一下,但没有找到任何超过这些限制的答案。


方法

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


想法

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

假设:

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

也不清楚该文件的操作系统磁盘缓存是否会计入内存限制。我认为不是,因为多次从磁盘冷读取1GB的文件已经需要5秒钟以上的时间。

解决方案:

因此,让我们从一个如何做到这一点的简单例子开始(我们稍后将对此进行优化和调整):

  • 首先创建一个数字范围的直方图(例如100万组,但这还不起作用-见下文)
  • 因此,创建一个大小为max value / 1 million(目前太大)的uint32数组,我们将在其中放置bucket的计数(0-999999、100000-1999999,依此类推)
  • 循环浏览数字列表,每次递增数组的第n个值(数字所属的bucket)
  • 既然我们有了一个计数数组,我们就可以很容易地计算出中值在哪个区间(或范围)
  • 再次循环列表,现在只将符合该范围的数字存储在数组中
  • 对数组进行排序,并计算哪一项是中值(同时使用所有存储桶的计数)

当然,我们需要对上面的内容进行一点调整。

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

其次,对于使用范围为100万的bucket,我们必须创建一个太大的数组。

因此,最好的选择是进行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_pos1的桶中。

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

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

23
16

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

count
3
16 -> median
23
2

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

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

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

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

在我的第一个答案中,我给出了一个解决方案,可以在二进制数列表或集合中找到中值(有内存限制),而不必对整个集合进行排序。

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

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

样本集:

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)会作弊。

您可以尝试中值算法。它是一种时间复杂度为O(n)的就地算法
1.阅读此处
2。维基百科文章