参照下面方法,写一个剔除交叉时间的方法,传参List<TimePeriod> sourceList,...
Erstellt am: 7. Januar 2026
Beantwortet mit GPT-5.2 Thinking von Chat01
Erstellt am: 7. Januar 2026
Beantwortet mit GPT-5.2 Thinking von Chat01
参照下面方法,写一个剔除交叉时间的方法,传参List<TimePeriod> sourceList, List<TimePeriod> removeList ,边界值精确到秒也需要剔除,用java8的stream写,不修改现有类和方法,尽量不使用辅助方法,方法要简洁
/**
* 合并重叠时间
*
* @param list 目标时间列表
* @return 合并后的时间列表
*/
public static List<TimePeriod> mergeOverlapping(List<TimePeriod> list) {
if (list == null || list.isEmpty()) {
return list;
}
list.sort(Comparator.comparing(TimePeriod::getStartTime));
textLinkedList<TimePeriod> merge = new LinkedList<>(); for (TimePeriod current : list) { if (merge.isEmpty() || merge.getLast().getEndTime().isBefore(current.getStartTime())) { merge.add(current); } else { TimePeriod last = merge.getLast(); last.setEndTime(last.getEndTime().isAfter(current.getEndTime()) ? last.getEndTime() : current.getEndTime()); } } return new ArrayList<>(merge); } @Setter @Getter @NoArgsConstructor @AllArgsConstructor
public static class TimePeriod {
/**
* 开始时间
*/
private LocalDateTime startTime;
text/** * 结束时间 */ private LocalDateTime endTime; @Override public String toString() { return "TimePeriod{" + "startTime=" + startTime + ", endTime=" + endTime + '}'; } }
javapublic static List<TimePeriod> removeOverlapping(List<TimePeriod> sourceList, List<TimePeriod> removeList) { if (sourceList == null || sourceList.isEmpty()) { return sourceList; } if (removeList == null || removeList.isEmpty()) { return sourceList; } // 拷贝一份再 merge,避免污染入参对象(mergeOverlapping 会 setEndTime + sort) List<TimePeriod> sources = mergeOverlapping(sourceList.stream() .filter(Objects::nonNull) .map(t -> new TimePeriod(t.getStartTime(), t.getEndTime())) .collect(Collectors.toList())); List<TimePeriod> removes = mergeOverlapping(removeList.stream() .filter(Objects::nonNull) .map(t -> new TimePeriod(t.getStartTime(), t.getEndTime())) .collect(Collectors.toList())); return sources.stream() .flatMap(src -> removes.stream().reduce( Collections.singletonList(src), (acc, rm) -> acc.stream().flatMap(seg -> { LocalDateTime s = seg.getStartTime(), e = seg.getEndTime(); LocalDateTime rs = rm.getStartTime(), re = rm.getEndTime(); // 无交集(边界相等也算交集:只有严格在外侧才算无交集) if (e.isBefore(rs) || s.isAfter(re)) return Stream.of(seg); Stream.Builder<TimePeriod> b = Stream.builder(); // 左段:把 rm.start 这一秒也剔除 => 结束到 rm.start - 1s if (s.isBefore(rs)) { LocalDateTime le = rs.minusSeconds(1); if (!le.isBefore(s)) b.add(new TimePeriod(s, le)); } // 右段:把 rm.end 这一秒也剔除 => 开始从 rm.end + 1s if (e.isAfter(re)) { LocalDateTime ns = re.plusSeconds(1); if (!e.isBefore(ns)) b.add(new TimePeriod(ns, e)); } return b.build(); }).collect(Collectors.toList()), (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toList()) ).stream()) .sorted(Comparator.comparing(TimePeriod::getStartTime)) .collect(Collectors.toList()); }
不强求Stream,追求效率,复杂度控制n+m,并且代码尽量简洁,注释用 /* */
javapublic static List<TimePeriod> removeOverlapping(List<TimePeriod> sourceList, List<TimePeriod> removeList) { if (sourceList == null || sourceList.isEmpty()) return Collections.emptyList(); /* 拷贝入参,避免 mergeOverlapping 的 sort/setEndTime 影响原对象 */ List<TimePeriod> src = new ArrayList<>(sourceList.size()); for (TimePeriod t : sourceList) { if (t != null && t.getStartTime() != null && t.getEndTime() != null && !t.getStartTime().isAfter(t.getEndTime())) { src.add(new TimePeriod(t.getStartTime(), t.getEndTime())); } } if (src.isEmpty()) return Collections.emptyList(); if (removeList == null || removeList.isEmpty()) return src; List<TimePeriod> rm = new ArrayList<>(removeList.size()); for (TimePeriod t : removeList) { if (t != null && t.getStartTime() != null && t.getEndTime() != null && !t.getStartTime().isAfter(t.getEndTime())) { rm.add(new TimePeriod(t.getStartTime(), t.getEndTime())); } } if (rm.isEmpty()) return src; src = mergeOverlapping(src); rm = mergeOverlapping(rm); /* 双指针线性扫描:主逻辑 O(n+m);边界精确到秒也剔除(±1s) */ List<TimePeriod> res = new ArrayList<>(); int j = 0; for (int i = 0; i < src.size(); i++) { LocalDateTime s = src.get(i).getStartTime(); LocalDateTime e = src.get(i).getEndTime(); while (j < rm.size() && rm.get(j).getEndTime().isBefore(s)) j++; LocalDateTime cur = s; int k = j; while (k < rm.size() && !rm.get(k).getStartTime().isAfter(e)) { LocalDateTime rs = rm.get(k).getStartTime(); LocalDateTime re = rm.get(k).getEndTime(); if (!re.isBefore(cur)) { if (cur.isBefore(rs)) { LocalDateTime le = rs.minusSeconds(1); if (!le.isBefore(cur)) res.add(new TimePeriod(cur, le)); } cur = re.plusSeconds(1); if (cur.isAfter(e)) break; } if (!re.isAfter(e)) k++; else break; } if (!cur.isAfter(e)) res.add(new TimePeriod(cur, e)); /* 只在 remove 段完全落在当前 source 之前时推进 j,保持整体线性 */ while (j < rm.size() && rm.get(j).getEndTime().isBefore(s)) j++; } return res; }
说明(简短):
src/rm先各自mergeOverlapping做成有序且不重叠的区间,然后用双指针线性剔除;交叉部分按“包含边界秒”剔除,所以左段到remove.start-1s,右段从remove.end+1s开始。
在下面代码基础进行修改,targetPeriods不需要合并,即便有交叉,但目前代码有问题,单元测试结果是,多了一个为何?
[TimePeriod{startTime=2026-01-03T15:00:01, endTime=2026-01-03T18:00}, TimePeriod{startTime=2026-01-03T15:00:01, endTime=2026-01-03T18:00}]
java@Test void removeOverlappingTest() { List<TimePeriod> sourceList = new ArrayList<>(); sourceList.add(new TimePeriod(LocalDateTime.parse("2026-01-01 13:00:00", DATE_TIME), LocalDateTime.parse("2026-01-02 12:00:00", DATE_TIME))); sourceList.add(new TimePeriod(LocalDateTime.parse("2026-01-02 09:00:00", DATE_TIME), LocalDateTime.parse("2026-01-02 18:00:00", DATE_TIME))); sourceList.add(new TimePeriod(LocalDateTime.parse("2026-01-02 14:00:00", DATE_TIME), LocalDateTime.parse("2026-01-03 18:00:00", DATE_TIME))); List<TimePeriod> removeList = new ArrayList<>(); removeList.add(new TimePeriod(LocalDateTime.parse("2026-01-01 07:00:00", DATE_TIME), LocalDateTime.parse("2026-01-02 16:00:00", DATE_TIME))); removeList.add(new TimePeriod(LocalDateTime.parse("2026-01-02 10:00:00", DATE_TIME), LocalDateTime.parse("2026-01-02 13:00:00", DATE_TIME))); removeList.add(new TimePeriod(LocalDateTime.parse("2026-01-02 05:00:00", DATE_TIME), LocalDateTime.parse("2026-01-03 15:00:00", DATE_TIME))); List<TimePeriod> result1List = removeOverlapping(sourceList, removeList); System.out.println(result1List); }
java/** * 从已有时段列表,剔除指定时段列表(精确到秒,结果集不含边界) * * @param targetPeriods 目标时段列表 * @param cullPeriods 需要剔除时段列表 * @return 结果列表 */ public static List<TimePeriod> removeOverlapping(List<TimePeriod> targetPeriods, List<TimePeriod> cullPeriods) { if (CollUtil.isEmpty(targetPeriods) || CollUtil.isEmpty(cullPeriods)) { return targetPeriods; } List<TimePeriod> culls = mergeOverlapping(cullPeriods); /* 双指针线性扫描:主逻辑 O(n+m);边界精确到秒也剔除(±1s) */ List<TimePeriod> result = new ArrayList<>(); int j = 0; for (TimePeriod targetPeriod : targetPeriods) { LocalDateTime start = targetPeriod.getStartTime(); LocalDateTime end = targetPeriod.getEndTime(); while (j < culls.size() && culls.get(j).getEndTime().isBefore(start)) { j++; } LocalDateTime current = start; int k = j; while (k < culls.size() && !culls.get(k).getStartTime().isAfter(end)) { LocalDateTime rs = culls.get(k).getStartTime(); LocalDateTime re = culls.get(k).getEndTime(); if (!re.isBefore(current)) { if (current.isBefore(rs)) { LocalDateTime le = rs.minusSeconds(1); if (!le.isBefore(current)) result.add(new TimePeriod(current, le)); } current = re.plusSeconds(1); if (current.isAfter(end)) break; } if (!re.isAfter(end)) { k++; } else { break; } } if (!current.isAfter(end)) { result.add(new TimePeriod(current, end)); } /* 只在 remove 段完全落在当前 source 之前时推进 j,保持整体线性 */ while (j < culls.size() && culls.get(j).getEndTime().isBefore(start)) { j++; } } return result; }
pythonfrom datetime import datetime, timedelta def dt(s): return datetime.strptime(s, "%Y-%m-%d %H:%M:%S") source=[ (dt("2026-01-01 13:00:00"), dt("2026-01-02 12:00:00")), (dt("2026-01-02 09:00:00"), dt("2026-01-02 18:00:00")), (dt("2026-01-02 14:00:00"), dt("2026-01-03 18:00:00")), ] remove=[ (dt("2026-01-01 07:00:00"), dt("2026-01-02 16:00:00")), (dt("2026-01-02 10:00:00"), dt("2026-01-02 13:00:00")), (dt("2026-01-02 05:00:00"), dt("2026-01-03 15:00:00")), ] # merge remove remove_sorted=sorted(remove) merged=[] for s,e in remove_sorted: if not merged or merged[-1][1] < s: # strict before merged.append([s,e]) else: merged[-1][1]=max(merged[-1][1], e) merged=[tuple(x) for x in merged] merged [(datetime.datetime(2026, 1, 1, 7, 0), datetime.datetime(2026, 1, 3, 15, 0))]
pythondef remove_overlapping(target, culls): res=[] j=0 for start,end in target: while j < len(culls) and culls[j][1] < start: j+=1 cur=start k=j while k < len(culls) and culls[k][0] <= end: rs,re = culls[k] if re >= cur: if cur < rs: le = rs - timedelta(seconds=1) if le >= cur: res.append((cur,le)) cur = re + timedelta(seconds=1) if cur > end: break if re <= end: k+=1 else: break if cur <= end: res.append((cur,end)) while j < len(culls) and culls[j][1] < start: j+=1 return res remove_overlapping(source, merged) [(datetime.datetime(2026, 1, 3, 15, 0, 1), datetime.datetime(2026, 1, 3, 18, 0))]
你现在这段实现是“逐条 target 计算差集然后 append”,当 targetPeriods 自己有交叉/重复覆盖时,同一段剩余时间会被产出多次,所以你看到结果里出现两条一模一样的:
[15:00:01~18:00, 15:00:01~18:00]
这通常意味着:至少有两条 targetPeriod 在剔除后都还能覆盖到 2026-01-03 15:00:01 ~ 2026-01-03 18:00(比如 targetPeriods 里有重复段、或者有两段都跨到 1/3 18:00)。
你贴出来的这组 targetPeriods(第二段止于 1/2 18:00)按逻辑一般只会产出 1 条;若你跑出来是 2 条,基本就是 targetPeriods 实际数据里“覆盖 1/3 18:00 的段不止一条”(常见是复制/拼接时重复加入了同一个区间)。
下面是在你代码基础上改:不合并 targetPeriods,但用一个 coveredEnd 把“已经处理过的 target 并集部分”跳过去,避免重复产出;同时把 j 推进到 k,保证扫描是线性的(并删掉最后那段重复 while)。
javapublic static List<TimePeriod> removeOverlapping(List<TimePeriod> targetPeriods, List<TimePeriod> cullPeriods) { if (CollUtil.isEmpty(targetPeriods) || CollUtil.isEmpty(cullPeriods)) { return targetPeriods; } /* 拷贝一份,避免 mergeOverlapping sort/setEndTime 污染入参 */ List<TimePeriod> cullCopy = new ArrayList<>(cullPeriods.size()); for (TimePeriod t : cullPeriods) { if (t != null && t.getStartTime() != null && t.getEndTime() != null && !t.getStartTime().isAfter(t.getEndTime())) { cullCopy.add(new TimePeriod(t.getStartTime(), t.getEndTime())); } } if (cullCopy.isEmpty()) return targetPeriods; List<TimePeriod> culls = mergeOverlapping(cullCopy); List<TimePeriod> result = new ArrayList<>(); int j = 0; LocalDateTime coveredEnd = null; /* 已处理过的 target 并集结束点(含秒),用于去重 */ for (TimePeriod targetPeriod : targetPeriods) { if (targetPeriod == null || targetPeriod.getStartTime() == null || targetPeriod.getEndTime() == null) continue; LocalDateTime start = targetPeriod.getStartTime(); LocalDateTime end = targetPeriod.getEndTime(); if (start.isAfter(end)) continue; /* targetPeriods 不合并,但要跳过与已处理并集重叠的部分,避免重复产出 */ if (coveredEnd != null && !start.isAfter(coveredEnd)) { start = coveredEnd.plusSeconds(1); } if (start.isAfter(end)) { if (coveredEnd == null || coveredEnd.isBefore(end)) coveredEnd = end; continue; } while (j < culls.size() && culls.get(j).getEndTime().isBefore(start)) j++; LocalDateTime current = start; int k = j; while (k < culls.size() && !culls.get(k).getStartTime().isAfter(end)) { LocalDateTime rs = culls.get(k).getStartTime(); LocalDateTime re = culls.get(k).getEndTime(); if (!re.isBefore(current)) { if (current.isBefore(rs)) { LocalDateTime le = rs.minusSeconds(1); /* 边界秒也剔除 */ if (!le.isBefore(current)) result.add(new TimePeriod(current, le)); } current = re.plusSeconds(1); /* 边界秒也剔除 */ if (current.isAfter(end)) break; } if (!re.isAfter(end)) k++; else break; } if (!current.isAfter(end)) result.add(new TimePeriod(current, end)); j = k; /* 线性推进,避免重复扫描 */ if (coveredEnd == null || coveredEnd.isBefore(end)) coveredEnd = end; } return result; }
要点就两个:
coveredEnd 跳过已处理并集的重叠部分(仍然是 O(n+m) 的扫描逻辑)。如果你愿意,我也可以把你这组测试的“真实跑出两条”的原因定位到具体是哪一条 targetPeriod 产生了第二条(你把实际 targetPeriods 打印出来我就能指出来)。