Bootstrap

【数据结构和算法】--RangeSet时间范围管理示例

一、问题

最近项目要求对一批时间范围进行管理,要求不能交叉。RangeSet是专门用于高效处理范围集合。

二、RangeSet实现原理

RangeSet表示一组不重叠的、非空的范围集合。RangeSet中的每个范围都是一个Range对象,Range对象表示一个具有起始和结束边界的范围。实现RangeSet接口,主要有两个实现类:ImmutableRangeSet和TreeRangeSet。ImmutableRangeSet是一个不可修改的RangeSet,而TreeRangeSet则是利用树的形式来实现,提供了高效的查询和插入操作。

2.1、RangeSet常用方法

complement():返回RangeSet的补集视图。complement也是RangeSet类型,包含了不相连的、非空的区间。
subRangeSet(Range<C>):返回RangeSet与给定Range的交集视图。这扩展了传统排序集合中的headSet、subSet和tailSet操作。
asRanges():用Set<Range<C>>表现RangeSet,这样可以遍历其中的RangeasSet(DiscreteDomain<C>)(仅ImmutableRangeSet支持):用ImmutableSortedSet<C>表现RangeSet,以区间中所有元素的形式而不是区间本身的形式查看。(这个操作不支持DiscreteDomainRangeSet都没有上边界,或都没有下边界的情况)
contains(C)RangeSet最基本的操作,判断RangeSet中是否有任何区间包含给定元素。
rangeContaining(C):返回包含给定元素的区间;若没有这样的区间,则返回nullencloses(Range<C>):简单明了,判断RangeSet中是否有任何区间包括给定区间。
span():返回包括RangeSet中所有区间的最小区间。

2.2、核心原理

RangeSet的实现原理主要基于一种称为“范围树”的数据结构。范围树是一种平衡树,其中每个节点都表示一个范围。树中的节点按照范围的起始位置进行排序,以便快速查找和定位特定的范围。
当向RangeSet中添加一个新的范围时,它会遍历范围树,找到与新范围相交或相邻的现有范围,并进行合并。合并后的范围会被插入到树中的适当位置,以保持树的平衡性。
对于查询操作,RangeSet可以利用范围树的结构进行快速查找。【例如,当查询一个元素是否包含在RangeSet中时,可以从树的根节点开始,沿着适当的分支向下遍历,直到找到一个包含该元素的范围或确定该元素不在RangeSet中。】

2.3、核心特性

自动合并范围
当向RangeSet中添加一个新的范围时,它会自动与现有的范围进行合并。如果新的范围与某个现有范围相交或相邻,它们会被合并成一个更大的范围。这种自动合并的特性使得RangeSet能够保持范围的不重叠性,从而简化了范围集合的管理。
高效的查询操作
RangeSet提供了丰富的查询操作,可以快速地判断一个元素是否在某个范围内、获取包含某个元素的范围等。这些查询操作都是基于对范围树的高效遍历实现的,能够在对数时间内给出结果。
灵活的范围操作
除了基本的查询操作外,RangeSet还支持各种范围操作,如并集、交集、差集等。这些操作可以方便地对范围集合进行组合和变换,满足各种复杂的需求。

2.4、基本使用

   public void way1(){
        RangeSet<Integer> rangeSet = TreeRangeSet.create();
        // 向RangeSet中添加几个不连续的范围 【是一种合并的方式】
        rangeSet.add(Range.closed(1, 4));     // [1, 4]
        rangeSet.add(Range.open(5, 8));       // [1, 4] (5, 8)
        rangeSet.add(Range.closedOpen(10, 12));// [1, 4] (5, 8) [10, 12)
        rangeSet.add(Range.greaterThan(15));   // [1, 4] (5, 8) [10, 12) (15, +∞)
        rangeSet.add(Range.downTo(14,BoundType.OPEN)); //[1, 4] (5, 8) [10, 12) (14, +∞)
        rangeSet.add(Range.closedOpen(6, 11)); // [1, 4] (5, 12) (14, +∞)
        //返回某个元素在Set集合中,归属哪个范围
        Range<Integer> containing1 = rangeSet.rangeContaining(5); //null,5是边界的,半开区间
        Range<Integer> containing2 = rangeSet.rangeContaining(13); //null, 13不在任何区间
        Range<Integer> containing3 = rangeSet.rangeContaining(15); //(14, +∞)
        Range<Integer> containing4 = rangeSet.rangeContaining(4); // [1, 4]

        // 获取RangeSet的最小和最大元素(注意这不是一个Range,而是两个元素)
        Integer minValue = rangeSet.asRanges().stream().map(Range::lowerEndpoint).min(Integer::compareTo).orElse(null);
          //如果无穷大,这个会抛异常 java.lang.IllegalStateException: range unbounded on this side
        Integer maxValue = rangeSet.asRanges().stream().map(Range::upperEndpoint).max(Integer::compareTo).orElse(null);
        System.out.println("Min value: " + minValue); // Min value: 1
        System.out.println("Max value: " + maxValue); // Max value: 2147483647 (Integer.MAX_VALUE,因为rangeSet包含(15..+∞))

        //删除一个范围
        rangeSet.remove(Range.open(8, 15)); //一个有部分交叉的范围删除  [1, 4] (5, 8] [15, +∞)
        rangeSet.remove(Range.open(9, 11)); //一个无任何交叉的范围删除  [1, 4] (5, 8] [15, +∞)
        rangeSet.remove(Range.closedOpen(6, 7)); //一个包含的范围删除  [[1‥4], (5‥6), [7‥8], [15‥+∞)]
        //获取重叠的部分
        RangeSet<Integer> complement = rangeSet.subRangeSet(Range.open(2, 17)); //[(2‥4], (5‥6), [7‥8], [15‥17)]
        RangeSet<Integer> complement1 = rangeSet.subRangeSet(Range.open(6, 7));
        //备注:[[1‥4], (5‥6), [7‥8], [15‥+∞)] 和[6,7]重叠为[],发现边界7不算重叠,用subRangeSet判断有交集不好!!
        RangeSet<Integer> complement2 = rangeSet.complement().subRangeSet(Range.open(2, 17)); //[(4‥5], [6‥7), (8‥15)]

        //判断区间是否包含某个值   [1, 4] (5, 12) (14, +∞)
        Set<Range<Long>> ranges= timeRangeValue.asRanges();
        Iterator<Range<Long>> iterator = ranges.iterator();
        while (iterator.hasNext()) {
            Range<Long> tempRange = iterator.next();
            if(tempRange.contains(12L)){
                System.out.println("timeRangeValue has range contains:{12}");
            }
            boolean emp = tempRange.isEmpty(); //是否是空区间
            Long lowerEnd = tempRange.lowerEndpoint(); //返回区间的端点值;如果区间没有对应的边界,抛出 IllegalStateException;
            Long upperEnd = tempRange.upperEndpoint();
            boolean hasLower = tempRange.hasLowerBound();  //返回区间边界类型,CLOSED 或 OPEN;如果区间没有对应的边界,抛出 IllegalStateException
            boolean hasUpper = tempRange.hasUpperBound();
        }
        // [1, 4] 包含[2,3]
        Range.closed(1,4).encloses(Range.closed(2,3)); true
        // (3..6) 包含(3..6)
        Range.open(3,6).encloses(Range.open(3,6));//true
        // [3..6] 包含[4..4),虽然后者是空区间;
        Range.closed(3,6).encloses(Range.closedOpen(4,4));//true
        // (3..6]不 包含[3..6]
        Range.openClosed(3,6).encloses(Range.closed(3,6));//false
        // [4..5]不 包含(3..6),虽然前者包含了后者的所有值,离散域[discrete domains]可以解决这个问题
        Range.closed(4,5).encloses(Range.open(3,6));//false

        //判断区间是否相连 【两个区间的并集是连续集合的形式(空区间的特殊情况除外)】
        System.out.println(Range.closed(3,5).isConnected(Range.open(5,10)));//true
        System.out.println(Range.closed(0,9).isConnected(Range.closed(3,4)));//true
        System.out.println(Range.closed(0,5).isConnected(Range.closed(3,9)));//true
        System.out.println(Range.open(3,5).isConnected(Range.open(5,10)));//false
        System.out.println(Range.closed(1,5).isConnected(Range.closed(6,10)));//false

        //求区间的交集 【当且仅当两个区间是相连的,它们才有交集。如果两个区间没有交集,该方法将抛出 IllegalArgumentException】
        //Range.intersection(Range)返回两个区间的交集:既包含于第一个区间,又包含于另一个区间的最大区间。
        System.out.println(Range.closed(3,5).intersection(Range.closed(4,6)));//[4..5]

        //区间的并集【同时包括两个区间的最小区间】
        System.out.println(Range.closed(3,5).span(Range.closed(7,9)));//[3..9]
        System.out.println(Range.closed(3,5).span(Range.closed(4,8)));//[3..8]

    }

三、具体应用

yyyy-MM-dd类型的时间,要求构建范围,新加入的时间范围不可重叠。


    /**
     * 添加数据,有交集范围的数据,不能加入
     * @param start
     * @param end
     */
    public Boolean addTimeToRange(String start,String end){
        start = start.replaceAll("-","");
        end = end.replaceAll("-","");
        Long lStart = Long.valueOf(start);
        Long lEnd = Long.valueOf(end);
        if(!hasIntersection(lStart,lEnd)){ //无交集
            timeRangeValue.add(Range.closed(lStart,lEnd));
            return true;
        }
        return false;
    }

    /**
     * 是否有交集
     * @param lStart
     * @param lEnd
     * @return
     */
    public Boolean hasIntersection(Long lStart,Long lEnd){
        if(timeRangeValue.isEmpty()){
            return false;
        }
        Set<Range<Long>> ranges= timeRangeValue.asRanges();
        Iterator<Range<Long>> iterator = ranges.iterator();
        while (iterator.hasNext()){
            Range<Long> tempRange = iterator.next();
            Range<Long> inter;
            try{
                inter = tempRange.intersection(Range.closed(lStart,lEnd));
            }catch (Exception e){
                inter = null;
            }
            if(null != inter){ //有交集,就退出
                return true;
            }
        }
        return false;
    }

示例验证

public static void main(String[] args) {
        RangeCreateService rangeCreateService = new RangeCreateService();

         rangeCreateService.addTimeToRange("2024-03-01","2024-05-31");
        rangeCreateService.addTimeToRange("2024-06-01","2024-06-08");
        rangeCreateService.addTimeToRange("2024-05-31","2024-06-01");
        rangeCreateService.addTimeToRange("2024-06-21","2024-07-08");
        rangeCreateService.addTimeToRange("2024-06-11","2024-06-21");
        rangeCreateService.addTimeToRange("2024-05-23","2024-05-25");
        rangeCreateService.addTimeToRange("2024-01-24","2024-06-02");
    }
;