C# Самое быстрое пересечение 2 наборов отсортированных чисел
Я вычисляю пересечение 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 миллиона раз. Так что каждая микросекунда на счету.5 ответов:
Если у вас есть два набора, которые сортируются, вы можете реализовать более быстрое пересечение, чем все, что предоставляется из коробки с LINQ.
В основном, держите открытыми два курсора
IEnumerator<T>
, по одному для каждого набора. В любой момент продвигайте то, что имеет меньшее значение. Если они совпадают в любой точке, продвигайте их оба и так далее, пока не дойдете до конца любого итератора.Хорошая вещь в этом заключается в том, что вам нужно только повторить каждый набор один раз, и вы можете сделать это в O(1) память.
Вот пример реализации - непроверенный, но он компилируется :) предполагается, что обе входящие последовательности не содержат дубликатов и отсортированы, как в соответствии с предоставленным компаратором (pass in
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 до 19 999. В первой итерации вам нужно будет посмотреть на весь набор-ваш старт нижний / верхний индексы будут равны 0 и 19 999 соответственно. Однако после того, как вы нашли совпадение с индексом 1000, следующий шаг (где вы ищете 2000) может начинаться с более низкого индекса 2000. По мере вашего продвижения диапазон, в котором вам нужно искать, постепенно сужается. Однако стоит ли это дополнительных затрат на реализацию или нет-это уже другой вопрос.
Поскольку оба списка отсортированы, вы можете получить решение, перебрав их не более одного раза (вы также можете пропустить часть одного списка, в зависимости от фактических значений, которые они содержат).
Это решение сохраняет "указатель" на ту часть списка, которую мы еще не исследовали, и сравнивает первый неисследованный номер каждого списка между ними. Если один меньше другого, указатель на список, к которому он принадлежит, увеличивается, чтобы указать на следующий номер. Если они равны, то число добавляется к результату пересечения, и оба указателя увеличиваются.
Вышеописанный подход к решению этой конкретной задачи в стиле учебника C, и учитывая простоту кода, я был бы удивлен, увидев более быстрое решение.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; } }
Вы используете довольно неэффективный метод Linq для такого рода задач, вы должны выбрать
Intersect
в качестве отправной точки.var intersection = firstSet.Intersect(secondSet);
Попробуйте это. Если вы измеряете его производительность и все еще находите его громоздким, обратитесь за дополнительной помощью (или, возможно, следуйте подходу Джона Скита).
Я использовал подход Джона, но мне нужно было выполнить это пересечение сотни тысяч раз для массовой операции на очень больших наборах и требовалось больше производительности. Случай, в котором я работал, был сильно несбалансированным размером списков (например, 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's method } }
Я все еще не уверен в этом пункте при котором вы безубыточны, нужно сделать еще несколько тестов