С# самое быстрое пересечение двух наборов отсортированных чисел

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

Я пробовал кучу простых опций и в настоящее время использую это:

foreach (var index in firstSet)
{
    if (secondSet.BinarySearch(index) < 0)
        continue;

    //do stuff
}

Оба firstSet и secondSet имеют тип List.

Я также пытался использовать LINQ:

var intersection = firstSet.Where(t => secondSet.BinarySearch(t) >= 0).ToList();

а затем перейдя через intersection.

Но поскольку оба этих набора отсортированы, я чувствую, что есть лучший способ сделать это. Обратите внимание, что я не могу удалить элементы из наборов, чтобы сделать их меньше. Оба набора обычно состоят примерно из 50 элементов.

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

ПРИМЕЧАНИЕ. Я делаю это примерно 5,3 миллиона раз. Таким образом, каждая микросекунда считается.

Ответ 1

Если у вас есть два набора, которые были отсортированы, вы можете реализовать более быстрое пересечение, чем все, что предоставлено из коробки LINQ.

В принципе, держите два курсора IEnumerator<T> открытым, по одному для каждого набора. В любой момент, вперед, в зависимости от того, что имеет меньшее значение. Если они совпадают в любой момент, продвигайте их обоих и так далее, пока не достигнете конца либо итератора.

Самое приятное в том, что вам нужно только перебирать каждый набор один раз, и вы можете сделать это в памяти O (1).

Здесь пример реализации - непроверенный, но он компилируется:) Предполагается, что обе входящие последовательности не дублируются и сортируются, как в соответствии с предоставленным компаратором (передать Comparer<T>.Default):

(Там больше текста в конце ответа!)

static IEnumerable<T> IntersectSorted<T>(this IEnumerable<T> sequence1,
    IEnumerable<T> sequence2,
    IComparer<T> comparer)
{
    using (var cursor1 = sequence1.GetEnumerator())
    using (var cursor2 = sequence2.GetEnumerator())
    {
        if (!cursor1.MoveNext() || !cursor2.MoveNext())
        {
            yield break;
        }
        var value1 = cursor1.Current;
        var value2 = cursor2.Current;

        while (true)
        {
            int comparison = comparer.Compare(value1, value2);
            if (comparison < 0)
            {
                if (!cursor1.MoveNext())
                {
                    yield break;
                }
                value1 = cursor1.Current;
            }
            else if (comparison > 0)
            {
                if (!cursor2.MoveNext())
                {
                    yield break;
                }
                value2 = cursor2.Current;
            }
            else
            {
                yield return value1;
                if (!cursor1.MoveNext() || !cursor2.MoveNext())
                {
                    yield break;
                }
                value1 = cursor1.Current;
                value2 = cursor2.Current;
            }
        }
    }
}

EDIT: Как отмечено в комментариях, в некоторых случаях у вас может быть один вход, который намного больше, чем другой, и в этом случае вы могли бы сэкономить много времени, используя двоичный поиск для каждого элемента из меньшего набора в пределах большой набор. Это требует произвольного доступа к более крупному набору, однако (это просто предпосылка бинарного поиска). Вы даже можете сделать это немного лучше, чем наивный бинарный поиск, используя совпадение от предыдущего результата, чтобы дать нижнюю границу двоичного поиска. Предположим, что вы искали значения 1000, 2000 и 3000 в наборе с каждым целым числом от 0 до 19999. На первой итерации вам нужно будет просмотреть весь набор - ваши начальные нижние/верхние индексы будут равны 0 и 19 999 соответственно. Однако после того, как вы нашли совпадение с индексом 1000, следующий шаг (где вы ищете 2000) может начинаться с более низкого индекса 2000 года. По мере того, как вы продвигаетесь, диапазон, в котором вам нужно искать, постепенно сужается. Независимо от того, стоит ли это для дополнительной стоимости реализации или нет, это другое дело.

Ответ 2

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

Это решение хранит "указатель" на ту часть списка, которую мы еще не изучили, и сравнивает первое не проверенное количество каждого списка между ними. Если один меньше другого, указатель на список, к которому он принадлежит, увеличивается, чтобы указать на следующее число. Если они равны, число добавляется к результату пересечения и оба указателя увеличиваются.

var firstCount = firstSet.Count;
var secondCount = secondSet.Count;
int firstIndex = 0, secondIndex = 0;
var intersection = new List<int>();

while (firstIndex < firstCount && secondIndex < secondCount)
{
    var comp = firstSet[firstIndex].CompareTo(secondSet[secondIndex]);
    if (comp < 0) {
        ++firstIndex;
    }
    else if (comp > 0) {
        ++secondIndex;
    }
    else {
        intersection.Add(firstSet[firstIndex]);
        ++firstIndex;
        ++secondIndex;
    }
}

Вышеприведенный подход C-стиля для учебников, предназначенный для решения этой конкретной проблемы, и учитывая простоту кода, я был бы удивлен, увидев более быстрое решение.

Ответ 3

Вы используете довольно неэффективный метод Linq для такого рода задач, вы должны выбрать Intersect как отправную точку.

var intersection = firstSet.Intersect(secondSet);

Попробуйте это. Если вы измеряете его на производительность и все еще считаете его громоздким, плачьте за дополнительной помощью (или, возможно, следуйте за подходом Джона Скита).

Ответ 4

Я использовал подход Jon, но мне нужно было выполнить это пересечение сотни тысяч раз для массовой операции на очень больших наборах и требовать большей производительности. Случай, в который я работал, был сильно несбалансированным размером списков (например, 5 и 80 000) и хотел избежать повторения всего большого списка.

Я обнаружил, что обнаружение дисбаланса и переход на альтернативный алгоритм дали мне огромные преимущества по сравнению с конкретными наборами данных:

public static IEnumerable<T> IntersectSorted<T>(this List<T> sequence1,
        List<T> sequence2,
        IComparer<T> comparer)
{
    List<T> smallList = null;
    List<T> largeList = null;

    if (sequence1.Count() < Math.Log(sequence2.Count(), 2))
    {
        smallList = sequence1;
        largeList = sequence2;
    }
    else if (sequence2.Count() < Math.Log(sequence1.Count(), 2))
    {
        smallList = sequence2;
        largeList = sequence1;
    }

    if (smallList != null)
    {
        foreach (var item in smallList)
        {
            if (largeList.BinarySearch(item, comparer) >= 0)
            {
                yield return item;
            }
        }
    }
    else
    {
        //Use Jon method
    }
}

Я до сих пор не уверен в том, в какой момент вы просто ломаетесь, нужно провести еще несколько тестов

Ответ 5

попробуйте

firstSet.InterSect (secondSet).ToList ()

или

firstSet.Join(secondSet, o => o, id => id, (o, id) => o)