我一次给出一个非常大的数字列表,我需要打印“ 中位数”。
更清楚地说,可以有“ 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上进行了搜索,但找不到任何通过这些限制的答案。
我的方法是获取所有数字,将它们保存在文本文件中,对其进行排序,然后获取“ 中位数”。
因此,我想优化这些想法之一以通过极限,或者通过任何新想法通过这些极限。
我更喜欢使用第二个主意,因为与其他两个主意不同,它通过了 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 ();
}
答案 0 :(得分:2)
假设:
我将假定数字列表已经是二进制形式(因为我们将需要多次遍历数据,并且每次将文本转换为二进制将花费额外的处理时间)。那将是1GB(125M * 64bit)的文件。
还不清楚该文件的OS磁盘缓存是否会计入内存限制。我会假设不是,因为多次从磁盘中读取1GB的文件会花费超过5秒钟的时间。
解决方案:
因此,让我们从一个简单的示例开始做起(我们稍后将对其进行优化和调整):
uint32
(目前太大)的max value / 1 million
数组,我们将在其中放置存储桶的数量(0-999999、1000000-1999999,依此类推)。当然,我们需要对以上内容进行一些调整。
首先,与其使用一百万的范围,不如使用二的幂。这样,我们可以简单地使用带有掩码的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个计数(before
和after
)计算中位数的位置。
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