Как эффективно объединить INT диапазоны в потоке? - PullRequest
10 голосов
/ 20 января 2011

Нам дан непрерывный поток целочисленных диапазонов, таких как [1,3], [5,10], [2,6], ... когда появляется каждый новый диапазон, нам нужно проверить существующие диапазоны и посмотреть, перекрывается ли он с любым существующим диапазоном, и если какое-либо перекрытие найдено, все перекрывающиеся диапазоны удаляются, и вместо этого вставляется объединенный диапазон. Нам нужен эффективный алгоритм для этого. Обратите внимание, что диапазон приходит по одному, который формирует поток ...

Был задан этот вопрос во время интервью. Идеи?

Цель состоит в том, чтобы объединить перекрывающиеся диапазоны в один. например, если у нас есть 3 диапазона в следующем порядке: [1,3], [2,6], [5,10]. Затем мы сначала объединяем первые два в [1,6], затем сливаемся с третьим, и оно становится [1,10].

Ответы [ 5 ]

9 голосов
/ 21 января 2011

Стандартный алгоритм для этой задачи - дерево интервалов .

0 голосов
/ 27 июля 2018

Поскольку интервалы являются потоковыми, они должны быть сохранены в некоторой коллекции, которая эффективна для

  • вставка новых интервалов
  • Нахождение интервалов перекрытия
  • объединение перекрывающихся интервалов при добавлении новых интервалов

Операция объединения должна учитывать следующие случаи

  • без перекрытия вставка (7, 9) в [(1, 5), (10, 15)] дает [(1,5), (7, 9), (10,15)]

  • некоторое перекрытие вставка (4, 6) в [(1, 5), (10, 15)] дает [(1,6), (10,15)]

  • перекрытие моста вставка (4, 11) в [(1, 5), (10, 15), (20, 30)] дает [(1, 15), (20, 30)]

  • максимальное перекрытие вставка (1, 20) в [(2, 5), (7, 9), (13, 15)] дает [(1, 20)]

  • полное перекрытие вставка (5, 6) в [(1, 7), (10, 19)] дает [(1, 7), (10, 19)]

  • и множество промежуточных случаев и варианты того, что указано выше

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

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

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

Стоит исследовать интервальные деревья , определенные в википедии, однако эта структура данных не объединяет интервалы, она может только запрашивать их, поэтому это также не поддерживает наш вариант использования.

Лучший способ - использовать двоичное дерево для хранения интервалов, где интервал имеет определенный метод сравнения , который возвращает, что элементы равны, если есть какое-либо перекрытие. Это позволяет очень эффективно находить перекрывающиеся интервалы за O(lgn) времени.

Пример класса сравнения выглядит следующим образом

public class Interval<T extends Comparable<T>> implements Comparable<Interval<T>>
{
    private final T start_;
    private final T end_;

    public Interval(final T start, final T end)
    {
        this.start_ = start;
        this.end_ = end;
    }

    @Override 
    public int compareTo(Interval other) {
        if (other.start_.compareTo(this.end_) == -1) { 
            return -1;
        } else if (other.start_.compareTo(this.end_) == 1) { 
            return 1; 
        } else { 
            return 0;
        }
    }
}

Набор деревьев (или набор, или любой из его вариантов) может быть расширен или составлен так, чтобы соответствовать операции вставки. Обратите внимание: я использую вариант treemap, поскольку java не позволяет возвращать элемент в наборе.

public class IntervalCollection
{
    private Map<Interval, Interval> intervalCollection_ = new TreeMap<>();

    public void insert(Interval interval)
    {
        while (intervalCollection_.containsKey(interval)) {

            Interval other = intervalCollection_.get(interval);
            intervalCollection_.remove(interval);

            interval = new Interval(Math.min(interval.start_, other.start_),
                Math.max(interval.end_, other.end_));
        }

        intervalCollection_.put(interval, interval);
    }
}

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

0 голосов
/ 02 августа 2012

Сервер интервальных деревьев хорошо подходит для этой цели, но вот еще один подход, который я использовал для добавления к диапазону.

Предполагается, что список, к которому добавляется новый кортеж (rane),либо пустой, либо не имеет перекрывающихся диапазонов.Во-вторых, вход имеет вид a, b, где a <= b. Вы можете преобразовать список кортежей в один список чисел, а затем добавить к нему новый кортеж. </p>

Пусть rangeList будет текущим спискомдиапазоны.например.[1, 4, 6, 10, 12, 14] будет означать список диапазонов [(1, 4), (6, 10), (12, 14)]

  1. Если списокпусто, просто вставьте элементы кортежа в список, и все готово
  2. Найдите позицию элемента a, b в списке с помощью бинарного поиска.(Если элемент не существует, вернуть позицию наименьшего элемента больше, чем искомое число) *
  3. Пусть возвращаемыми позициями будут pos_a, pos_b для элементов кортежа a, b соответственно.
  4. Если pos_a четное, list_A = [a]
  5. Если pos_b четное, list_B = [b]
  6. Новый список = rangeList [0: pos_a] + list_A + list_B + rangeList [b: end] и все готово.

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

def subroutine(rangeList, currTuple)
"""
rangeList: Existing list of non overlapping tuples
currTuple: The range tuple to be added
"""
    if not rangeList:
        rangeList.extend(currTuple)
    else:
        a, b = binSearch(currTuple, rangeList)
        list_changed = rangeList[0:a]
        if a%2 == 0:
            list_changed.append(currTuple[0])
        if b%2 == 0:
            list_changed.append(currTuple[1])
        list_changed.extend(rangeList[b:])
    return list_changed
0 голосов
/ 20 января 2011

Вы можете представить диапазон как одну последовательность чередующихся точек «включения» и «выключения». Всякий раз, когда появляется новый диапазон, производится поиск его начальной и конечной точек и предпринимаются соответствующие действия по слиянию или вставке.

Чтобы получить хорошую производительность для требуемых операций & mdash; искать, вставлять и удалять & ndash; возможно, здесь нужно использовать что-то вроде B-дерева.

0 голосов
/ 20 января 2011

Когда новый кортеж входит в (newstart,newend), выполните бинарный поиск по номеру newstart-1 в списке существующих закрывающих элементов и аналогично по newend+1 в списке существующих открывающих элементов.

Объедините слюбые совпадающие диапазоны.

Если ни один диапазон не совпадает, вставьте между двумя ближайшими диапазонами.

Обновление: Поцарапайте, я решил не ту проблему: объединение диапазонов касания.Но решение с перекрывающимся диапазоном не будет слишком отличаться.

  1. Бинарный поиск для наибольшего существующего начального элемента, где start(n) <= newstart
  2. Бинарный поиск для наименьшего существующего конечного элемента, где end(n) >= newstart
  3. Если оба возвращают один и тот же индекс, ваш старт кортежа можно объединить с n-й записью, замените newstart на start(n)
  4. Двоичный поиск для наибольшего существующего начального элемента, где start(m) <= newend
  5. Двоичный поиск наименьшего существующего конечного элемента, где end(m) >= newend
  6. Если оба возвращают один и тот же индекс, ваш конец кортежа можно объединить с m-й записью, замените newend на end(m)
  7. Заменить все записи между n-м и m-м индексами на кортеж (newstart,newend).
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...