票据区间管理:从需求分析到完整实现
一、需求分析与解题思路
1. 问题理解
业务场景:
- 每张票据有一个连续的号码区间(如1-100)
- 业务操作会随机拆分出子区间(如10-15,30-39)
- 需要实时获取:
- 当前剩余可用区间
- 已拆分的区间集合
- 原始完整区间
2. 核心挑战
- 区间运算:如何高效计算剩余区间
- 状态维护:如何跟踪原始、拆分和剩余区间
- 并发安全:多线程环境下的数据一致性
3. 解决思路
- 基础建模:用Range类表示区间
- 核心算法:实现区间减法运算
- 状态管理:Ticket类维护三种区间状态
- 服务封装:TicketService提供线程安全操作
- 可视化: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;
}
逻辑流程:
- 检查左侧是否有剩余(当前start < 其他start)
- 检查右侧是否有剩余(当前end > 其他end)
- 返回剩余部分(可能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值
四、系统扩展建议
-
持久化存储:
- 使用数据库保存票据状态
- 实现
TicketRepository
接口
-
批量操作优化:
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(); } }
-
性能监控:
- 添加拆分操作耗时统计
- 监控剩余区间碎片化程度
这个实现完整展示了从需求分析到代码实现的完整过程,包含:
- 基础数据结构设计(Range)
- 核心业务逻辑(Ticket)
- 线程安全服务(TicketService)
- 使用演示和算法解析
- 扩展建议