票据剩余区间计算解决方案

票据区间管理:从需求分析到完整实现

一、需求分析与解题思路

1. 问题理解

业务场景

  • 每张票据有一个连续的号码区间(如1-100)
  • 业务操作会随机拆分出子区间(如10-15,30-39)
  • 需要实时获取:
    • 当前剩余可用区间
    • 已拆分的区间集合
    • 原始完整区间

2. 核心挑战

  • 区间运算:如何高效计算剩余区间
  • 状态维护:如何跟踪原始、拆分和剩余区间
  • 并发安全:多线程环境下的数据一致性

3. 解决思路

  1. 基础建模:用Range类表示区间
  2. 核心算法:实现区间减法运算
  3. 状态管理:Ticket类维护三种区间状态
  4. 服务封装:TicketService提供线程安全操作
  5. 可视化:TicketVisualizer方便调试查看

二、完整实现代码(含详细注释)

1. Range类(区间基础模型)

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 表示一个连续的整数区间,包含起始和结束值
 */
public class Range implements Comparable<Range> {
    
    
    private final int start;
    private final int end;

    /**
     * 构造函数
     * @param start 区间起始值(包含)
     * @param end 区间结束值(包含)
     * @throws IllegalArgumentException 如果起始值大于结束值
     */
    public Range(int start, int end) {
    
    
        if (start > end) {
    
    
            throw new IllegalArgumentException("区间起始值不能大于结束值");
        }
        this.start = start;
        this.end = end;
    }

    public int getStart() {
    
    
        return start;
    }

    public int getEnd() {
    
    
        return end;
    }

    /**
     * 检查数字是否包含在区间内
     * @param number 要检查的数字
     * @return 如果数字在区间内返回true
     */
    public boolean contains(int number) {
    
    
        return number >= start && number <= end;
    }

    /**
     * 检查当前区间是否与另一区间重叠
     * @param other 另一区间
     * @return 如果两区间有重叠返回true
     */
    public boolean overlaps(Range other) {
    
    
        return this.start <= other.end && this.end >= other.start;
    }

    /**
     * 从当前区间减去另一区间
     * @param other 要减去的区间
     * @return 减法操作后的剩余区间列表
     */
    public List<Range> subtract(Range other) {
    
    
        List<Range> result = new ArrayList<>();
        
        if (!this.overlaps(other)) {
    
    
            result.add(this);
            return result;
        }
        
        if (this.start < other.start) {
    
    
            result.add(new Range(this.start, other.start - 1));
        }
        
        if (this.end > other.end) {
    
    
            result.add(new Range(other.end + 1, this.end));
        }
        
        return result;
    }

    /**
     * 计算区间长度(包含的数字个数)
     * @return 区间长度
     */
    public int length() {
    
    
        return end - start + 1;
    }

    @Override
    public boolean equals(Object o) {
    
    
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Range range = (Range) o;
        return start == range.start && end == range.end;
    }

    @Override
    public int hashCode() {
    
    
        return Objects.hash(start, end);
    }

    @Override
    public String toString() {
    
    
        return "[" + start + "-" + end + "]";
    }

    @Override
    public int compareTo(Range other) {
    
    
        return Integer.compare(this.start, other.start);
    }
}

2. Ticket类(票据状态管理)

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * 表示一张票据及其区间状态
 */
public class Ticket {
    
    
    private final String ticketNumber;
    private final Range originalRange;
    private final List<Range> splitRanges;
    private List<Range> remainingRanges;

    /**
     * 构造函数
     * @param ticketNumber 票据编号
     * @param originalRange 原始区间
     */
    public Ticket(String ticketNumber, Range originalRange) {
    
    
        this.ticketNumber = Objects.requireNonNull(ticketNumber);
        this.originalRange = Objects.requireNonNull(originalRange);
        this.splitRanges = new ArrayList<>();
        this.remainingRanges = new ArrayList<>();
        this.remainingRanges.add(new Range(originalRange.getStart(), originalRange.getEnd()));
    }

    public String getTicketNumber() {
    
    
        return ticketNumber;
    }

    public Range getOriginalRange() {
    
    
        return originalRange;
    }

    /**
     * 获取已拆分的区间(不可修改的视图)
     * @return 已拆分区间列表
     */
    public List<Range> getSplitRanges() {
    
    
        return Collections.unmodifiableList(splitRanges);
    }

    /**
     * 获取剩余区间(不可修改的视图)
     * @return 剩余区间列表
     */
    public List<Range> getRemainingRanges() {
    
    
        return Collections.unmodifiableList(remainingRanges);
    }

    /**
     * 执行拆分操作
     * @param rangeToSplit 要拆分的区间
     * @throws IllegalArgumentException 如果拆分区间无效
     */
    public void split(Range rangeToSplit) {
    
    
        if (!isValidSplitRange(rangeToSplit)) {
    
    
            throw new IllegalArgumentException("拆分区间不合法");
        }

        List<Range> newRemaining = new ArrayList<>();
        for (Range current : remainingRanges) {
    
    
            if (current.overlaps(rangeToSplit)) {
    
    
                newRemaining.addAll(current.subtract(rangeToSplit));
            } else {
    
    
                newRemaining.add(current);
            }
        }
        
        splitRanges.add(rangeToSplit);
        remainingRanges = mergeAdjacentRanges(newRemaining);
    }

    /**
     * 检查拆分区间是否有效
     * @param range 要检查的区间
     * @return 如果有效返回true
     */
    private boolean isValidSplitRange(Range range) {
    
    
        for (Range remaining : remainingRanges) {
    
    
            if (remaining.getStart() <= range.getStart() && 
                remaining.getEnd() >= range.getEnd()) {
    
    
                return true;
            }
        }
        return false;
    }

    /**
     * 合并相邻区间
     * @param ranges 要合并的区间列表
     * @return 合并后的区间列表
     */
    private List<Range> mergeAdjacentRanges(List<Range> ranges) {
    
    
        if (ranges.isEmpty()) {
    
    
            return ranges;
        }

        List<Range> sorted = new ArrayList<>(ranges);
        sorted.sort(Comparator.comparingInt(Range::getStart));

        List<Range> merged = new ArrayList<>();
        Range current = sorted.get(0);

        for (int i = 1; i < sorted.size(); i++) {
    
    
            Range next = sorted.get(i);
            if (current.getEnd() + 1 >= next.getStart()) {
    
    
                current = new Range(
                    current.getStart(),
                    Math.max(current.getEnd(), next.getEnd())
                );
            } else {
    
    
                merged.add(current);
                current = next;
            }
        }
        merged.add(current);

        return merged;
    }

    /**
     * 计算剩余区间占总区间的比例
     * @return 比例值(0.0到1.0)
     */
    public double getRemainingRatio() {
    
    
        int originalLength = originalRange.length();
        if (originalLength == 0) return 0.0;
        
        int remainingLength = 0;
        for (Range range : remainingRanges) {
    
    
            remainingLength += range.length();
        }
        
        return (double) remainingLength / originalLength;
    }
}

3. TicketService类(线程安全服务)

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 提供线程安全的票据管理服务
 */
public class TicketService {
    
    
    private final Map<String, Ticket> ticketStore = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    /**
     * 创建新票据
     * @param ticketNumber 票据编号
     * @param originalRange 原始区间
     * @throws IllegalArgumentException 如果票据已存在
     */
    public void createTicket(String ticketNumber, Range originalRange) {
    
    
        writeLock.lock();
        try {
    
    
            if (ticketStore.containsKey(ticketNumber)) {
    
    
                throw new IllegalArgumentException("票据已存在: " + ticketNumber);
            }
            ticketStore.put(ticketNumber, new Ticket(ticketNumber, originalRange));
        } finally {
    
    
            writeLock.unlock();
        }
    }

    /**
     * 拆分票据区间
     * @param ticketNumber 票据编号
     * @param rangeToSplit 要拆分的区间
     * @throws IllegalArgumentException 如果票据不存在或拆分区间无效
     */
    public void splitTicket(String ticketNumber, Range rangeToSplit) {
    
    
        writeLock.lock();
        try {
    
    
            Ticket ticket = getTicket(ticketNumber);
            ticket.split(rangeToSplit);
        } finally {
    
    
            writeLock.unlock();
        }
    }

    /**
     * 获取票据的原始区间
     * @param ticketNumber 票据编号
     * @return 原始区间,如果票据不存在返回null
     */
    public Range getOriginalRange(String ticketNumber) {
    
    
        readLock.lock();
        try {
    
    
            Ticket ticket = ticketStore.get(ticketNumber);
            return ticket != null ? ticket.getOriginalRange() : null;
        } finally {
    
    
            readLock.unlock();
        }
    }

    /**
     * 获取已拆分的区间
     * @param ticketNumber 票据编号
     * @return 已拆分区间列表,如果票据不存在返回空列表
     */
    public List<Range> getSplitRanges(String ticketNumber) {
    
    
        readLock.lock();
        try {
    
    
            Ticket ticket = ticketStore.get(ticketNumber);
            return ticket != null ? ticket.getSplitRanges() : Collections.emptyList();
        } finally {
    
    
            readLock.unlock();
        }
    }

    /**
     * 获取剩余区间
     * @param ticketNumber 票据编号
     * @return 剩余区间列表,如果票据不存在返回空列表
     */
    public List<Range> getRemainingRanges(String ticketNumber) {
    
    
        readLock.lock();
        try {
    
    
            Ticket ticket = ticketStore.get(ticketNumber);
            return ticket != null ? ticket.getRemainingRanges() : Collections.emptyList();
        } finally {
    
    
            readLock.unlock();
        }
    }

    /**
     * 获取剩余比例
     * @param ticketNumber 票据编号
     * @return 剩余比例(0.0到1.0),如果票据不存在返回0.0
     */
    public double getRemainingRatio(String ticketNumber) {
    
    
        readLock.lock();
        try {
    
    
            Ticket ticket = ticketStore.get(ticketNumber);
            return ticket != null ? ticket.getRemainingRatio() : 0.0;
        } finally {
    
    
            readLock.unlock();
        }
    }

    /**
     * 内部方法:获取票据对象
     * @param ticketNumber 票据编号
     * @return 票据对象
     * @throws IllegalArgumentException 如果票据不存在
     */
    private Ticket getTicket(String ticketNumber) {
    
    
        Ticket ticket = ticketStore.get(ticketNumber);
        if (ticket == null) {
    
    
            throw new IllegalArgumentException("票据不存在: " + ticketNumber);
        }
        return ticket;
    }
}

4. 票据状态可视化工具

import java.util.List;

/**
 * 票据状态可视化工具
 */
public class TicketVisualizer {
    
    
    /**
     * 打印票据状态
     * @param service 票据服务
     * @param ticketNumber 票据编号
     */
    public static void printTicketStatus(TicketService service, String ticketNumber) {
    
    
        System.out.println("\n=== 票据状态 ===");
        System.out.println("票据编号: " + ticketNumber);
        
        Range original = service.getOriginalRange(ticketNumber);
        System.out.println("原始区间: " + (original != null ? original : "无"));
        
        List<Range> splits = service.getSplitRanges(ticketNumber);
        System.out.println("\n已拆分区间(" + splits.size() + "个):");
        for (int i = 0; i < splits.size(); i++) {
    
    
            System.out.println("  " + (i + 1) + ". " + splits.get(i));
        }
        
        List<Range> remaining = service.getRemainingRanges(ticketNumber);
        System.out.println("\n剩余区间(" + remaining.size() + "段):");
        for (int i = 0; i < remaining.size(); i++) {
    
    
            System.out.println("  " + (i + 1) + ". " + remaining.get(i));
        }
        
        double ratio = service.getRemainingRatio(ticketNumber);
        System.out.printf("\n剩余比例: %.2f%%\n", ratio * 100);
        System.out.println("====================\n");
    }
}

5. 演示主类

/**
 * 票据系统演示
 */
public class TicketSystemDemo {
    
    
    public static void main(String[] args) {
    
    
        // 初始化服务
        TicketService service = new TicketService();
        TicketVisualizer visualizer = new TicketVisualizer();
        
        // 创建票据
        String ticketNo = "T20230001";
        service.createTicket(ticketNo, new Range(1, 100));
        System.out.println("创建票据: " + ticketNo + " 区间[1-100]");
        visualizer.printTicketStatus(service, ticketNo);
        
        // 第一次拆分
        Range split1 = new Range(10, 20);
        service.splitTicket(ticketNo, split1);
        System.out.println("执行拆分: " + split1);
        visualizer.printTicketStatus(service, ticketNo);
        
        // 第二次拆分
        Range split2 = new Range(30, 40);
        service.splitTicket(ticketNo, split2);
        System.out.println("执行拆分: " + split2);
        visualizer.printTicketStatus(service, ticketNo);
        
        // 第三次拆分(与前一区间重叠)
        Range split3 = new Range(35, 45);
        service.splitTicket(ticketNo, split3);
        System.out.println("执行拆分: " + split3);
        visualizer.printTicketStatus(service, ticketNo);
        
        // 尝试无效拆分
        try {
    
    
            Range invalidSplit = new Range(150, 200);
            service.splitTicket(ticketNo, invalidSplit);
        } catch (IllegalArgumentException e) {
    
    
            System.out.println("\n拆分操作失败: " + e.getMessage());
        }
        
        // 最终状态
        System.out.println("\n最终状态:");
        visualizer.printTicketStatus(service, ticketNo);
    }
}

三、关键算法解析

1. 区间减法运算

public List<Range> subtract(Range other) {
    
    
    List<Range> result = new ArrayList<>();
    
    if (this.start < other.start) {
    
    
        // 左侧剩余部分
        result.add(new Range(this.start, other.start - 1)); 
    }
    
    if (this.end > other.end) {
    
    
        // 右侧剩余部分
        result.add(new Range(other.end + 1, this.end));
    }
    
    return result;
}

逻辑流程

  1. 检查左侧是否有剩余(当前start < 其他start)
  2. 检查右侧是否有剩余(当前end > 其他end)
  3. 返回剩余部分(可能0、1或2个区间)

2. 合并相邻区间

private List<Range> mergeAdjacentRanges(List<Range> ranges) {
    
    
    ranges.sort(Comparator.comparingInt(Range::getStart));
    List<Range> merged = new ArrayList<>();
    Range current = ranges.get(0);
    
    for (int i = 1; i < ranges.size(); i++) {
    
    
        Range next = ranges.get(i);
        if (current.getEnd() + 1 >= next.getStart()) {
    
    
            // 合并条件:相邻或重叠
            current = new Range(current.getStart(), Math.max(current.getEnd(), next.getEnd()));
        } else {
    
    
            merged.add(current);
            current = next;
        }
    }
    merged.add(current);
    return merged;
}

合并规则

  • 区间按起始值排序
  • 如果当前区间的end+1 ≥ 下一区间的start,则合并
  • 合并后取两区间的最大end值

四、系统扩展建议

  1. 持久化存储

    • 使用数据库保存票据状态
    • 实现TicketRepository接口
  2. 批量操作优化

    public void batchSplit(String ticketNo, List<Range> splits) {
          
          
        writeLock.lock();
        try {
          
          
            Ticket ticket = getTicket(ticketNo);
            for (Range split : splits) {
          
          
                ticket.split(split);
            }
        } finally {
          
          
            writeLock.unlock();
        }
    }
    
  3. 性能监控

    • 添加拆分操作耗时统计
    • 监控剩余区间碎片化程度

这个实现完整展示了从需求分析到代码实现的完整过程,包含:

  • 基础数据结构设计(Range)
  • 核心业务逻辑(Ticket)
  • 线程安全服务(TicketService)
  • 使用演示和算法解析
  • 扩展建议