一、问题
最近项目要求对一批时间范围进行管理,要求不能交叉。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,这样可以遍历其中的Range。
asSet(DiscreteDomain<C>)(仅ImmutableRangeSet支持):用ImmutableSortedSet<C>表现RangeSet,以区间中所有元素的形式而不是区间本身的形式查看。(这个操作不支持DiscreteDomain 和RangeSet都没有上边界,或都没有下边界的情况)
contains(C):RangeSet最基本的操作,判断RangeSet中是否有任何区间包含给定元素。
rangeContaining(C):返回包含给定元素的区间;若没有这样的区间,则返回null。
encloses(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");
}