用于计算阵列中重复元素的高效算法,该算法不适合RAM

时间:2016-09-07 18:28:06

标签: arrays algorithm performance bigdata

我认为这个问题在很大程度上描述了这个问题。我需要计算一个数组中重复的元素,这些元素无法完全加载到计算机内存中。阵列的大小可以达到50Gb甚至更大。在我的特定任务中,该数组的元素是长度不超过256个字符的字符串,以UTF-8编码(总共512个字节)。字符串数量约为1亿。例如,如果我在输入上有以下数组(为简洁起见缩短了字符串):

VERY NICE ELEMENT_1
VERY NICE ELEMENT_1
VERY NICE ELEMENT_2
VERY NICE ELEMENT_2
NOT SO GOOD ELEMENT
NOT SO GOOD ELEMENT
BAD ELEMENT
BAD ELEMENT
BAD ELEMENT
PERFECT FIFTH ELEMENT

算法应该输出以下内容(可能不完全按照该顺序):

VERY NICE ELEMENT_1 2
VERY NICE ELEMENT_2 2
NOT SO GOOD ELEMENT 2
BAD ELEMENT 3
PERFECT FIFTH ELEMENT 1

换句话说,我需要做SELECT COUNT(*) GROUP BY所做的事。

我猜这个算法应该通过将元素划分成一些正常适合RAM的组来执行多个阶段的计数。然后它应该将这些组减少为一个组。但它怎么能做到这一点?它如何有效地合并这些组?

硬盘具有强大的无限容量。编程语言并不重要。我需要知道一个抽象的算法。我有类似任务的经验,我需要对那种数组进行排序。在那里我做了同样的事情,将所有元素划分为分区,然后将它们合并为一个文件。但在这种情况下,我不知道该怎么做"合并部分"。

提前感谢您的帮助。

5 个答案:

答案 0 :(得分:2)

的伪代码

第一阶段:部分处理+分区

M 成为发生字符串的地图

  1. 虽然未到达输入文件的末尾,但
  2. 从文件
  3. 中读取下一个字符串 s
  4. 如果 s 不在 M 中,请 M [ s ] = 0
  5. M [ s ] + = 1
  6. 如果 M 的大小超过某个限制,请按键排序 M ,将其写入新的临时文件 F i ,然后重置 M
  7. 循环结束
  8. 与第5步一样冲洗 M
  9. 第二阶段:合并

    输入:格式'键值'的文件{ F i }在每一行(按键排序)

    P 成为三元组{K V F}的优先级队列,其中排序仅由三元组的第一个元素执行,即{{1} }}

    1. 对于来自{ F i }的每个文件 f ,请阅读第一行' {{ 1}}'从 f 并将{K f }推送到 P
    2. 让current_key ='',current_value = 0
    3. P 不为空
    4. key value来自 P 的广告{key value f }
    5. 如果尚未到达 f 的结尾,请阅读下一行&#39; key1 value1&#39;从 f 并将{ f }推回 P ,否则关闭并删除 f < / LI>
    6. key2 value2如果key2 value2然后,则输出current_key == key1, 并设置current_value += value1current_key current_value
    7. 循环结束
    8. 输出current_key=key1
    9. Python实现

      current_value=value1

答案 1 :(得分:1)

您可以对文件进行排序,然后按顺序处理它并计算相同的项目,它们将在一起。然后,只要一个项目与前一个项目不同,您就可以动态输出结果记录。

答案 2 :(得分:1)

我只是根据每行的哈希码将文件拆分成多个文件。从1x 50GB文件制作1000x 50MB文件。然后分别处理每个文件,它会毫无问题地适应内存。

protected static string[] Partition(string inputFileName, string outPath, int partitions)
{
    string[] fileNames = Enumerable.Range(0, partitions)
        .Select(i => Path.Combine(outPath, "part" + i))
        .ToArray();

    StreamWriter[] writers = fileNames
        .Select(fn => new StreamWriter(fn))
        .ToArray();

    StreamReader file = new StreamReader(inputFileName);
    string line;
    while ((line = file.ReadLine()) != null)
    {
        int partition = Math.Abs(line.GetHashCode() % partitions);
        writers[partition].WriteLine(line);
    }
    file.Close();

    writers.AsParallel().ForAll(c => c.Close());

    return fileNames;
}

protected static void CountFile(string inputFileName, StreamWriter writer)
{
    Dictionary<string, int> dict = new Dictionary<string, int>();

    StreamReader file = new StreamReader(inputFileName);
    string line;
    while ((line = file.ReadLine()) != null)
    {
        int count;
        if (dict.TryGetValue(line, out count))
        {
            dict[line] = count + 1;
        }
        else
        {
            dict.Add(line, 1);
        }
    }
    file.Close();

    foreach (var kv in dict)
    {
        writer.WriteLine(kv.Key + ": " + kv.Value);
    }
}

protected static void CountFiles(string[] fileNames, string outFile)
{
    StreamWriter writer = new StreamWriter(outFile);
    foreach (var fileName in fileNames)
    {
        CountFile(fileName, writer);
    }
    writer.Close();
}

static void Main(string[] args)
{
    var fileNames = Partition("./data/random2g.txt", "./data/out", 211);           
    CountFiles(fileNames, "./data/random2g.out");
}

<强>基准

我决定尝试比较排序方法(Leon)和散列。如果你真的不需要它,那么排序是相当多的工作。我制作了20亿个数字的文件。分布(long)Math.Exp(rnd.NextDouble() * 30)以相同的概率产生所有长度的数字(最多14个)。此分布产生许多唯一值,但同时也会重复多次。甚至角色的概率也各不相同这对人工数据来说并不坏。

File size: 16,8GiB
Number of lines: 2G (=2000000000)
Number of distinct lines: 576M
Line occurences: 1..46M, average: 3,5
Line length: 1..14, average: 7
Used characters: '0', '1',...,'9'
Character frequency: 8,8%..13%, average: 10%
Disc: SSD

排序结果

10M lines in partition
10M distinct lines in partition
114 partitions
Partition size: 131MiB
Sum of partitions size: 14,6GiB
Partitioning time: 105min
Merging time: 180min
Total time: 285min (=4hod 45min)

这种方法可以节省空间,因为分区包含部分合并的数据。

哈希结果

7M..54M lines in partition, average: 9,5M
2723766..2732318 distinct lines in partition, average: 2,73M
211 partitions
Partition size 73MiB..207MiB, average: 81MiB
Sum of partitions size: 16,8GiB
Partitioning time: 6min
Merging time: 15min
Total time: 21min

虽然每个分区的大小不同,但所有分区中不同行的数量几乎相同。这意味着哈希函数按预期工作。处理每个分区所需的内存是相同的。但确实无法保证,因此如果需要高可靠性,则必须为这些情况添加一些回退策略(将文件重新编码为更小的文件,切换到该文件的排序等)。机会是,它永远不会真正使用,所以从性能的角度来看,这不是问题。

Hashing beats以10倍以上的因子排序,另一方面,其中一些可能源于python本身效率低下。

答案 3 :(得分:0)

浏览文件并将第一个字母的行索引放入文件中。

一个3,45,23 ... b 112,34,546 ......

然后你可以用并行处理这些,因为你只需要每个文件再检查一次。

至少那是我的第一个想法。

显然,如果单词大多是随机的,并且不是所有内容都以相同的字母开头,或者最坏的情况,每个单词都是相同的,那么这是最好的。

答案 4 :(得分:0)

使用hadoop框架并对输入字符串执行map reduce。