关于12306购票算法的设计与实现

在网上一直听别人吹12306的算法多么多么复杂;整的挺玄乎的;周末在高铁站无聊的时候顺便思考这个问题打发时间;

最后发现它这个算法其实挺简单的;没有网上的那么复杂;

思路

简单说一下思路;

它其实就是一个二叉树的分裂算法;

假设我们的票有12个站点; 这里用1-12表示每个站点的名字; 1,2,3,4,5,6,7,8,9,10,11,12;

用户随机购买一段行程; 例如:从2号站上车到8号站下车;当出票成功之后,这张全程票就会裂变成两张新的票;区间分别是1-2;8-12;

如下

再例如:又有一个用户购买了9-11区间;那么就将8-12区间作为一个主节点;8-12节点重新裂变成:8-9;11-12;两个节点‘

现在这张票还剩下 1-2; 8-9; 11-12三个区间的票;这三个区间可以继续卖;但是已经无法再裂变了;

这就是一个超级简单的二叉树;当在某一个节点下购买一张票;那么这个节点就会自动裂变成0-2个子节点;全程票就是我们的根节点;

问题二: 权重问题;

当我们卖9-11的票时;我们可以在8-12里面出票;也可以在全程票1-12中出票;为了不影响购买全程票的用户;我们只能选择在8-12节点下出票;这里我们以区间数作为我们的权重判断标准;(这里不考虑多人候补的情况)

我们以节点经过的区间数量为权重; 例如1-2:算1个区间;1-12算11个区间;区间越小;权重越高;优先在这些区间里面出票;

问题三:树完整性问题

多棵数以座位号区分;

每个座位号都是一颗独立的数;

简单实现

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Data
class Plan implements Comparable<Plan> {

    @ApiModelProperty("起始站点")
    private Integer startPoint;

    @ApiModelProperty("终点站点")
    private Integer endPoint;

    @ApiModelProperty("经过的站点数量")
    private Integer routeSiteNum;

    @ApiModelProperty("座位号")
    private Integer seatNum;


    @Override
    public int compareTo(Plan o) {
        return this.startPoint.compareTo(o.getStartPoint());
    }

    public static Plan build(Integer startPoint, Integer endPoint, Integer seatNum) {
        if (startPoint.equals(endPoint)) {
            return null;
        }
        Plan plan = new Plan();
        plan.setStartPoint(startPoint);
        plan.setEndPoint(endPoint);
        plan.setRouteSiteNum(endPoint - startPoint);
        plan.setSeatNum(seatNum);
        return plan;
    }

    public static Plan build(Integer startPoint, Integer endPoint) {
        if (startPoint.equals(endPoint)) {
            return null;
        }
        Plan plan = new Plan();
        plan.setStartPoint(startPoint);
        plan.setEndPoint(endPoint);
        plan.setRouteSiteNum(endPoint - startPoint);
        return plan;
    }

    public final static String[] addressName = new String[]{"北京", "上海", "广州", "深圳", "成都", "杭州", "武汉", "重庆", "西安", "南京", "长沙", "南宁"};


    public static boolean play(TreeMap<Integer, List<Plan>> map, Plan plan, TreeMap<Integer, List<Plan>> ticketMap) {
        // 最简单的实现
        for (Integer key : map.keySet()) {
            if (key < plan.getRouteSiteNum()) {
                continue;
            }
            List<Plan> plans = map.get(key);
            for (Plan item : plans) {
                if (item.startPoint <= plan.getStartPoint() && item.getEndPoint() >= plan.getEndPoint()) {
                    // 这里表示出票成功
                    // 如果出票成功,那么就会生成1-2张新的票据
                    log.info("出票成功:原票起始站: {};终点站:{}", addressName[item.getStartPoint() - 1], addressName[item.getEndPoint() - 1]);
                    plans.remove(item);
                    plan.setSeatNum(item.getSeatNum());
                    List<Plan> tickets = ticketMap.getOrDefault(plan.getSeatNum(), new ArrayList<>());
                    tickets.add(plan);
                    // 这里无所谓排序;只是为了debug 的时候可以更好的观察每个座位的出票情况;
                    Collections.sort(tickets);
                    ticketMap.put(plan.getSeatNum(), tickets);
                    addNewPlanToMap(map, build(item.getStartPoint(), plan.getStartPoint(), item.getSeatNum()));
                    addNewPlanToMap(map, build(plan.getEndPoint(), item.getEndPoint(), item.getSeatNum()));
                    return true;
                }
            }
        }
        return false;
    }

    public static void addNewPlanToMap(TreeMap<Integer, List<Plan>> map, Plan plan) {
        if (plan == null) {
            return;
        }
        List<Plan> planList = map.getOrDefault(plan.getRouteSiteNum(), new ArrayList<Plan>());
        planList.add(plan);
        map.put(plan.getRouteSiteNum(), planList);
    }

}

@Slf4j
public class TrainPlan {

    public static void main(String[] args) {
        TreeMap<Integer, List<Plan>> map = new TreeMap<>();
        // 这里就是初始化1000张全程票
        for (int i = 0; i < 1000; i++) {
            Plan plan = Plan.build(1, 12, i + 1);
            assert plan != null;
            List<Plan> planList = map.getOrDefault(plan.getRouteSiteNum(), new ArrayList<>());
            planList.add(plan);
            map.put(plan.getRouteSiteNum(), planList);
        }
        int i = 0;
        // 准备4000个购票区间,模拟用户买票;
        List<Plan> currentPlan = new ArrayList<>();
        while (i < 4000) {
            int startPoint = (int) (Math.random() * 12) + 1;
            int endPoint = (int) (Math.random() * 12) + 1;
            if (startPoint < endPoint) {
                currentPlan.add(Plan.build(startPoint, endPoint));
                i += 1;
            }
        }
        log.info("程序准备完成...");
        List<Plan> errorPlan = new ArrayList<>(); //
        // 这里记录每个座位的出票情况; key是座位号
        TreeMap<Integer, List<Plan>> trainTicketMap = new TreeMap<>();
        for (Plan plan : currentPlan) {
            boolean play = Plan.play(map, plan, trainTicketMap);
            if (play) {
                log.info("出票成功: {} ~ {}; 座位号: {}", Plan.addressName[plan.getStartPoint() - 1], Plan.addressName[plan.getEndPoint() - 1], plan.getSeatNum());
            } else {
                errorPlan.add(plan);
            }
        }
        if (errorPlan.size() > 0) {
            for (Plan plan : errorPlan) {
                log.error("出票失败: {} ~ {}", Plan.addressName[plan.getStartPoint() - 1], Plan.addressName[plan.getEndPoint() - 1]);
            }
        }
    }

}

执行结果;;;

例如:1号座位的出票结果为

1-3区间 出票成功

3-5区间 出票成功

5-6区间 出票成功

6-9区间 出票成功

9-10区间 出票成功

10-12区间 出票成功

总体说1号座位全程都卖出去了

总座位数1000;

随机购买4000张票;有1536张出票失败;

这里的权重优先级可以继续优化调整,我这里只是用写一个思路,没有细化

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值