【问题标题】:Comparing 2 huge lists using C# multiple times (with a twist)多次使用 C# 比较 2 个巨大的列表(稍作改动)
【发布时间】:2009-06-25 21:07:56
【问题描述】:

大家好,你们来到这里的社区很棒。我是一名电气工程师,一边做一些“编程”工作以帮助支付账单。我这样说是因为我想让你考虑到我没有接受过适当的计算机科学培训,但我在过去的 7 年里一直在编码。

我有几个带有信息的 Excel 表格(全是数字),基本上它是在一列中的“拨打的电话号码”和到另一列中每个数字的分钟数。另外,我有一个我所在国家/地区不同运营商的“运营商前缀代码”列表。我想要做的是将每个运营商的所有“流量”分开。这是场景:

第一个拨打的号码行123456789ABCD,100

我有一个运营商 1 的 12,000 多个前缀代码的列表,这些代码的长度各不相同,我需要检查每个人:

前缀代码 11234567

我需要检查所拨号码的前 7 位数字并将其与所拨号码进行比较,如果找到匹配项,我会将分钟数添加到小计中以备后用。 请注意,并非所有前缀代码的长度都相同,有时它们会更短或更长。

其中大部分应该是小菜一碟,我应该能够做到,但我对大量数据感到有点害怕;有时拨打的号码列表包含多达 30,000 个号码,“运营商前缀代码”列表大约 13,000 行长,我通常检查 3 个运营商,这意味着我必须做很多“匹配”。

有没有人知道如何使用 C# 有效地做到这一点?或任何其他语言,老实说。我需要经常这样做,并且设计一个工具来做到这一点会更有意义。我需要一个具有“计算机科学家”背景的人的良好视角。

列表不需要在 excel 工作表中,我可以导出到 csv 文件并从那里工作,我不需要“MS Office”界面。

感谢您的帮助。

更新:

感谢大家花时间回答我的问题。我想在我的无知中我过分夸大了“高效”这个词。我不会每隔几秒钟执行一次这项任务。这是我每天必须做一次的事情,我讨厌使用 Excel 和 VLOOKUP 等。

我从你们那里学到了新的概念,我希望我可以利用你们的想法构建一个或多个解决方案。

【问题讨论】:

  • 虽然我真的很喜欢 Skeet 的 trie 建议,但我认为您不必担心这个特定应用程序的效率。 30,000 个数字对一台计算机来说并不多——即使在一台糟糕的机器上,我也无法想象计算总数字需要超过 10 秒的时间。此外,您可以一次在内存中存储 100 万个整数,而且仍然小于 4MB。记住他们所说的过早优化...
  • 谢谢朱丽叶,我没这么看。
  • 由于运营商前缀代码的长度可能不同,任何运营商前缀代码都可以是更长运营商前缀代码的前缀(即前缀 123 和 12345)吗?如果是这样,哪个优先?
  • 这些列表总是不同的。所有电话号码的长度都是 13 位,因此“123”前缀意味着它包括使用“123”+XXXXXXXXXX 的所有可能的谴责。这意味着类似:“111111111111”的内容包括 10 个派生词:“1111111111110、1111111111111、1111111111112 等)。希望这足够清楚。英语不是我的第一语言。

标签: c# list compare matching


【解决方案1】:

更新

您可以做一个简单的技巧 - 将前缀按其第一个数字分组到字典中,并仅将数字与正确的子集进行匹配。我假设每个前缀至少有三个数字,我使用以下两个 LINQ 语句对其进行了测试。

const Int32 minimumPrefixLength = 3;

var groupedPefixes = prefixes
    .GroupBy(p => p.Substring(0, minimumPrefixLength))
    .ToDictionary(g => g.Key, g => g);

var numberPrefixes = numbers
    .Select(n => groupedPefixes[n.Substring(0, minimumPrefixLength)]
        .First(n.StartsWith))
    .ToList();

那么这有多快? 15.000 个前缀和 50.000 个数字只用了不到 250 毫秒。两行代码足够快吗?

请注意,性能很大程度上取决于最小前缀长度 (MPL),因此取决于您可以构造的前缀组的数量。

MPL 运行时 ----------------- 1 10.198 毫秒 2 1.179 毫秒 3 205 毫秒 4 130 毫秒 5 107 毫秒

只是给出一个粗略的想法 - 我只跑了一次,还有很多其他事情要做。

原答案

我不太关心性能——一台普通的台式电脑可以轻松处理具有 1 亿行的数据库表。也许需要五分钟,但我假设您不想每隔一秒执行一次任务。

我刚刚做了一个测试。我生成了一个包含 5 到 10 位数字的 15.000 个唯一前缀的列表。根据这些前缀,我生成了 50.000 个带有前缀和额外 5 到 10 位数字的号码。

List<String> prefixes = GeneratePrefixes();
List<String> numbers = GenerateNumbers(prefixes);

然后我使用以下 LINQ to Object 查询来查找每个数字的前缀。

var numberPrefixes = numbers.Select(n => prefixes.First(n.StartsWith)).ToList();

嗯,在我的 2.0 GHz Core 2 Duo 笔记本电脑上花了大约一分钟。因此,如果一分钟的处理时间是可以接受的,如果包括聚合,可能是两到三分钟,我不会尝试优化任何东西。当然,如果程序可以在一两秒内完成任务,那将是非常好的,但这会增加相当多的复杂性和许多出错的地方。设计、编写和测试都需要时间。 LINQ 语句只花了我几秒钟的时间。

测试应用程序

请注意,生成许多前缀确实很慢,可能需要一两分钟。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;

namespace Test
{
    static class Program
    {
        static void Main()
        {
            // Set number of prefixes and calls to not more than 50 to get results
            // printed to the console.

            Console.Write("Generating prefixes");
            List<String> prefixes = Program.GeneratePrefixes(5, 10, 15);
            Console.WriteLine();

            Console.Write("Generating calls");
            List<Call> calls = Program.GenerateCalls(prefixes, 5, 10, 50);
            Console.WriteLine();

            Console.WriteLine("Processing started.");

            Stopwatch stopwatch = new Stopwatch();

            const Int32 minimumPrefixLength = 5;

            stopwatch.Start();

            var groupedPefixes = prefixes
                .GroupBy(p => p.Substring(0, minimumPrefixLength))
                .ToDictionary(g => g.Key, g => g);

            var result = calls
                .GroupBy(c => groupedPefixes[c.Number.Substring(0, minimumPrefixLength)]
                    .First(c.Number.StartsWith))
                .Select(g => new Call(g.Key, g.Sum(i => i.Duration)))
                .ToList();

            stopwatch.Stop();

            Console.WriteLine("Processing finished.");
            Console.WriteLine(stopwatch.Elapsed);

            if ((prefixes.Count <= 50) && (calls.Count <= 50))
            {
                Console.WriteLine("Prefixes");
                foreach (String prefix in prefixes.OrderBy(p => p))
                {
                    Console.WriteLine(String.Format("  prefix={0}", prefix));
                }

                Console.WriteLine("Calls");
                foreach (Call call in calls.OrderBy(c => c.Number).ThenBy(c => c.Duration))
                {
                    Console.WriteLine(String.Format("  number={0} duration={1}", call.Number, call.Duration));
                }

                Console.WriteLine("Result");
                foreach (Call call in result.OrderBy(c => c.Number))
                {
                    Console.WriteLine(String.Format("  prefix={0} accumulated duration={1}", call.Number, call.Duration));
                }
            }

            Console.ReadLine();
        }

        private static List<String> GeneratePrefixes(Int32 minimumLength, Int32 maximumLength, Int32 count)
        {
            Random random = new Random();
            List<String> prefixes = new List<String>(count);
            StringBuilder stringBuilder = new StringBuilder(maximumLength);

            while (prefixes.Count < count)
            {
                stringBuilder.Length = 0;

                for (int i = 0; i < random.Next(minimumLength, maximumLength + 1); i++)
                {
                    stringBuilder.Append(random.Next(10));
                }

                String prefix = stringBuilder.ToString();

                if (prefixes.Count % 1000 == 0)
                {
                    Console.Write(".");
                }

                if (prefixes.All(p => !p.StartsWith(prefix) && !prefix.StartsWith(p)))
                {
                    prefixes.Add(stringBuilder.ToString());
                }
            }

            return prefixes;
        }

        private static List<Call> GenerateCalls(List<String> prefixes, Int32 minimumLength, Int32 maximumLength, Int32 count)
        {
            Random random = new Random();
            List<Call> calls = new List<Call>(count);
            StringBuilder stringBuilder = new StringBuilder();

            while (calls.Count < count)
            {
                stringBuilder.Length = 0;

                stringBuilder.Append(prefixes[random.Next(prefixes.Count)]);

                for (int i = 0; i < random.Next(minimumLength, maximumLength + 1); i++)
                {
                    stringBuilder.Append(random.Next(10));
                }

                if (calls.Count % 1000 == 0)
                {
                    Console.Write(".");
                }

                calls.Add(new Call(stringBuilder.ToString(), random.Next(1000)));
            }

            return calls;
        }

        private class Call
        {
            public Call (String number, Decimal duration)
            {
                this.Number = number;
                this.Duration = duration;
            }

            public String Number { get; private set; }
            public Decimal Duration { get; private set; }
        }
    }
}

【讨论】:

  • @Daniel:非常感谢,我也会试试你的方法。
  • @Daniel:如何将“分钟”SUM 合并到您的解决方案中?
  • 我在我的测试应用程序中添加了聚合呼叫持续时间 - 希望它有所帮助。
【解决方案2】:

在我看来,您需要从运营商前缀构建trie。您最终会得到一个单一的 trie,其中终止节点会告诉您该前缀的运营商。

然后创建一个从运营商到intlong(总数)的字典。

然后,对于每个拨打的号码行,沿着特里查找,直到找到运营商。找出运营商到目前为止的总分钟数,并添加当前行 - 然后继续。

【讨论】:

  • 这是一个很好(并且相对有效)的解决方案,但我想知道他是否需要自己生成 trie。显然,对于 12k 前缀代码,他不是自己生成的,而是需要使用工具来生成。因此,要么他正在制作一个自动代码生成器来创建然后针对他的数据运行的 trie,要么他使用更传统的方法。
  • 我在想象来自某处文件的前缀代码。当您阅读该文件时,构建 trie 相当容易。不过,最好对 数百万 行拨号号码使用相同的 trie,而不仅仅是 30,000。
  • @John Skeet:感谢您的回答,我对此很感兴趣,但不幸的是,我不具备进行此操作所需的所有必要知识,您能否详细说明您的回答?如果没有,我将继续“谷歌搜索”。谢谢。
  • 好吧,如果你搜索“trie C#”,你会发现一些你可以适应的实现,我敢肯定。您需要详细说明哪一点?
  • 谢谢乔恩,我会去找的。​​span>
【解决方案3】:

可以相当有效地做到这一点的最简单的数据结构是集合列表。为每个运营商创建一个集合以包含所有前缀。

现在,将呼叫与运营商关联:

foreach (Carrier carrier in carriers)
{
    bool found = false;

    for (int length = 1; length <= 7; length++)
    {
        int prefix = ExtractDigits(callNumber, length);

        if (carrier.Prefixes.Contains(prefix))
        {
            carrier.Calls.Add(callNumber);
            found = true;
            break;
        }
    }

    if (found)
        break;
}

如果您有 10 家运营商,则每次通话的集合中将有 70 次查找。但是在集合中查找并不太慢(比线性搜索快得多)。因此,与蛮力线性搜索相比,这应该会给您带来相当大的速度。

您可以更进一步,根据长度对每个运营商的前缀进行分组。这样一来,如果运营商只有长度为 7 和 4 的前缀,您就会知道只需提取和查找这些长度,每次都查看该长度的前缀集。

【讨论】:

    【解决方案4】:

    如何将您的数据转储到几个数据库表中,然后使用 SQL 查询它们?简单!

    CREATE TABLE dbo.dialled_numbers ( number VARCHAR(100), minutes INT )
    
    CREATE TABLE dbo.prefixes ( prefix VARCHAR(100) )
    
    -- now populate the tables, create indexes etc
    -- and then just run your query...
    
    SELECT p.prefix,
        SUM(n.minutes) AS total_minutes
    FROM dbo.dialled_numbers AS n
        INNER JOIN dbo.prefixes AS p
            ON n.number LIKE p.prefix + '%'
    GROUP BY p.prefix
    

    (这是为 SQL Server 编写的,但对于任何其他 DBMS 来说应该很容易翻译。)

    【讨论】:

    • @Luke:另一个我以前没有考虑过的好主意。作为“编码”技能的练习,我想尝试所有这些技能。
    【解决方案5】:

    也许在数据库而不是 C# 中执行此操作会更简单(不一定更有效)。

    您可以在数据库中插入行并在插入时确定载体并将其包含在记录中(可能在插入触发器中)。

    那么您的报告将是对表格的求和查询。

    【讨论】:

      【解决方案6】:

      我可能只是将条目放在一个列表中,对其进行排序,然后使用binary search 来查找匹配项。定制二分搜索匹配标准以返回第一个匹配的项目,然后沿着列表迭代,直到找到一个不匹配的项目。二进制搜索只需要大约 15 次比较即可搜索 30,000 个项目的列表。

      【讨论】:

        【解决方案7】:

        您可能希望在 C# 中使用 HashTable

        这样你就有了键值对,你的键可以是电话号码,你的值可以是总分钟数。如果在密钥集中找到匹配项,则修改总分钟数,否则,添加新密钥。

        然后你只需要修改你的搜索算法,不要查看整个密钥,而只查看它的前 7 位数字。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-10-31
          • 1970-01-01
          • 2015-02-21
          • 1970-01-01
          • 2019-09-13
          • 2023-03-22
          • 2021-09-27
          • 1970-01-01
          相关资源
          最近更新 更多