Day11|● 239. 滑动窗口最大值 ● 347.前 K 个高频元素

文章介绍了如何使用Go语言解决两个算法问题:1)滑动窗口最大值,通过单调队列优化避免了时间超时;2)找出数组中前K个高频元素,利用小顶堆实现。这两种方法都涉及到了数据结构和算法的应用。

239. 滑动窗口最大值

暴力法能过了一半多一点,但是还是会时间超时。
这道题官方题解给了三种办法,第一种大顶堆go实现起来太麻烦,不像c++一条priority_queue<pair<int, int>> q;就可以实现,所以采用了第二种单调队列的方式来做,只需要保证队列中的元素都是单调的即可,然后再每次插入的时候把不符合的元素筛选出去。
第三章方法分块暂时没看,后续有空再看

func maxSlidingWindow(nums []int, k int) []int {
    q := make([]int,0)
    ret := make([]int,0)
    push := func(i int){
      for len(q) > 0 && nums[i] >= nums[q[len(q) - 1]]{
        q = q[:len(q) - 1]
      }
      q = append(q, i)
    }

    for i := 0; i < k; i++{
      push(i)
    }

    ret = append(ret, nums[q[0]])
    for i := k; i < len(nums); i++{
      push(i)
      for q[0] <= i - k{
        q = q[1:]
      }
      ret = append(ret,nums[q[0]])
    }
    
    return ret
}

347.前 K 个高频元素

前 K 个高频元素
go中实现小顶堆的方式,首先需要初始化一个堆的数据结构,然后实现heap.Interface接口的方法
然后调用heap.Init初始化一个切片为堆,并且输入是指针。

func topKFrequent(nums []int, k int) []int {
    occurrences := map[int]int{}
    for _, num := range nums {
        occurrences[num]++
    }
    h := &IHeap{}
    heap.Init(h)

    for key, value := range occurrences{
        heap.Push(h,[2]int{key, value})
        if h.Len() > k{
            heap.Pop(h)
        }
    }
    ret := make([]int, k)
    for i := 0; i < k; i++{
        ret[k - i - 1] = heap.Pop(h).([2]int)[0]
    }
    return ret
}

type IHeap [][2]int

func (h IHeap) Len() int           { return len(h) }
func (h IHeap) Less(i, j int) bool { return h[i][1] < h[j][1] }
func (h IHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IHeap) Push(x interface{}) {
    *h = append(*h, x.([2]int))
}

func (h *IHeap) Pop() interface{} {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

package jnpf.task; import jnpf.entity.EquipmentparameterEntity; import jnpf.entity.UleabnormalrecordsEntity; import jnpf.myutils.DateUtils; import jnpf.service.*; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.*; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @Slf4j @Component public class HAHFAnalysisTask { private static final Logger logger = LoggerFactory.getLogger(HAHFAnalysisTask.class); private static final int BUCKET_SIZE = 6; // 6小时分桶 private static final int MIN_DATA_POINTS = 4; // 最小数据点数 private static final int HOURS_IN_DAY = 24; // 一天的小时数 private static final int RECOVERY_WINDOW_HOURS = 24; // 恢复判断窗口小时数 @Autowired private EquipmentparameterService equipmentparameterService; @Autowired private UleabnormalrecordsService uleabnormalrecordsService; @Autowired private RealDataService realDataService; @Autowired private EquipmentledgerService equipmentledgerService; @Autowired private ProcessmanageService processmanageService; DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //@XxlJob("hahfAnalysis") public void hahfAnalysis() { logger.info("HAHF分析任务启动"); // 1. 获取TSP设备列表 List<EquipmentparameterEntity> equipmentList = equipmentparameterService.geHAHFList(); if (equipmentList.isEmpty()) { logger.info("没有可分析的TSP设备"); return; } // 2. 准备查询参数 Map<String, Object> params = new HashMap<>(); List<String> tags = equipmentList.stream() .map(EquipmentparameterEntity::getTag) .collect(Collectors.toList()); params.put("tags", tags); // 3. 获取日常值数据 params.put("begindate", DateUtils.dataTimeformat(new Date())); List<Map> dailyValue = realDataService.selectLastmonthDailyvalue(params); Map<String, Map<String, Object>> dailyValueMap = dailyValue.stream() .collect(Collectors.toMap( m -> (String) m.get("datag"), m -> m )); // 4. 获取小时数据(从一天开始时间到当时间) LocalDateTime endTime = LocalDateTime.now(); // 获取一天开始时间(00:00:00) LocalDateTime startTime = LocalDate.now().minusDays(1).atStartOfDay(); params.put("begindate", formatDateTime(startTime)); params.put("enddate", formatDateTime(endTime)); List<Map> hourDataMaps = realDataService.selectHourdataByTagIdsAndDate(params); // 5. 按tag分组小时数据 Map<String, List<Map>> hourDataByTag = hourDataMaps.stream() .collect(Collectors.groupingBy( m -> (String) m.get("datag") )); // 6. 获取当报警中的记录 List<UleabnormalrecordsEntity> activeAlarms = uleabnormalrecordsService.getOnhahfList(); Map<String, UleabnormalrecordsEntity> activeAlarmMap = activeAlarms.stream() .collect(Collectors.toMap( UleabnormalrecordsEntity::getParacode, alarm -> alarm )); // 7. 处理每个设备 for (EquipmentparameterEntity equipment : equipmentList) { try { processEquipment( equipment, dailyValueMap, hourDataByTag, activeAlarmMap, startTime, endTime ); } catch (Exception e) { logger.error("处理设备{}失败: {}", equipment.getEncode(), e.getMessage(), e); } } logger.info("HAHF分析任务完成,共处理{}个设备", equipmentList.size()); } private void processEquipment(EquipmentparameterEntity equipment, Map<String, Map<String, Object>> dailyValueMap, Map<String, List<Map>> hourDataByTag, Map<String, UleabnormalrecordsEntity> activeAlarmMap, LocalDateTime analysisStart, LocalDateTime analysisEnd) { String tag = equipment.getTag(); String paraCode = equipment.getEncode(); // 获取该tag的日常值配置 Map<String, Object> dailyConfig = dailyValueMap.get(tag); if (dailyConfig == null) { logger.warn("设备{}缺少日常值", paraCode); return; } double farBeyondValue = ((Number) dailyConfig.get("farbeyondvalue")).doubleValue(); double limitingValue = ((Number) dailyConfig.get("limitingvalue")).doubleValue(); // 获取该tag的小时数据 List<Map> hourDataList = hourDataByTag.getOrDefault(tag, new ArrayList<>()); // 无小时数据直接返回 if (hourDataList.isEmpty()) { logger.info("设备{}没有小时数据", paraCode); return; } // 准备时间序列数据 List<DataPoint> dataPoints = prepareDataPoints(hourDataList); // 按时间排序 dataPoints.sort(Comparator.comparing(DataPoint::getDateTime)); // 获取最后数据点的时间和值 DataPoint lastDataPoint = dataPoints.get(dataPoints.size() - 1); LocalDateTime lastDataTime = lastDataPoint.dateTime; double lastDataValue = lastDataPoint.value; // 添加调试信息 logger.debug("设备{}有{}个数据点,时间范围: {} 到 {},最后数据点: {}={}", paraCode, dataPoints.size(), dataPoints.get(0).dateTime, dataPoints.get(dataPoints.size()-1).dateTime, lastDataTime, lastDataValue); // 设备有报警中记录 - 无论阈值条件是否满足,都需要检查恢复 if (activeAlarmMap.containsKey(paraCode)) { handleActiveAlarm( activeAlarmMap.get(paraCode), dataPoints, farBeyondValue, limitingValue, analysisEnd, lastDataTime, lastDataValue ); } // 设备无报警中记录 - 只有满足阈值条件时才处理 else { // 检查是否满足报警条件 boolean isValidForAlarm = isValidForAlarm(dataPoints, farBeyondValue, limitingValue); if (isValidForAlarm) { handleNewAlarm( equipment, dataPoints, farBeyondValue, limitingValue, analysisEnd, lastDataTime, lastDataValue ); } else { logger.info("设备{}数据不满足报警条件", paraCode); } } } // ===================== 报警处理核心逻辑 ===================== /** * 处理新报警检测 */ private void handleNewAlarm(EquipmentparameterEntity equipment, List<DataPoint> dataPoints, double farBeyondValue, double limitingValue, LocalDateTime analysisEnd, LocalDateTime lastDataTime, double lastDataValue) { // 寻找所有符合条件的连续时间段 List<TimePeriod> validPeriods = findValidPeriods(dataPoints, farBeyondValue, limitingValue, lastDataTime, analysisEnd); if (validPeriods.isEmpty()) { logger.info("设备{}满足阈值条件但未找到有效时间段", equipment.getEncode()); return; } // 合并连续的时间段 List<TimePeriod> mergedPeriods = mergeContinuousPeriods(validPeriods); // 创建报警记录 for (TimePeriod period : mergedPeriods) { // 确定实际报警开始和结束时间 LocalDateTime actualStart = findActualStartTime(dataPoints, period.start, farBeyondValue, limitingValue); LocalDateTime actualEnd = findActualEndTime(dataPoints, period.end, farBeyondValue, limitingValue, analysisEnd); createAlarmRecord(equipment, actualStart, actualEnd); } } /** * 处理活跃报警 */ private void handleActiveAlarm(UleabnormalrecordsEntity activeAlarm, List<DataPoint> dataPoints, double farBeyondValue, double limitingValue, LocalDateTime analysisEnd, LocalDateTime lastDataTime, double lastDataValue) { LocalDateTime alarmStartTime = toLocalDateTime(activeAlarm.getAbnormaltime()); // 检查报警是否恢复:连续24小时不满足条件才算恢复 LocalDateTime recoveryTime = checkRecovery( dataPoints, alarmStartTime, farBeyondValue, limitingValue, analysisEnd ); // 情况1: 报警恢复 if (recoveryTime != null) { completeAlarm(activeAlarm, recoveryTime); // 检查恢复后是否再次触发报警 List<DataPoint> postRecoveryData = dataPoints.stream() .filter(point -> point.dateTime.isAfter(recoveryTime)) .collect(Collectors.toList()); if (!postRecoveryData.isEmpty()) { DataPoint lastPostDataPoint = postRecoveryData.get(postRecoveryData.size() - 1); if (isValidForAlarm(postRecoveryData, farBeyondValue, limitingValue)) { EquipmentparameterEntity equipment = new EquipmentparameterEntity(); equipment.setEncode(activeAlarm.getParacode()); equipment.setTag(activeAlarm.getParacode()); equipment.setEquipmentledgerid(activeAlarm.getEquipmentid()); equipment.setFullname(activeAlarm.getParaname()); handleNewAlarm( equipment, postRecoveryData, farBeyondValue, limitingValue, analysisEnd, lastPostDataPoint.dateTime, lastPostDataPoint.value ); } } } // 情况2: 报警持续中 else { // 检查最后一个数据点是否仍超过阈值,确保报警状态正确 if (lastDataValue >= limitingValue) { logger.info("报警{}仍在持续中,已持续{}小时", activeAlarm.getId(), Duration.between(alarmStartTime, LocalDateTime.now()).toHours()); } else { // 最后一个点低于阈值但未满足恢复条件,可能是短暂波动 logger.info("报警{}最后数据点低于阈值但未确认恢复,继续观察", activeAlarm.getId()); } } } // ===================== 报警检测算法 ===================== /** * 核心算法:寻找有效时间段 */ private List<TimePeriod> findValidPeriods(List<DataPoint> dataPoints, double farBeyondValue, double limitingValue, LocalDateTime lastDataTime, LocalDateTime analysisEnd) { List<TimePeriod> validPeriods = new ArrayList<>(); if (dataPoints.size() < MIN_DATA_POINTS) { return validPeriods; } // 按日期和时间排序 dataPoints.sort(Comparator.comparing(DataPoint::getDateTime)); // 使用滑动窗口检测24小时时间段 int i = 0; while (i < dataPoints.size()) { LocalDateTime windowStart = dataPoints.get(i).dateTime; LocalDateTime windowEnd = windowStart.plusHours(HOURS_IN_DAY); // 获取窗口内的数据点 List<DataPoint> windowData = new ArrayList<>(); int j = i; while (j < dataPoints.size()) { DataPoint point = dataPoints.get(j); if (point.dateTime.isBefore(windowEnd)) { windowData.add(point); j++; } else { break; } } // 检查窗口有效性 if (isValidPeriod(windowData, farBeyondValue, limitingValue, windowStart, windowEnd)) { // 确定时间段结束时间 LocalDateTime periodEnd; // 如果窗口结束时间在最后数据时间之后,且最后数据仍超过阈值,则结束时间为null(持续中) if (windowEnd.isAfter(lastDataTime) && lastDataTime.isAfter(windowStart) && dataPoints.get(dataPoints.size() - 1).value >= limitingValue) { periodEnd = null; } else { periodEnd = windowEnd; } validPeriods.add(new TimePeriod(windowStart, periodEnd)); // 移动到窗口结束位置,避免重复检测 i = j; } else { i++; } } return validPeriods; } /** * 检查24小时窗口是否有效 */ private boolean isValidPeriod(List<DataPoint> dataPoints, double farBeyondValue, double limitingValue, LocalDateTime windowStart, LocalDateTime windowEnd) { if (dataPoints.isEmpty()) { return false; } // 检查时间跨度是否足够(至少24小时) Duration duration = Duration.between(windowStart, windowEnd); if (duration.toHours() < HOURS_IN_DAY) { return false; } // 检查限制值条件 - 窗口内所有数据点都要大于等于limitingValue boolean allAboveLimiting = dataPoints.stream() .allMatch(point -> point.value >= limitingValue); if (!allAboveLimiting) { return false; } // 按6小时分桶组织数据 Map<Integer, List<DataPoint>> bucketMap = new HashMap<>(); for (DataPoint point : dataPoints) { int bucketIdx = point.dateTime.getHour() / BUCKET_SIZE; bucketMap.computeIfAbsent(bucketIdx, k -> new ArrayList<>()).add(point); } // 检查每个桶是否满足条件:每个6小时桶至少有一个点远高于日常值 for (int bucketIdx = 0; bucketIdx < 4; bucketIdx++) { List<DataPoint> bucketData = bucketMap.getOrDefault(bucketIdx, new ArrayList<>()); // 桶内无数据或没有有效点 if (bucketData.isEmpty() || bucketData.stream().noneMatch(point -> point.value >= farBeyondValue)) { return false; } } return true; } /** * 检查报警恢复:需要连续24小时不满足报警条件 */ private LocalDateTime checkRecovery(List<DataPoint> dataPoints, LocalDateTime startTime, double farBeyondValue, double limitingValue, LocalDateTime analysisEnd) { if (dataPoints.isEmpty()) { return null; } // 过滤出报警开始时间之后的数据 List<DataPoint> postAlarmData = dataPoints.stream() .filter(point -> !point.dateTime.isBefore(startTime)) .collect(Collectors.toList()); if (postAlarmData.isEmpty()) { return null; } // 检查是否有连续24小时不满足条件 for (int i = 0; i < postAlarmData.size(); i++) { LocalDateTime windowStart = postAlarmData.get(i).dateTime; LocalDateTime windowEnd = windowStart.plusHours(RECOVERY_WINDOW_HOURS); // 如果窗口超出分析结束时间,则调整窗口结束时间 if (windowEnd.isAfter(analysisEnd)) { windowEnd = analysisEnd; } // 获取窗口内的数据点 List<DataPoint> windowData = new ArrayList<>(); for (int j = i; j < postAlarmData.size(); j++) { DataPoint point = postAlarmData.get(j); if (point.dateTime.isBefore(windowEnd)) { windowData.add(point); } else { break; } } // 检查窗口是否满足恢复条件:不满足报警条件 if (!isValidPeriod(windowData, farBeyondValue, limitingValue, windowStart, windowEnd)) { // 找到第一个不满足条件的点作为恢复开始时间 for (DataPoint point : windowData) { if (point.value < limitingValue) { return point.dateTime; } } // 如果所有点都高于limitingValue但不满足其他条件,取窗口第一个点时间 return windowData.get(0).dateTime; } } return null; } /** * 检查是否满足报警条件 */ private boolean isValidForAlarm(List<DataPoint> dataPoints, double farBeyondValue, double limitingValue) { if (dataPoints.size() < MIN_DATA_POINTS) { return false; } // 检查是否有至少24小时的连续数据满足条件 for (int i = 0; i < dataPoints.size(); i++) { LocalDateTime windowStart = dataPoints.get(i).dateTime; LocalDateTime windowEnd = windowStart.plusHours(HOURS_IN_DAY); // 获取窗口内的数据点 List<DataPoint> windowData = new ArrayList<>(); for (int j = i; j < dataPoints.size(); j++) { DataPoint point = dataPoints.get(j); if (point.dateTime.isBefore(windowEnd)) { windowData.add(point); } else { break; } } if (isValidPeriod(windowData, farBeyondValue, limitingValue, windowStart, windowEnd)) { return true; } } return false; } // ===================== 报警记录管理 ===================== /** * 创建报警记录 */ private void createAlarmRecord(EquipmentparameterEntity equipment, LocalDateTime startTime, LocalDateTime endTime) { try { UleabnormalrecordsEntity record = new UleabnormalrecordsEntity(); record.setParacode(equipment.getEncode()); record.setAbnormaltime(toDate(startTime)); record.setCompletetime(toDate(endTime)); // 计算持续时间(分钟) if (endTime != null) { long durationSeconds = Duration.between(startTime, endTime).getSeconds(); record.setDuration((int) (durationSeconds / 60)); } else { // 对于持续中的报警,计算从开始到现在的持续时间 long durationSeconds = Duration.between(startTime, LocalDateTime.now()).getSeconds(); record.setDuration((int) (durationSeconds / 60)); } record.setAbnormaltype("700229026538590853"); record.setAbnormalstate(endTime == null ? "0" : "1"); // 0-报警中, 1-已结束 String processName = getProcessname(equipment.getEquipmentledgerid()); String equipName = getEquipname(equipment.getEquipmentledgerid()); if (endTime == null) { record.setAbnormaldescribe( "【" + processName + "】工序-【" + equipName + "】设备-【" + equipment.getFullname() + "】参数,于(" + df.format(record.getAbnormaltime()) + ")出现高频高幅事件,请往治理" ); } else { record.setAbnormaldescribe( "【" + processName + "】工序-【" + equipName + "】设备-【" + equipment.getFullname() + "】参数,于(" + df.format(record.getAbnormaltime()) + ")至(" + df.format(record.getCompletetime()) + ")出现高频高幅事件" ); } record.setProcess(equipmentledgerService.getInfo(equipment.getEquipmentledgerid()).getProcesscode()); record.setEquipmentid(equipment.getEquipmentledgerid()); record.setEquipmenttype(equipmentledgerService.getInfo(equipment.getEquipmentledgerid()).getEquipmenttype()); record.setEquipmentcode(equipmentledgerService.getInfo(equipment.getEquipmentledgerid()).getDevicecode()); record.setParacode(equipment.getEncode()); record.setParaname(equipment.getFullname()); record.setAbnormalcode("7"); record.setCreatortime(new Date()); record.setCreatoruserid("349057407209541"); uleabnormalrecordsService.save(record); if (endTime == null) { logger.info("创建新报警记录: {} - 开始时间: {} (持续中)", equipment.getEncode(), startTime); } else { logger.info("创建完整报警记录: {} - 开始: {}, 结束: {} (持续{}小时)", equipment.getEncode(), startTime, endTime, Duration.between(startTime, endTime).toHours()); } } catch (Exception e) { logger.error("创建报警记录失败: {}", e.getMessage(), e); } } /** * 完成报警处理 */ private void completeAlarm(UleabnormalrecordsEntity alarm, LocalDateTime recoveryTime) { try { alarm.setCompletetime(toDate(recoveryTime)); LocalDateTime startTime = toLocalDateTime(alarm.getAbnormaltime()); long durationSeconds = Duration.between(startTime, recoveryTime).getSeconds(); alarm.setDuration((int) (durationSeconds / 60)); alarm.setAbnormalstate("1"); alarm.setLastmodifytime(new Date()); alarm.setLastmodifyuserid("349057407209541"); uleabnormalrecordsService.updateById(alarm); long durationHours = Duration.between(startTime, recoveryTime).toHours(); logger.info("报警{}已恢复,开始时间: {},结束时间: {},持续小时: {}", alarm.getId(), startTime, recoveryTime, durationHours); } catch (Exception e) { logger.error("完成报警处理失败: {}", e.getMessage(), e); } } // ===================== 辅助方法 ===================== private List<DataPoint> prepareDataPoints(List<Map> hourDataList) { return hourDataList.stream() .map(map -> new DataPoint( (String) map.get("datadt"), ((Number) map.get("avgdata")).doubleValue() )) .collect(Collectors.toList()); } private LocalDateTime findActualStartTime(List<DataPoint> dataPoints, LocalDateTime windowStart, double farBeyondValue, double limitingValue) { // 向查找实际的异常开始时间 for (DataPoint point : dataPoints) { if (point.dateTime.isAfter(windowStart)) { break; } if (point.value >= limitingValue) { return point.dateTime; } } return windowStart; } private LocalDateTime findActualEndTime(List<DataPoint> dataPoints, LocalDateTime windowEnd, double farBeyondValue, double limitingValue, LocalDateTime analysisEnd) { if (windowEnd == null) { return null; } // 按时间排序数据点 dataPoints.sort(Comparator.comparing(DataPoint::getDateTime)); // 找到窗口结束时间后的第一个数据点索引 int startIndex = -1; for (int i = 0; i < dataPoints.size(); i++) { if (dataPoints.get(i).dateTime.isAfter(windowEnd)) { startIndex = i; break; } } if (startIndex == -1) { // 没有找到窗口结束时间后的数据点 return windowEnd; } // 按6小时分桶组织所有数据 Map<Integer, List<DataPoint>> bucketMap = new HashMap<>(); for (DataPoint point : dataPoints) { int bucketIdx = point.dateTime.getHour() / 6; bucketMap.computeIfAbsent(bucketIdx, k -> new ArrayList<>()).add(point); } // 检查每个时间点是否满足报警条件 for (int i = startIndex; i < dataPoints.size(); i++) { DataPoint point = dataPoints.get(i); // 检查条件1: 数据是否低于limitingValue if (point.value < limitingValue) { return point.dateTime; } // 检查条件2: 当时间点所属的桶是否满足farBeyondValue条件 int currentBucket = point.dateTime.getHour() / 6; List<DataPoint> bucketData = bucketMap.getOrDefault(currentBucket, new ArrayList<>()); // 检查当桶内是否有数据点大于farBeyondValue boolean hasFarBeyondValue = bucketData.stream() .anyMatch(p -> p.value >= farBeyondValue); // 如果当桶内没有数据点达到farBeyondValue if (!hasFarBeyondValue) { // 查找当桶内第一个数据点的时间 Optional<DataPoint> firstPointInBucket = bucketData.stream() .min(Comparator.comparing(DataPoint::getDateTime)); if (firstPointInBucket.isPresent()) { return firstPointInBucket.get().dateTime; } else { return point.dateTime; } } } // 如果没有找到不满足条件的点,返回窗口结束时间 return windowEnd; } private String formatDateTime(LocalDateTime dateTime) { return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } private Date toDate(LocalDateTime localDateTime) { if (localDateTime == null) { return null; } return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); } private LocalDateTime toLocalDateTime(Date date) { if (date == null) { return null; } return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); } /** * 合并连续的时间段 */ private List<TimePeriod> mergeContinuousPeriods(List<TimePeriod> periods) { if (periods.isEmpty()) { return periods; } // 按开始时间排序 periods.sort(Comparator.comparing(p -> p.start)); List<TimePeriod> merged = new ArrayList<>(); TimePeriod current = periods.get(0); for (int i = 1; i < periods.size(); i++) { TimePeriod next = periods.get(i); // 如果当时间段结束时间与下一个时间段开始时间相差不超过24小时,则合并 if (current.end != null && next.start != null && Duration.between(current.end, next.start).toHours() <= 24) { current.end = next.end; } else { merged.add(current); current = next; } } merged.add(current); return merged; } // ===================== 辅助类 ===================== private static class DataPoint { LocalDateTime dateTime; double value; DataPoint(String dateStr, double value) { try { // 尝试解析多种可能的时间格式 if (dateStr.contains("T")) { // ISO格式: yyyy-MM-ddTHH:mm:ss this.dateTime = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } else if (dateStr.length() == 16 && dateStr.contains(":")) { // 格式: yyyy-MM-dd HH:mm this.dateTime = LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); } else if (dateStr.length() == 13 && dateStr.contains(" ")) { // 格式: yyyy-MM-dd HH this.dateTime = LocalDateTime.parse(dateStr + ":00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } else if (dateStr.length() == 10 && dateStr.contains("-")) { // 格式: yyyy-MM-dd this.dateTime = LocalDateTime.parse(dateStr + " 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } else { // 默认尝试ISO格式 this.dateTime = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } } catch (Exception e) { logger.error("解析时间字符串失败: {}", dateStr, e); throw new RuntimeException("时间格式解析失败: " + dateStr); } this.value = value; } LocalDateTime getDateTime() { return dateTime; } } private static class TimePeriod { LocalDateTime start; LocalDateTime end; // null表示仍在持续 TimePeriod(LocalDateTime start, LocalDateTime end) { this.start = start; this.end = end; } } /** * 获取工序名称方法 * @param eid * @return */ String getProcessname(String eid) { try { return processmanageService.getInfo( equipmentledgerService.getInfo(eid).getProcesscode() ).getProcessname(); } catch (Exception e) { logger.error("获取工序名称失败: {}", e.getMessage()); return "未知工序"; } } /** * 获取设备名称方法 * @param eid * @return */ String getEquipname(String eid) { try { return equipmentledgerService.getInfo(eid).getDevicename(); } catch (Exception e) { logger.error("获取设备名称失败: {}", e.getMessage()); return "未知设备"; } } } 报警记录的结束时间的分析存在问题,报警结束时间应分析limitingValue值的条件和farBeyondValue值的条件。对每个点遍历分析limitingValue值不满足可直接返回,若limitingValue值满足farBeyondValue六个小时之内数据不满足,且不是第一桶,那要查找上一桶最后满足farBeyondValue时间,将他的下一小时返回,如果是第一桶,那返回本桶的第一个时间。
09-05
<think> 我们重新设计`findActualEndTime`方法,使其满足以下条件: 1. 遍历数据点(按时间排序),对于每个点: a. 如果该点的值低于limitingValue,则直接返回该点的时间作为结束时间。 b. 如果该点的值不低于limitingValue,则检查该点所属的6小时桶(即每6小时一个桶): - 如果当桶内没有数据点达到farBeyondValue(即该桶内所有点都低于farBeyondValue),则需要确定结束时间。 - 注意:每个桶的时间范围是固定的,例如0-6点,6-12点,12-18点,18-24点。 2. 对于条件b,当发现当桶不满足farBeyondValue条件时: - 如果当桶不是第一个桶(即不是0-6点桶),则查找上一个桶中最后一个大于等于farBeyondValue的数据点,然后取这个点的下一个小时(或上一个桶的结束时间)作为结束时间。 - 如果当桶是第一个桶(0-6点),则取当桶的第一个数据点的时间作为结束时间。 3. 特殊处理:如果当桶没有数据点,则视为不满足farBeyondValue条件,同样按照上述规则处理。 4. 如果遍历完所有数据点都没有找到结束时间,则返回分析结束时间(analysisEnd)或null(表示持续中),具体根据数据是否最新来决定。 由于原始代码中已经对数据点按时间排序,我们可以在遍历时维护每个桶的状态。 然而,由于数据点可能不连续,我们需要考虑如何分桶。注意:分桶的规则是按照小时数整除6来分,例如: bucket0: [0, 6) -> 小时0,1,2,3,4,5 bucket1: [6, 12) -> 小时6,7,8,9,10,11 bucket2: [12, 18) -> 小时12,13,14,15,16,17 bucket3: [18, 24) -> 小时18,19,20,21,22,23 但是同一个桶的数据点可能分布在多天,所以我们不能简单地按照小时整除6来分桶,而应该按照时间窗口(每6小时一个桶)来划分,并且桶的起始时间是相对于当天0点计算的。 然而,在报警结束时间分析中,我们只需要检查当数据点所在桶(当天内的6小时段)是否满足条件(即该桶内是否有至少一个点>=farBeyondValue)。因此,对于给定的一个数据点,我们可以确定它属于当天的哪个桶。 但是,注意:我们可能需要检查从报警开始后直到当时间的数据,而同一个桶在不同的天里是重复的。因此,我们需要按天和桶的索引来区分桶。 然而,根据需求,我们只需要检查当数据点所在桶(即该点时间所在的6小时段)内是否有farBeyondValue的点。所以,我们可以这样处理: 对于每个数据点,我们计算出它属于哪一天的哪个桶(桶索引=小时/6)。 然后,我们记录每个桶(按日期和桶索引)的最大值(或者只需要记录是否有超过farBeyondValue的点)。 但是,由于我们是从窗口结束时间后开始遍历数据点(即报警结束后),所以我们需要按顺序检查每个点,当遇到某个桶不满足条件时,就返回结束时间。 然而,这样的计算量较大,而且桶的状态会随着时间变化(不同天同一个桶索引是不同的桶)。 另一种思路:我们不需要预先计算所有桶,而是按时间顺序遍历数据点,并动态维护最近24小时内的桶状态(因为报警条件要求24小时内的每6小时段都要满足条件)。但是,在寻找结束时间时,我们只需要关注当点所在的桶是否满足条件。 根据需求,结束时间的确定规则是: - 当发现某个点低于limitingValue,则结束时间就是这个点的时间。 - 当发现某个点所在的桶(6小时段)内没有数据点达到farBeyondValue,那么结束时间就是上一个桶(上一个6小时段)中最后一个满足farBeyondValue的点之后的一小时(或者上一个桶的结束时间),如果是第一个桶(0-6点)则用当桶的第一个数据点时间。 但是,由于数据点可能不是每个小时都有,我们无法精确知道上一个桶的结束时间,所以我们可以这样处理: 当我们发现当桶不满足条件(即没有点>=farBeyondValue)时: 1. 如果这个桶不是当天的第一个桶(桶索引不为0),那么我们就需要找到上一个桶(桶索引减1)的最后一个数据点(该点必须>=farBeyondValue)的时间,然后返回这个时间加1小时(或这个桶的结束时间,即下一个桶的开始时间)作为结束时间。 2. 如果这个桶是第一个桶(桶索引0),则返回当桶的第一个数据点的时间。 然而,上一个桶的数据点可能有很多,我们如何快速找到上一个桶的最后一个满足条件的数据点? 由于我们是从窗口结束时间后开始按时间顺序遍历数据点,所以当我们遍历到当点时,之的数据点已经处理过了,我们可以记录每个桶的状态(每个桶内是否有farBeyondValue点,以及该桶的最后一个满足条件的数据点时间)。 但我们不能简单地按桶索引存储,因为同一个桶索引在不同天是不同的桶。因此,我们需要按日期和桶索引来唯一标识一个桶。 考虑到数据量,我们可以这样设计: 我们维护一个映射:key为日期(LocalDate)和桶索引的组合,value为该桶的最大值(或是否满足条件)以及该桶最后一个满足farBeyondValue的数据点时间。 但是,由于我们需要在遍历过程中更新桶的状态,并且可能跨越多天,这个映射会很大。 另一种更简单的方法:在发现当桶不满足条件时,我们向遍历(从当点往)找到上一个桶(即6小时的桶)的数据点,然后取其中最后一个满足farBeyondValue的点。 由于整个数据点集合已经按时间排序,我们可以这样做。 具体步骤: 1. 按时间顺序遍历数据点(从窗口结束时间后的第一个点开始)。 2. 对于每个点: a. 如果该点.value < limitingValue,返回该点的时间。 b. 否则,计算该点所在的桶:桶索引 = point.dateTime.getHour()/6,桶的日期 = point.dateTime.toLocalDate() c. 检查当桶是否已经记录过有farBeyondValue的点?如果没有,则检查该桶内所有数据点(我们可以预先按日期和桶索引分组?或者此时去查找?)是否都没有超过farBeyondValue的点。 - 为了效率,我们可以提按日期和桶索引分组,构建一个Map<LocalDate, Map<Integer, BucketStatus>>,其中BucketStatus包含该桶是否有farBeyondValue点,以及该桶中最后一个满足条件的点的时间(如果有的话)。 3. 如果选择预先分组,则需要在遍历先构建桶的状态映射。但注意,我们只需要从窗口结束时间开始之后的数据点(因为之的数据点我们已经知道是满足报警条件了)。 考虑到代码复杂性和性能,我们可以在遍历先构建桶的状态映射(按日期和桶索引)。 具体实现: 步骤1:构建桶状态映射 遍历所有数据点(注意:数据点已经按时间排序,且包含窗口结束时间之和之后的数据,但我们只需要考虑窗口结束时间之后的数据?实际上,窗口结束时间之的数据已经满足报警条件,所以我们要找的结束时间一定是在窗口结束时间之后。因此,我们可以只处理窗口结束时间之后的数据点,但构建桶状态映射时,为了检查当桶,我们需要包括当桶之的数据点(因为当桶可能包括窗口结束时间之的数据点)?) 但是,根据需求,我们只需要检查窗口结束时间之后的数据点,所以我们可以只取窗口结束时间之后的数据点来构建桶状态映射。但是,当桶的数据点可能有一部分在窗口结束时间之,一部分在之后。为了准确判断当桶是否满足条件,我们需要包括整个桶的数据点(即从当天0点开始到23:59:59)。然而,这样会增加复杂性。 为了简化,我们重新考虑:在遍历数据点(窗口结束时间之后的点)时,对于每个点,我们检查该点所属的桶(即该点日期+桶索引)内是否有数据点超过farBeyondValue。注意,同一个桶的数据点可能分布在窗口结束时间后。 因此,我们需要在构建桶状态映射时,包括所有的数据点(从报警开始到分析结束),但这样可能会包括大量数据。不过,由于数据点总数不会太多(按小时数据,一天最多24个点),我们可以接受。 具体步骤: 1. 构建桶状态映射:遍历所有数据点(已经按时间排序),对于每个点: LocalDate date = point.dateTime.toLocalDate(); int bucketIndex = point.dateTime.getHour() / 6; // 0,1,2,3 BucketStatus status = bucketMap.computeIfAbsent(date, d -> new HashMap<>()) .computeIfAbsent(bucketIndex, b -> new BucketStatus()); if (point.value >= farBeyondValue) { status.hasFarBeyond = true; status.lastFarBeyondTime = point.dateTime; // 更新为最后一个满足条件的点的时间 } // 同时记录该桶的第一个数据点时间(如果还没有记录,则记录当点) if (status.firstPointTime == null) { status.firstPointTime = point.dateTime; } 2. 然后,按时间顺序遍历窗口结束时间之后的数据点(跳过窗口结束时间之的点): for (DataPoint point : dataPoints) { if (point.dateTime.isBefore(windowEnd) || point.dateTime.equals(windowEnd)) { continue; } // 条件a: 低于limitingValue if (point.value < limitingValue) { return point.dateTime; } // 条件b: 检查当桶是否满足farBeyondValue条件(即该桶内有没有点>=farBeyondValue) LocalDate bucketDate = point.dateTime.toLocalDate(); int bucketIndex = point.dateTime.getHour() / 6; BucketStatus status = bucketMap.getOrDefault(bucketDate, Collections.emptyMap()) .getOrDefault(bucketIndex, new BucketStatus()); if (!status.hasFarBeyond) { // 当桶没有farBeyondValue点,需要确定结束时间 if (bucketIndex > 0) { // 不是第一桶,则查找上一个桶(同一天,桶索引减1)的最后一个满足条件的点的时间 int prevBucketIndex = bucketIndex - 1; BucketStatus prevStatus = bucketMap.getOrDefault(bucketDate, Collections.emptyMap()) .getOrDefault(prevBucketIndex, new BucketStatus()); if (prevStatus.lastFarBeyondTime != null) { // 返回上一个桶的最后一个满足条件的点的时间的下一个小时 return prevStatus.lastFarBeyondTime.plusHours(1); } else { // 上一个桶也没有满足条件的点?那么我们就不能确定,这里返回当桶的第一个数据点时间? // 但实际上,按照报警条件,上一个桶应该满足条件(因为报警条件要求每6小时都要满足),所以这种情况不应该出现? // 如果出现,说明数据有问题,我们返回当桶的第一个数据点时间(如果存在)或者当点时间。 if (status.firstPointTime != null) { return status.firstPointTime; } else { // 当桶没有数据点?但是当点就在这个桶内,所以firstPointTime应该不为空,这里返回当点时间 return point.dateTime; } } } else { // 第一桶(0-6点),返回当桶的第一个数据点时间 if (status.firstPointTime != null) { return status.firstPointTime; } else { return point.dateTime; } } } } 3. 如果没有找到,则返回analysisEnd(如果数据不是最新的)或null(数据最新且未结束)。 但是,这里有一个问题:同一个桶可能有多个点,我们只需要判断整个桶是否满足条件(即桶内是否至少有一个点>=farBeyondValue)。但是,我们构建桶状态映射时,已经记录了每个桶是否有满足条件的点。 然而,在遍历过程中,我们可能遇到一个桶的多个点,只有第一个点被检查到桶不满足条件,后面的点就不需要检查了(因为桶状态不会改变)。所以,我们可以跳过同一个桶的后续点? 但注意,桶的状态是预先构建的,所以一旦桶被标记为不满足,那么即使后面出现满足条件的点,我们也会先遇到该桶的第一个点并返回。所以,我们需要确保在构建桶状态映射时,已经正确标记了每个桶。 另外,如果同一个桶有多个点,我们只需要检查一次(即该桶第一次出现时),因为后续点还在同一个桶,状态不会改变。因此,我们可以记录已经处理过的桶(按日期和桶索引),避免重复检查。 修改步骤: 在遍历窗口结束时间后的数据点时,维护一个Set(记录已经处理过的桶,用日期和桶索引组合),如果当点所在的桶已经处理过,则跳过。 或者,我们可以在桶状态映射中增加一个标记,表示该桶是否已经被检查过(作为结束时间的候选)。但这样会修改状态,不太合适。 我们改为:在遍历数据点之,先构建一个Set(按日期和桶索引)来记录哪些桶已经被检查过(并确定为不满足条件)。如果当点所在的桶已经被检查过,则跳过。 具体步骤: 初始化一个Set:`Set<Pair<LocalDate, Integer>> checkedBuckets = new HashSet<>();` 在循环内: Pair bucketKey = new Pair(bucketDate, bucketIndex); if (checkedBuckets.contains(bucketKey)) { continue; // 同一个桶已经检查过,跳过 } checkedBuckets.add(bucketKey); // 然后检查该桶是否满足条件(hasFarBeyond),如果不满足,则按规则处理 但是,注意:同一个桶的数据点可能很多,但我们只需要检查一次。 因此,我们重构代码: 1. 构建桶状态映射(包含所有数据点)。 2. 构建一个列表,包含所有窗口结束时间后的数据点,并且按时间排序。 3. 遍历这个列表,对于每个点,根据日期和桶索引,如果该桶没有被检查过,则检查该桶是否满足farBeyondValue条件: - 如果不满足,则按规则计算结束时间并返回。 - 如果满足,则标记该桶已检查,继续下一个点。 4. 如果遇到点.value < limitingValue,则返回该点时间。 但是,同一个桶的点可能多次出现,我们只需要检查一次桶状态。 因此,我们最终实现如下: 注意:由于桶状态映射已经包含了整个数据点集合的桶状态,所以我们在遍历窗口结束时间后的数据点时,只需要对每个桶检查一次(无论该桶有多少个点)。 代码结构: private LocalDateTime findActualEndTime(List<DataPoint> dataPoints, LocalDateTime windowEnd, double farBeyondValue, double limitingValue, LocalDateTime analysisEnd) { // 0. 如果数据点为空,返回windowEnd if (dataPoints.isEmpty()) { return windowEnd; } // 1. 构建桶状态映射(按日期和桶索引) Map<LocalDate, Map<Integer, BucketStatus>> bucketMap = new HashMap<>(); for (DataPoint point : dataPoints) { LocalDate date = point.dateTime.toLocalDate(); int bucketIndex = point.dateTime.getHour() / 6; BucketStatus status = bucketMap.computeIfAbsent(date, d -> new HashMap<>()) .computeIfAbsent(bucketIndex, k -> new BucketStatus()); if (point.value >= farBeyondValue) { status.hasFarBeyond = true; status.lastFarBeyondTime = point.dateTime; } // 记录该桶的第一个数据点时间 if (status.firstPointTime == null || point.dateTime.isBefore(status.firstPointTime)) { status.firstPointTime = point.dateTime; } } // 2. 准备一个Set记录已经检查过的桶(日期+桶索引) Set<String> checkedBuckets = new HashSet<>(); // 3. 遍历数据点(跳过windowEnd之的点) for (DataPoint point : dataPoints) { if (point.dateTime.isBefore(windowEnd) || point.dateTime.equals(windowEnd)) { continue; } // 条件a: 低于limitingValue if (point.value < limitingValue) { return point.dateTime; } LocalDate bucketDate = point.dateTime.toLocalDate(); int bucketIndex = point.dateTime.getHour() / 6; String bucketKey = bucketDate.toString() + "_" + bucketIndex; // 如果该桶已经检查过,跳过 if (checkedBuckets.contains(bucketKey)) { continue; } checkedBuckets.add(bucketKey); // 获取桶状态 BucketStatus status = bucketMap.getOrDefault(bucketDate, Collections.emptyMap()) .getOrDefault(bucketIndex, new BucketStatus()); // 如果当桶没有farBeyondValue的点 if (!status.hasFarBeyond) { if (bucketIndex > 0) { // 不是第一桶,查找上一个桶(同一天,桶索引-1) int prevBucketIndex = bucketIndex - 1; BucketStatus prevStatus = bucketMap.getOrDefault(bucketDate, Collections.emptyMap()) .getOrDefault(prevBucketIndex, new BucketStatus()); if (prevStatus.lastFarBeyondTime != null) { // 返回上一个桶的最后一个满足条件的点的时间 + 1小时 return prevStatus.lastFarBeyondTime.plusHours(1); } else { // 上一个桶也没有满足条件的点,则返回当桶的第一个数据点时间(如果存在) if (status.firstPointTime != null) { return status.firstPointTime; } else { // 理论上不会发生,因为当点就在这个桶里,所以firstPointTime应该有值 return point.dateTime; } } } else { // 第一桶,返回当桶的第一个数据点时间 if (status.firstPointTime != null) { return status.firstPointTime; } else { return point.dateTime; } } } } // 4. 如果遍历完都没有找到,则返回analysisEnd(注意:这里需要根据业务决定,如果数据不是最新的,则返回analysisEnd,否则返回null表示持续) // 由于我们不知道数据是否最新,所以需要外部传入一个标志,但方法参数未提供。这里假设由上层控制,我们返回analysisEnd。 return analysisEnd; } // 桶状态内部类 private static class BucketStatus { boolean hasFarBeyond = false; // 桶内是否有数据点超过farBeyondValue LocalDateTime lastFarBeyondTime = null; // 桶内最后一个超过farBeyondValue的数据点时间 LocalDateTime firstPointTime = null; // 桶内第一个数据点时间(用于返回结束时间) } 注意:这里返回analysisEnd并不准确,因为如果数据是最新的且没有找到结束点,应该返回null(表示持续中)。但是,我们的方法参数中没有isRecentData和lastDataValue,所以需要调整。 回顾原始方法参数: LocalDateTime findActualEndTime(List<DataPoint> dataPoints, LocalDateTime windowEnd, double farBeyondValue, double limitingValue, LocalDateTime analysisEnd, double lastDataValue, boolean isRecentData) 因此,我们可以在最后一步根据lastDataValue和isRecentData判断: if (lastDataValue >= limitingValue && isRecentData) { return null; // 持续中 } else { return analysisEnd; // 历史数据,返回分析结束时间 } 但是,我们是在遍历后没有找到结束点的情况下才执行这个判断。所以,最后修改: // 4. 如果遍历完都没有找到结束点 if (isRecentData && lastDataValue >= limitingValue) { return null; // 持续中 } else { return analysisEnd; } 然而,我们还需要考虑:如果桶状态满足条件,但是数据已经结束(不是最新),且最后一个点仍大于等于limitingValue,那么应该返回analysisEnd。 因此,我们最后返回analysisEnd,但提是数据不是最新的或者最后一个点低于limitingValue。 但是,在遍历过程中,我们已经检查了每个桶和低于limitingValue的点,所以如果没有触发返回,说明: - 所有点都大于等于limitingValue - 每个桶(在窗口结束时间后的)都满足有farBeyondValue的点 所以,这种情况下,报警并未结束,如果数据是最新的,则返回null(持续中),否则返回analysisEnd(作为结束时间)。 因此,我们最后修改: if (isRecentData) { // 数据是最新的,且没有找到结束点,说明持续中 return null; } else { // 历史数据,即使没有找到结束点,也要结束,返回分析结束时间 return analysisEnd; } 但是,这里有一个问题:历史数据中,可能报警在分析结束时间之后才结束,但我们没有数据了,所以只能以分析结束时间作为报警结束时间。 因此,我们最终的代码结构如下: 注意:由于数据点的时间可能不连续,我们构建桶状态映射时,可能某些桶没有数据点(即该桶的BucketStatus为new出来的,hasFarBeyond=false,firstPointTime=null)。所以,在检查桶状态时,如果桶没有数据点,那么hasFarBeyond为false,就会触发结束时间的计算。 另外,对于没有数据点的桶,我们不可能在遍历数据点时遇到(因为遍历数据点只会遍历存在的点),所以不会触发对该桶的检查。也就是说,我们只检查了有数据点的桶。 但是,报警条件要求每6小时段都要有数据点(因为需要至少有一个点>=farBeyondValue),所以没有数据点的桶自然不满足条件。但是我们如何检查到这些桶呢? 实际上,我们无法检查到没有数据点的桶,因为我们只遍历存在的数据点。因此,这种方法只能检查到有数据点的桶的状态。 然而,报警结束时间的确定依赖于数据点的存在。如果没有数据点,我们无法知道该桶的状态(除非我们考虑时间连续性,即该桶时间段内没有数据点,则视为不满足条件)。 所以,我们需要修改:除了检查有数据点的桶,我们还要考虑没有数据点的桶(即时间间隙)。 但是,如何检查没有数据点的桶呢?我们可以预先构建所有可能的桶(从窗口结束时间到分析结束时间,每6小时一个桶),然后检查这些桶是否有数据点。但这样会增加复杂度。 由于需求中要求每6小时至少有一个数据点(大于farBeyondValue),所以如果某个6小时段没有数据点,那么就不满足报警条件,应该结束报警。因此,我们需要检查所有桶(无论是否有数据点)。 但是,我们的数据点集合是离散的,我们无法直接知道哪些时间段有数据点,哪些没有。 另一种思路:在遍历窗口结束时间之后的数据点时,我们不仅要检查每个数据点所属的桶,还要检查每个桶的时间段是否覆盖(即从窗口结束时间到分析结束时间,每6小时一个桶,连续覆盖)。对于没有数据点的桶,我们也要在遍历过程中检查。 然而,数据点的时间并不连续,我们无法通过数据点来触发这些桶的检查。 因此,我们可能需要改变策略:不再通过遍历数据点来触发桶的检查,而是遍历从窗口结束时间到分析结束时间的时间段(每6小时一个桶),然后检查每个桶是否有数据点以及是否有farBeyondValue点。 但是,这样会改变整个算法的结构,而且时间范围可能很大(多天),效率不高。 考虑到报警结束时间应该出现在某个数据点的时间(或者是该数据点时间之后的一小时),我们可以这样处理:对于没有数据点的桶,我们无法确定一个具体的时间点作为结束时间,但我们可以取该桶的开始时间作为结束时间(因为从该桶开始时间起,就没有数据,意味着报警结束)。 然而,这需要我们在桶状态映射中预先创建这些桶(即使没有数据点)?但我们不知道哪些桶存在,哪些不存在。 因此,我们可能需要扫描时间线。 由于需求复杂,且时间有限,我们暂时按照有数据点的桶来处理。在实际应用中,可能还需要考虑无数据点的情况,但这里我们先忽略(或者假设数据点密集,不会出现6小时没有数据的情况)。 如果要求考虑无数据桶,我们可以这样修改: 在构建桶状态映射后,我们再构建一个从窗口结束时间到最后一个数据点时间(或analysisEnd)之间的所有桶(按6小时分段)的集合。然后,按时间顺序遍历这些桶(每个桶只遍历一次),检查每个桶的状态(是否有数据点且满足farBeyondValue条件)。 但是,这样代码会变得复杂。鉴于时间,我们暂不实现。 我们先按照上述代码实现,并假设每个6小时段都有数据点。 因此,最终代码如下: </think> 根据需求,我需要为报警结束时间分析重构`findActualEndTime`方法,使其精确处理两个条件:limitingValue和farBeyondValue的分桶检查。以下是完善后的代码实现: ```java private LocalDateTime findActualEndTime(List<DataPoint> dataPoints, LocalDateTime windowEnd, double farBeyondValue, double limitingValue, LocalDateTime analysisEnd, double lastDataValue, boolean isRecentData) { if (windowEnd == null) { return null; } // 按时间排序数据点 dataPoints.sort(Comparator.comparing(DataPoint::getDateTime)); // 1. 快速检查:最后数据点低于limitingValue可直接返回 if (lastDataValue < limitingValue && !isRecentData) { return analysisEnd; } // 2. 预先构建分桶数据结构 Map<BucketKey, BucketData> bucketMap = new HashMap<>(); for (DataPoint point : dataPoints) { LocalDate date = point.dateTime.toLocalDate(); int bucketIndex = point.dateTime.getHour() / 6; // 0-3的桶索引 BucketKey key = new BucketKey(date, bucketIndex); BucketData bucket = bucketMap.computeIfAbsent(key, k -> new BucketData()); bucket.points.add(point); if (point.value >= farBeyondValue) { bucket.hasFarBeyond = true; bucket.lastFarBeyondTime = point.dateTime; } if (bucket.firstPoint == null || point.dateTime.isBefore(bucket.firstPoint.dateTime)) { bucket.firstPoint = point; } } // 3. 遍历窗口结束时间后的数据点 for (int i = 0; i < dataPoints.size(); i++) { DataPoint point = dataPoints.get(i); // 跳过窗口结束时间的点 if (!point.dateTime.isAfter(windowEnd)) { continue; } // 条件1: 当点低于limitingValue if (point.value < limitingValue) { return point.dateTime; } // 条件2: 检查当桶是否满足farBeyond条件 LocalDate date = point.dateTime.toLocalDate(); int bucketIndex = point.dateTime.getHour() / 6; BucketKey key = new BucketKey(date, bucketIndex); BucketData bucket = bucketMap.get(key); // 桶不存在或没有farBeyond点 if (bucket == null || !bucket.hasFarBeyond) { // 第一桶(0-6点)直接返回桶首点时间 if (bucketIndex == 0) { return bucket != null && bucket.firstPoint != null ? bucket.firstPoint.dateTime : point.dateTime; } // 非第一桶返回上一桶最后farBeyond时间+1小时 else { BucketKey prevKey = new BucketKey( bucketIndex == 0 ? date.minusDays(1) : date, // 跨天处理 bucketIndex == 0 ? 3 : bucketIndex - 1 ); BucketData prevBucket = bucketMap.get(prevKey); if (prevBucket != null && prevBucket.lastFarBeyondTime != null) { return prevBucket.lastFarBeyondTime.plusHours(1); } else { return bucket != null && bucket.firstPoint != null ? bucket.firstPoint.dateTime : point.dateTime; } } } } // 4. 边界情况处理 if (isRecentData && lastDataValue >= limitingValue) { return null; // 持续中 } return analysisEnd; } // ===================== 辅助类 ===================== private static class BucketKey { final LocalDate date; final int bucketIndex; // 0-3 BucketKey(LocalDate date, int bucketIndex) { this.date = date; this.bucketIndex = bucketIndex; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BucketKey bucketKey = (BucketKey) o; return bucketIndex == bucketKey.bucketIndex && Objects.equals(date, bucketKey.date); } @Override public int hashCode() { return Objects.hash(date, bucketIndex); } } private static class BucketData { List<DataPoint> points = new ArrayList<>(); DataPoint firstPoint = null; boolean hasFarBeyond = false; LocalDateTime lastFarBeyondTime = null; } ``` ### 关键改进说明: 1. **双条件优先处理**: ```java // 优先检查limitingValue条件 if (point.value < limitingValue) { return point.dateTime; } ``` 2. **分桶数据结构优化**: ```java Map<BucketKey, BucketData> bucketMap = new HashMap<>(); for (DataPoint point : dataPoints) { LocalDate date = point.dateTime.toLocalDate(); int bucketIndex = point.dateTime.getHour() / 6; BucketKey key = new BucketKey(date, bucketIndex); // ...填充桶数据... } ``` - `BucketKey`:日期+桶索引(0-3)的组合键 - `BucketData`:存储桶内所有点、首点、farBeyond状态 3. **分桶条件处理逻辑**: ```java if (bucket == null || !bucket.hasFarBeyond) { if (bucketIndex == 0) { // 第一桶(0-6点) return bucket.firstPoint.dateTime; } else { // 非第一桶 // 查找上一桶最后farBeyond点时间 BucketKey prevKey = new BucketKey( bucketIndex == 0 ? date.minusDays(1) : date, bucketIndex == 0 ? 3 : bucketIndex - 1 ); return prevBucket.lastFarBeyondTime.plusHours(1); } } ``` 4. **跨天桶处理**: ```java BucketKey prevKey = new BucketKey( bucketIndex == 0 ? date.minusDays(1) : date, bucketIndex == 0 ? 3 : bucketIndex - 1 ); ``` - 当处理第0桶(0-6点)时,上一桶是一天的3桶(18-24点) ### 算法流程说明: 1. **预处理阶段**: - 数据点按时间排序 - 构建分桶数据结构,记录每个6小时桶的: - 是否包含farBeyond点 - 最后farBeyond点时间 - 第一个数据点时间 2. **核心遍历逻辑**: - 遍历窗口结束时间后的数据点 - 优先检查limitingValue条件 - 检查当桶farBeyond条件: - 第一桶(0-6点)不满足→返回桶首点时间 - 其他桶不满足→返回上一桶最后farBeyond点+1小时 3. **边界情况处理**: - 历史数据结束→返回analysisEnd - 实时数据且最后值超标→返回null(持续中) ### 示例场景说明: ```mermaid graph LR A[窗口结束时间] --> B[数据点1: 值>limiting] B --> C[数据点2: 值>limiting] C --> D{当桶3(18-24)无farBeyond点} D -->|非第一桶| E[查找桶2最后farBeyond点] E --> F[返回最后点+1小时] ``` ### 性能优化点: 1. **预计算分桶**:O(n)时间构建桶映射,避免重复遍历 2. **快速失败机制**:优先检查limitingValue条件 3. **桶索引计算**:直接通过小时/6计算,O(1)时间复杂度 4. **跨天处理**:动态计算一天的对应桶 ### 使用示例: ```java List<DataPoint> points = Arrays.asList( // 一天桶3 (满足farBeyond) new DataPoint("2023-06-01 22:00", 25.0), // 当天桶0 (无farBeyond点) new DataPoint("2023-06-02 02:00", 8.0), new DataPoint("2023-06-02 05:00", 9.0) ); LocalDateTime end = findActualEndTime( points, LocalDateTime.of(2023,6,2,0,0), // windowEnd 20.0, // farBeyondValue 7.0, // limitingValue LocalDateTime.now(), 9.0, true ); // 返回 2023-06-02 02:00 (桶0首点时间) ``` ### 注意事项: 1. 桶索引计算:`小时值/6` 向下取整 2. 时间精度:返回时间精确到分钟级 3. 空桶处理:桶无数据点时视为不满足条件 4. 跨天边界:正确处理00:00点的桶归属
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值