Сравнение 2 огромных списков с использованием C # несколько раз (с изюминкой) - PullRequest
4 голосов
/ 26 июня 2009

Привет всем, отличное сообщество у вас здесь. Я инженер-электрик, выполняю некоторую «программную» работу на стороне, чтобы помочь оплатить счета. Я говорю это потому, что хочу, чтобы вы приняли во внимание, что у меня нет надлежащего обучения информатике, но я программирую последние 7 лет.

У меня есть несколько таблиц Excel с информацией (все числовые), в основном это «набранные телефонные номера» в одном столбце и количество минут до каждого из этих номеров в другом. Отдельно у меня есть список «кодовых номеров префиксов операторов» для разных операторов в моей стране. Я хочу разделить весь «трафик» на каждого оператора. Вот сценарий:

Первая строка набранного номера : 123456789ABCD, 100 <- это будет 13-значный номер телефона и 100 минут. </p>

У меня есть список из 12 000+ префиксных кодов для носителя 1, эти коды различаются по длине, и мне нужно проверить каждый из них:

Код префикса 1 : 1234567 <- этот код состоит из 7 цифр. </p>

Мне нужно проверить первые 7 цифр для набранного номера и сравнить его с набранным номером. Если совпадение найдено, я добавлю количество минут к промежуточной сумме для дальнейшего использования. Обратите внимание, что не все префиксные коды имеют одинаковую длину, иногда они короче или длиннее.

Большая часть этого должна быть частью пирога, и я мог бы быть в состоянии сделать это, но я немного напуган огромным количеством данных; Иногда списки набранных номеров состоят из 30 000 номеров, а «код префикса оператора» составляет около 13 000 строк, и я обычно проверяю 3 оператора, что означает, что мне приходится проводить много «матчей».

У кого-нибудь есть идеи, как сделать это эффективно с помощью C #? Или любой другой язык, чтобы быть честным. Мне нужно делать это довольно часто, и разработка такого инструмента будет иметь гораздо больше смысла. Мне нужна хорошая перспектива от кого-то, кто имеет этот опыт "Computer Scientist".

Списки не обязательно должны быть в листах Excel, я могу экспортировать их в CSV-файл и работать оттуда, мне не нужен интерфейс "MS Office".

Спасибо за вашу помощь.

Обновление:

Спасибо всем за то, что уделили время на ответ на мой вопрос. Я полагаю, что в своем невежестве я преувеличил слово «эффективный». Я не выполняю эту задачу каждые несколько секунд. Это то, что я должен делать один раз в день, и я ненавижу делать с Excel, VLOOKUP и т. Д.

Я узнал о новых концепциях от вас, ребята, и я надеюсь, что смогу найти решение (я), используя ваши идеи.

Ответы [ 7 ]

11 голосов
/ 26 июня 2009

UPDATE

Вы можете сделать простой трюк - сгруппировать префиксы по их первым цифрам в словаре и сопоставлять числа только с правильным подмножеством. Я проверил это с помощью следующих двух операторов 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    Runtime
    -----------------
     1     10.198 ms
     2      1.179 ms
     3        205 ms
     4        130 ms
     5        107 ms

Просто чтобы дать общее представление - я сделал всего один прогон, и у меня много других вещей.

Оригинальный ответ

Я бы не заботился о производительности - обычный настольный ПК может спокойно работать с таблицами базы данных со 100 миллионами строк. Возможно, это займет пять минут, но я предполагаю, что вы не хотите выполнять задачу каждую секунду.

Я только что сделал тест. Я создал список с 15.000 уникальных префиксов с 5-10 цифрами. Из этих префиксов я сгенерировал 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();

Ну, это заняло около минуты на моем ноутбуке Core 2 Duo с 2,0 ГГц. Поэтому, если приемлемо время обработки в одну минуту, может быть, две или три, если вы включите агрегацию, я бы не стал ничего оптимизировать. Конечно, было бы очень хорошо, если бы программа могла выполнить задачу за секунду или две, но это добавит немного сложности и многих вещей, которые могут ошибиться. И на разработку, написание и тестирование уходит время. Оператор 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; }
        }
    }
}
8 голосов
/ 26 июня 2009

Звучит так, будто вам нужно построить три из префиксов оператора. В конечном итоге вы получите один три, где конечные узлы сообщат вам об этом префиксе.

Затем создайте словарь от носителя до int или long (всего).

Затем для каждой строки набранного номера просто двигайтесь вниз по дереву, пока не найдете перевозчика. Найдите общее количество минут для оператора и добавьте текущую строку, а затем продолжайте.

1 голос
/ 26 июня 2009

Как насчет выгрузки ваших данных в пару таблиц базы данных и последующего запроса к ним с помощью 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, но должно быть очень простым для перевода для любой другой СУБД.)

1 голос
/ 26 июня 2009

Самой простой структурой данных, которая сделала бы это достаточно эффективно, был бы список наборов. Сделайте набор для каждого оператора, который будет содержать все префиксы.

Теперь, чтобы связать звонок с оператором:

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, вы должны знать, что нужно извлекать и искать только эти длины, каждый раз просматривая набор префиксов этой длины.

0 голосов
/ 26 июня 2009

Вы можете использовать HashTable в C #.

Таким образом, у вас есть пары ключ-значение, и вашими ключами могут быть номера телефонов, а вашим значением - общее количество минут. Если в наборе ключей найдено совпадение, измените общее количество минут, иначе добавьте новый ключ.

Тогда вам просто нужно изменить алгоритм поиска, чтобы не просматривать весь ключ, а только его первые 7 цифр.

0 голосов
/ 26 июня 2009

Возможно, я бы просто поместил записи в список, отсортировал их, а затем использовал бы двоичный поиск для поиска совпадений. Адаптируйте критерии соответствия бинарного поиска, чтобы получить первый соответствующий элемент, затем выполняйте итерацию по списку, пока не найдете тот, который не соответствует. Бинарный поиск занимает всего около 15 сравнений для поиска в списке из 30 000 элементов.

0 голосов
/ 26 июня 2009

Может быть, было бы проще (не обязательно более эффективно) сделать это в базе данных вместо C #.

Вы можете вставить строки в базу данных, а при вставке определить носителя и включить его в запись (возможно, в триггер вставки).

Тогда вашим отчетом будет запрос суммы на таблицу.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...