【问题标题】:How to remove duplicate lines from a large text file efficiently?如何有效地从大型文本文件中删除重复行?
【发布时间】:2017-04-04 19:24:39
【问题描述】:

我想编辑一个文本,因为其中每一行都存在一次。每行始终包含 10 个字符。我通常处理 5-6 百万行。所以我目前使用的代码消耗了太多的内存。

我的代码:

File.WriteAllLines(targetpath, File.ReadAllLines(sourcepath).Distinct())

那么我怎样才能同时减少 RAM 消耗和时间消耗?

【问题讨论】:

  • 我想一种方法是对每一行进行一个小哈希并以这种方式找到重复项(可能涉及一些排序)。然后使用该结果从原始文件中删除重复项。不过散列可能很棘手,我不是专家。
  • @bronlund 你如何将 10 个字符散列成一个使用更少空间且不会导致冲突的散列?
  • 重复的行是否相互跟随,或者文件中的最后一行是否可以与第一行重复?十个字符的行的确切属性是什么?必须在输出中维护输入文件的顺序吗?一种方法可能是首先将输入分块到单独的列表中,方法是开始字符,然后在处理下一行时搜索这些列表。
  • 无论如何,这段代码应该运行得很好。 “当前消耗了太多 RAM” 是什么意思?实际问题是什么?此代码消耗更少 RAM 的唯一方法是将数据“分桶”到文件中,并且一次只加载一个相关文件,这会降低性能。
  • @Jianping 你不想为此创建 5-6 百万个文件。

标签: c# text-files


【解决方案1】:

考虑到how much memory a string will take in C#,并假设我们得到的 600 万条记录的长度为 10 个字符:

  • 以字节为单位的大小 ~= 20 + (length / 2 ) * 4;
  • 以字节为单位的总大小 ~= (20 + ( 10 / 2 ) * 4 )* 6000000 = 240 000 000
  • 以 Mb 为单位的总大小 ~= 230

现在,即使在 x86(32 位系统)上,230 MB 的空间也不是问题,因此您可以将所有数据加载到内存中。 为此,我将使用HashSet class,这显然是一个哈希集,可以让您在添加元素之前使用查找轻松消除重复项。

时间复杂度的大 O 表示法而言,在哈希集中查找的平均性能为 O(1),这是您可以获得的最佳性能。总共,您将使用查找 N 次,总计 N * O(1) = O(N)

根据 空间复杂度的大 O 表示法,您将使用 O(N) 空间,这意味着您使用的内存与元素数量成正比,这也是你能得到的最好的。

我不确定如果您在 C# 中实现算法并且不依赖任何外部组件(这也将至少使用 O(N)),是否可以使用更少的空间

话虽如此,您可以通过逐行顺序读取文件来优化一些场景,请参阅here。 如果您有很多重复项,这会产生更好的结果,但最坏的情况是所有行都不同时会消耗相同数量的内存。

最后一点,如果你看看 Distinct 方法是如何实现的,你会发现它也使用了哈希表的实现,虽然不是同一个类,但性能还是大致相同的,查看this question了解更多详情。

【讨论】:

  • 只有一个重要问题。 HashSet 的 int32 散列。会有哈希冲突,哈希冲突不一定是行重复。当散列冲突发生时,Distinct 正在测试项目。
  • @ipavlu,是的,会有冲突,但是字符串散列实现在所有 int32 值中具有非常好的均匀分布,其中有超过 400 万个,而我们只有 600 万个值。此外,字符串的 HashSet 仅使用 int32 键进行哈希桶寻址,之后使用链接和比较来进行字符串相等。所以没有字符串实例因为相同的哈希而“丢弃”。这就是大多数哈希表的工作方式。 LINQ 使用 Set 类,它只是 HashSet 类的复制粘贴。见实现referencesource.microsoft.com
  • 你说得对,我还在考虑只保留散列而不保留数据,但 HashSet 两者兼而有之……
  • 顺便说一句,我进行了测试并从随机数创建了 6 百万行文件。很容易产生很多冲突,尤其是当我们对数据一无所知时。第一个存储桶中大约有 580 万(在我的情况下是字典),第二个存储桶不到 20 万,第三个存储桶很少。
【解决方案2】:

正如ironstone13 纠正我的那样,HashSet 没问题,但确实存储了数据。 那么这也可以正常工作:

        string[] arr = File.ReadAllLines("file.txt");
        HashSet<string> hashes = new HashSet<string>();

        for (int i = 0; i < arr.Length; i++)
        {
            if (!hashes.Add(arr[i])) arr[i] = null;
        }

        File.WriteAllLines("file2.txt", arr.Where(x => x != null));

此实现的动机是内存性能和哈希冲突。 主要思想是只保留散列,当然它必须返回文件以获取它认为是散列冲突/重复的行,以检测它是哪一个。 (那部分没有实现)。

class Program
{
    static string[] arr;
    static Dictionary<int, int>[] hashes = new Dictionary<int, int>[1]
    { new Dictionary<int, int>() }
    ;
    static int[] file_indexes = {-1};


    static void AddHash(int hash, int index)
    {
        for (int h = 0; h < hashes.Length; h++)
        {
            Dictionary<int, int> dict = hashes[h];
            if (!dict.ContainsKey(hash))
            {
                dict[hash] = index;
                return;
            }
        }
        hashes = hashes.Union(new[] {new Dictionary<int, int>() {{hash, index}}}).ToArray();
        file_indexes = Enumerable.Range(0, hashes.Length).Select(x => -1).ToArray();
    }

    static int UpdateFileIndexes(int hash)
    {
        int updates = 0;
        for (int h = 0; h < hashes.Length; h++)
        {
            int index;
            if (hashes[h].TryGetValue(hash, out index))
            {
                file_indexes[h] = index;
                updates++;
            }
            else
            {
                file_indexes[h] = -1;
            }
        }
        return updates;
    }

    static bool IsDuplicate(int index)
    {
        string str1 = arr[index];
        for (int h = 0; h < hashes.Length; h++)
        {
            int i = file_indexes[h];
            if (i == -1 || index == i) continue;
            string str0 = arr[i];
            if (str0 == null) continue;
            if (string.CompareOrdinal(str0, str1) == 0) return true;
        }
        return false;
    }


    static void Main(string[] args)
    {
        arr = File.ReadAllLines("file.txt");

        for (int i = 0; i < arr.Length; i++)
        {
            int hash = arr[i].GetHashCode();

            if (UpdateFileIndexes(hash) == 0) AddHash(hash, i);
            else if (IsDuplicate(i)) arr[i] = null;
            else AddHash(hash, i);
        }

        File.WriteAllLines("file2.txt", arr.Where(x => x != null));



        Console.WriteLine("DONE");
        Console.ReadKey();
    }
}

【讨论】:

  • 我认为你必须使用 Int64 类型的 Array.LongLength,而不是 Array.Length,因为它只是 Int32,显然对于 600 万个项目的输入来说太小了
  • @ironstone Int32 的最大值约为 20 亿(更准确地说是 2147483647),明显大于 600 万(6000000)。
  • @ipvalu 您说您的代码针对性能进行了优化,但您确实意识到arr = File.ReadAllLines("file.txt") 将文件的所有文本都存储在内存中,对吧?
  • @vyrp,没错,我无法计算 2^32 / 2 - 1 清楚地表明该睡觉了
  • @vyrp 我当然愿意 :)。我只是在玩只记住哈希码和行索引的想法,必要时,两个哈希码是相同的,要归档。使用给定的刚性文件结构,很容易找到请求的行索引。我提供的算法可以向那个方向扩展,它会大大限制内存消耗,但时间消耗会更糟。
【解决方案3】:

在您编写数据之前,如果您的数据在列表或字典中,您可以运行 LINQ 查询并使用 group by 对所有类似的键进行分组。然后每次写入输出文件。

你的问题也有点模糊。您是否每次都创建下一个文本文件并且必须以文本形式存储数据?有更好的格式可以使用,例如 XML 和 json

【讨论】:

  • 执行更多的操作,尤其是 LINQ 操作,可能会很快导致更多的处理时间和更多的 RAM,当然还有分组。这里不需要分组...
  • 为什么这个答案是公认的答案?使用 Linq 的组在时间和空间上都不是高性能的。其他答案使用 HashSet 更好。
  • @Ali Tor,这个建议并不比你原来的代码好
  • 克里斯,请注意,原来的问题是使用 .Distinct() 方法。它是 LINQ! :) 实际上不:XML/JSON 不是内存或处理时间性能更好的格式,它们是为了便于交换而开发的,开发人员可以轻松理解简单的纯文本。因此,如果您对内存和时间处理性能很认真的话,最近开发了像谷歌协议缓冲区这样的二进制格式......
  • 注意,个人从未尝试过在文本文件中处理这么多数据。总是有某种数据库。我在想如果他在内存中有这个信息,他可以使用 linq 通过 linq 删除重复项
猜你喜欢
  • 1970-01-01
  • 2015-01-17
  • 2020-01-01
  • 2010-11-17
  • 1970-01-01
  • 1970-01-01
  • 2018-01-31
  • 1970-01-01
相关资源
最近更新 更多