【问题标题】:Efficiently find an integer in a sorted collection of integer ranges有效地在整数范围的排序集合中找到一个整数
【发布时间】:2020-11-22 08:01:48
【问题描述】:

问题

我有大量 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 搜索是不够有效的。

我也可以手动创建binary search tree,但我希望使用 Java 集合 API 使用比较器和 TreeSet 之类的东西,这样的事情应该是可能的?

通常,当使用 TreeSet 时,我会搜索相同类型的元素,例如,搜索 Person 对象,其中 firstname 和 lastname 必须匹配才能相等。但是在这种情况下,我想在 IntRanges 的 TreeSet 中搜索一个整数,所以不适合使用 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;
    }
}

【问题讨论】:

  • 您可以将java.util.Arrays.binarySearch 与虚拟IpRange 对象和自定义比较器一起使用
  • 范围可以重叠还是保证不相交?
  • @joni 是的,它们可能会重叠,尽管这可能不会经常发生。
  • @user binarySearch 似乎只适用于匹配等于的对象。因此,如果我要创建一个像 new IntRange(10, 10) 这样的假人,希望它与 IntRange(8, 12) 匹配,那么它就行不通了。
  • @user 我不确定为什么我尝试按照您的建议尝试java.util.Arrays.binarySearch(),但没有成功。当@axelclk 在他的解决方案中尝试它时,它确实有效。所以也许我在尝试时做错了什么。但它现在可以工作了:)

标签: java collections range binary-search-tree binary-search


【解决方案1】:

试试binarySearch(List<? extends T> list, T key, Comparator<? super T> c))

通过选择合适的Comparator&lt;IntRange&gt; 类,您可以获得负插入点(即(-(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 + "]";
    }
}

【讨论】:

  • axelclk 修改了他的答案。现在它完全有效。所以他基本上使用 binarySearch() (正如你也建议的那样),然后寻找附近的匹配项直到用尽。
  • 当元素不在列表中时,binarySearch会返回(-(insertion point) - 1),而不仅仅是插入点。此外,compareTo 的实现违反了对称性要求。调用a.compareTo(b) 时此方法可能返回零,但b.compareTo(a) 时返回非零。
  • 但是比较器具有与自然顺序相同的对称性要求。对于非重叠范围,您可以简单地按起点或终点排序。对于重叠范围,没有可靠的基于binarySearch 的解决方案可能。
【解决方案2】:

如果范围不相交(一个范围从不与其他范围重叠或包含其他范围),使用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());
}

对于一般情况,范围可能重叠并且您正在查找所有范围,一种解决方案是使用两个 TreeMap:一个将范围开始与范围结束相关联,另一个将反向。

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());

但这并不是特别有效。要获得高效的解决方案,您需要实现自定义数据结构,例如:

【讨论】:

  • 在我之前版本的问题中,我没有说明任何关于重叠的内容。但就我而言,重叠是可能的,所以我刚刚在问题中添加了这个约束。但是,对于 floorEntry,它仍然是一个有趣的答案。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-05-13
  • 1970-01-01
  • 2017-02-06
  • 2015-05-04
  • 2011-09-21
  • 1970-01-01
  • 2020-03-03
相关资源
最近更新 更多