Эффективно находите целое число в отсортированном наборе целочисленных диапазонов - PullRequest
1 голос
/ 01 августа 2020

Проблема

У меня большой список диапазонов IP-адресов, и я хочу эффективно найти диапазоны, в которые входит данный IP-адрес. Возможно перекрытие диапазонов. Для простоты и обобщения этой проблемы для Stackoverflow я заменяю IP-адрес целым числом. (Но в основном это может быть любой пользовательский класс, к которому может применяться диапазон и порядок диапазонов.)

Пример проблемы

// Note: this class has a natural ordering that is inconsistent with equals.
class IntRange implements Comparable<IntRange> {
    private int start;
    private int end;

    public IntRange(int start, int end) {
        this.start = start;
        this.end = end;
    }

    public boolean inRange(int i) {
        return i >= start && i <= end;
    }

    @Override
    public int compareTo(IntRange other) {
        if (start < other.start) {
            return -1;
        } else if (start <= other.start && end >= other.end) {
            return 0;
        } else {
            return 1;
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        IntRange intRange = (IntRange) o;
        return start == intRange.start && end == intRange.end;
    }

    @Override
    public int hashCode() {
        return Objects.hash(start, end);
    }
}

class Program {
    private static List<IntRange> findRanges(IntRange[] ranges, int i) {
        // How to implement this?
    }

    public static void main(String[] args) {
        IntRange[] ranges = {
                new IntRange(-10, 5),
                new IntRange(8, 11),
                new IntRange(9, 13),
                new IntRange(20, 30),
                new IntRange(800, 1000)
        };

        // Should contain IntRange(8, 12) and IntRange(9, 13) as result
        List<IntRange> matchingRanges = findRanges(ranges,10); 
    }
}

Учитывая приведенный выше список диапазонов, я хотел бы найти диапазоны, содержащие заданное целое число, например 10. В этом случае будет соответствовать только диапазон [8, 12], так что это будет результат.

Вопрос

Как можно Я решаю эту проблему с помощью Java Collection API, если возможно? Решение должно быть эффективным, поэтому простой перебор N по списку недостаточно эффективен. должно быть каким-то образом возможно с использованием Java Collection API с использованием компараторов и таких вещей, как TreeSet?

Обычно при использовании TreeSet я ищу тот же тип элемента, например, ищу объект Person , где имя и фамилия должны совпадать, чтобы быть равными. Но в этом случае я хочу найти целое число в TreeSet of IntRanges, поэтому метод equals не подходит.

Пример с IP-адресами вместо целых

Решения могут быть предоставлены для целых чисел вместо IP-адресов, чтобы вопрос был простым и общим. Но если вы хотите попробовать его для IP-адресов, можно ли использовать этот код для представления диапазонов IP-адресов:

class IpRange {
    private byte[] start; // 4 bytes for IPv4, 16 bytes for IPv6
    private byte[] end;

    // Only for testing purposes
    public IpRange(int start, int end) {
        this.start = BigInteger.valueOf(start).toByteArray();
        this.end = BigInteger.valueOf(end).toByteArray();
    }

    public IpRange(byte[] start, byte[] end) {
        this.start = start;
        this.end = end;
    }

    public boolean inRange(byte[] ip) {
        return Arrays.compare(start, ip) <= 0 && Arrays.compare(end, ip) >= 0;
    }

    public static void main(String[] args) {
        // Test 1: test inRange function
        IpRange ir = new IpRange(40, 60);
        System.out.println(ir.inRange(BigInteger.valueOf(39).toByteArray())); // false
        System.out.println(ir.inRange(BigInteger.valueOf(50).toByteArray())); // true
        System.out.println(ir.inRange(BigInteger.valueOf(61).toByteArray())); // false

        // Test 2
        // In production, this range contains thousands of entries
        IpRange[] ranges = {
                new IpRange(-10, 5),
                new IpRange(8, 12),
                new IpRange(20, 30),
                new IpRange(800, 1000)
        };

        // How to efficiently check in which ranges ip is 'inRange'?
        int ip = 25;
    }
}

Ответы [ 2 ]

3 голосов
/ 01 августа 2020

Если диапазоны не пересекаются (один диапазон никогда не перекрывается с другими диапазонами и не содержит их), это легко решить с помощью TreeMap.

Создайте TreeMap, который связывает начало диапазона с концом диапазон:

var map = new TreeMap<Integer,Integer>()
map.put(-10, 5)
map.put(8, 12)
map.put(20, 30)
map.put(800, 1000)

Затем вы можете использовать метод floorEntry, чтобы определить, находится ли число потенциально в пределах диапазона. Например, floorEntry (25) вернет запись карты с ключом 20 и значением 30, соответствующим диапазону 20-30. Затем вы просто проверяете, меньше ли ваше число, чем конец найденного вами диапазона.

boolean isContainedInRange(int value) {
    Map.Entry<Integer, Integer> entry = map.floorEntry(value);
    return entry != null && value < entry.getValue());
}

Для общего случая , где диапазоны могут перекрываться, и вы ищете все диапазоны, одно решение состоит в том, чтобы иметь два TreeMaps: одно связывает начало диапазона с концом диапазона, а другое делает обратное .

var reverseMap = new TreeMap<Integer,Integer>();
reverseMap.put(5, -10);
reverseMap.put(12, 8);
reverseMap.put(13, 9);
reverseMap.put(30, 20);

Теперь, учитывая значение, с помощью этих двух карт вы можете найти набор диапазонов, которые начинаются перед значением, используя map.headMap(). Вы также можете найти набор диапазонов, которые заканчиваются после заданного значения, используя reverseMap.tailMap(). Установленное пересечение этих двух дает вам все диапазоны, которые содержат данное значение. Пересечение рассчитывается методом Set.retainAll.

TreeMap<Integer, Integer> ranges = new TreeMap<>(map.headMap(value, true));
ranges.keySet().retainAll(reverseMap.tailMap(value).values());

Это не особенно эффективно. Для эффективного решения вам потребуется реализовать настраиваемую структуру данных, например:

2 голосов
/ 01 августа 2020

Попробуйте binarySearch (Список список, клавиша T, компаратор c))

Выбрав подходящий класс Comparator<IntRange>, вы можете получить отрицательную точку вставки (т.е. (-(insertion point) - 1)) или правильный индекс файла key. Точка вставки определяется как точка, в которой key будет вставлен в список. Дополнительный тест inRange() может проверить, доступен ли ключ в позиции индекса.

package examples;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

//Note: this class has a natural ordering that is inconsistent with equals.
class IntRange {

    private static class IntComparator implements Comparator<IntRange> {

        @Override
        public int compare(IntRange o1, IntRange o2) {
            if (o1.start <= o2.start && o1.end >= o2.end) {
                return 0;
            }
            if (o1.start < o2.start) {
                return -1;
            } else if (o1.start > o2.start) {
                return 1;
            } else if (o1.end > o2.end) {
                return 1;
            }
            return -1;
        }
    }

    private static List<IntRange> findRanges(List<IntRange> ranges, int i) {
        IntRange test = new IntRange(i, i);
        int index = Collections.binarySearch(ranges, test, new IntComparator());
        if (index < 0) {
            index = -(index + 1);
        }
        ArrayList<IntRange> result = new ArrayList<IntRange>();
        for (int j = index - 1; j >= 0; j--) {
            IntRange r = ranges.get(j);
            if (r.inRange(i)) {
                result.add(0, r);
            } else {
                break;
            }
        }
        for (int j = index; j < ranges.size(); j++) {
            IntRange r = ranges.get(j);
            if (r.inRange(i)) {
                result.add(r);
            } else {
                break;
            }
        }
        return result;
    }

    public static void main(String[] args) {
        ArrayList<IntRange> ranges = new ArrayList<IntRange>();
        ranges.add(new IntRange(-10, 5));
        ranges.add(new IntRange(8, 12));
        ranges.add(new IntRange(17, 20));
        ranges.add(new IntRange(20, 30));
        ranges.add(new IntRange(800, 1000));

        // Should contain IntRange(8, 12) as result
        List<IntRange> matchingRanges = findRanges(ranges, 10);
        for (int i = 0; i < matchingRanges.size(); i++) {
            System.out.println(matchingRanges.get(i).toString());
        }

        // Should contain IntRange(17, 20) and IntRange(20, 30) as result
        matchingRanges = findRanges(ranges, 20);
        for (int i = 0; i < matchingRanges.size(); i++) {
            System.out.println(matchingRanges.get(i).toString());
        }

    }

    private int start;

    private int end;

    public IntRange(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        IntRange intRange = (IntRange) o;
        return start == intRange.start && end == intRange.end;
    }

    @Override
    public int hashCode() {
        return Objects.hash(start, end);
    }

    public boolean inRange(int i) {
        return i >= start && i <= end;
    }

    @Override
    public String toString() {
        return "IntRange [start=" + start + ", end=" + end + "]";
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...