项目开发感悟——算法篇

最近在做一个物流的项目,自己负责线路配置这个模块还有部分接口的开发。

线路配置里面有三个模块:专线列表、班次模板、班次列表。

专线列表主要是记录区域车次停靠点,班次模板主要是用于批量生产班次,班次列表是实际业务逻辑要用到的数据,也是呈现给用户看的数据。

业务流程:

     后台方面:

在专线列表里面添加区域点,通过区域点,可以一键生成班次模板,通过班次模板可以一键生成班次列表;

譬如a,b,  c,  d,  e: 如图 1

然而生成班次模板的算法也很简单,将专线生面的区域点,按照顺序排列,采用排列组合的方式生成班次模板,然后在班次模板里面完善发车时间,耗时,价格等数据。

五个点生成10个班次模板就是 C_{5}^{2} :如图 2

通过班次模板生成连续多少天生成批量生成具体的班次,那班次的具体开始时间,结束时间,价格等都会生成。

                

     接口方面:

  1.也就是和携程里面一样,输入地点和起始时间,可以找到相应的班次信息。

  2.减箱子操作,类似火车票卖出去一张票的操作(这个我花的心力比较多)

  当然重点就是减箱子这个接口:

首先拿到需求的时候,专线只有单单一条(每个专线互不相关),并无子线这一说,所以我先以没有子线的时候说起:

假设现在专线上面有如图五个区域点为例子:

假设b - d 这一段卖出 ,那么受影响的班次就有a - d , a - e , b - d , b - e , c - d , c - e, 也就是说所有涵盖b - d这一段的班次和b - d里面的班次都要进行减箱子操作,那么怎么查找这些受影响的班次呢?正常直观的思维,找到所有起点是b的班次和所有终点是d的班次,还有起点终点是c的班次(如果中间何有c1,c2等,那么就得起点终点是,c,c1,c2的班次,依此类推),具体班次如图:3

通过图可以发现,出现了重复的班次,如果专线更长一点的话,那么就会造成资源的很多浪费,那么怎么解决呢。仔细看这些要减的班次,反观那些没有减的班次,会很容易发现,除了起点之前(包括起点)生成的班次和终点之后(包括终点)生成的班次不受影响,其他班次都要受影响,这样去思考问题就简单多了:如图

突然发现,自己的任务挺简单的,很容易就解决了;然而很快就发现高兴早了,需求有变动了,子线这个问题出来了,如图:4

所以以前生成班次模板的代码也要改,刚开始的事后想过用链表,后来发现其实没必要,只要变成下图的样子就行了,以前的代码也不用大改:如图 5

好了,现在回归正题,这个时候减箱子也会涉及到子线的问题了(这里说明一下,B1到D2其实是三辆车在跑,只不过是箱子运到B后换另一辆继续跑,有点像换乘,但是一个车一次就只能拖那么几个箱子,所以当b-d卖出一个箱子后,对应的b1 - d2也会减一个箱子),或许你会说直接像上述图5一样的延展,沿用原来的思想不就很好了吗,如果真这样做了,那么问题会很大,一方面会有减漏掉的bug,另外一方面这样做也会有很多不必要的查询,同时要遍历所有班次,这其实也是一件挺耗性能的事(虽然我一开始就是这样做的,后来重构代码的时候仔细考虑了下就改了),这么说你可能不服,先来谈谈有什么bug吧。同样我们以b - d 为列

1.第一个bug,我们可以很直观的发现b1 - b2 这趟班次是不用涉及到减箱子的操作了,应为这是另外一趟班次了。解决办法:设置一个判断语句,起点终点是同一子线上的点事就不进行减箱子操作,这样貌似好像解决,暂时不谈对性能的影响;

2.第二个bug,倘若b - d减了一个箱子,那么理所当然A -b1也要减一个箱子,其实并不用减。这次可就没法像上面那样好处里。

看到这里其实你大概应该有点想法,这样做其实不可行。我们可以直接沿用最开始的线路,不涉及子线按照原先的方法找到要减的线路,然后用原先线路的起点下面的子点和自己(如B,B1,B2),原先线路的终点下面的子点和自己(如D,D1,D2)进行笛卡尔乘积,这样就完美的避开了上述的两个bug;这里我就不细说,你可以自己去验证。

看到这里你可能觉得已经完了,事实没有,因为这样减箱子存在重复减的问题,如图:

倘若一个车一次只能装载6个箱子,那么初始的时候b-c, c-d, b-d,就都是6个箱子,倘若按照以前的算法,b-c卖掉6个箱子,这个时候b-d就和b-c一样只剩下0个箱子了,这个时候c-d再减一个箱子,那么就会如上图所示出现负值。出现重复减的状况!

针对这个状况,我是这样设计的。为了方便理解,我们将箱子当做有多少个座位,不考虑过程,只考虑发车前有多少个座位(最终重点就不计了),我们设置一个数组用来存放a-b, b-c, c-d, d-e的座位数(a,b,c,d用来存放座位数),倘若是b点(b-c)卖掉2个座位,c(c-d)卖掉1个座位,那么这个时候b-d就取卖掉座位多个剩余座位数,也就是去最小剩余座位数b.

下面是我的代码实现:



@Service("containeNumOperateService")
public class ContaineNumOperateService {

    @Resource(name = "daoSupport")
    private DaoSupport dao;

    /**
     * 通过主线的起始点,将子线(包括自己)进行箱子数量修改操作
     * @param startCity
     * @param endCity
     * @param pd
     * @param number 最小箱子数量
     * @throws Exception
     */
    private void changeClassesContaineNum(String startCity, String endCity,PageData pd,int number) throws Exception {
        String specialLineName = pd.getString("SPECIAL_LINE_NAME");

        //查询当前节点所有的子节点:如果当前节点是子节点,那么就查询兄弟节点(包括自己)
        List<String> startChildSpecialLineList = getChildSpecialLineList(startCity,specialLineName);
        List<String> endChildSpecialLineList = getChildSpecialLineList(endCity,specialLineName);

        //int number = minContaineNum(majorList, specialLineArray, startCity, endCity);

        //将已知起始点的子线(包括自己)进行箱子数量修改操作
        changeContaineNum(startChildSpecialLineList,endChildSpecialLineList,number,pd);
    }

    /**
     * 将已知起始点的子线(包括自己)进行箱子数量修改操作
     * @param startChildSpecialLineList
     * @param endChildSpecialLineList
     * @param number 最小箱子数量
     * @param pd
     * @throws Exception
     */
    private void changeContaineNum(List<String> startChildSpecialLineList,List<String> endChildSpecialLineList,int number,PageData pd) throws Exception{
        for(int m=0;m<startChildSpecialLineList.size();m++){
            for (int n=0;n<endChildSpecialLineList.size();n++){
                //设置查询条件,
                PageData pdQuery = new PageData();
                pdQuery.put("START_CITY", startChildSpecialLineList.get(m));
                pdQuery.put("END_CITY", endChildSpecialLineList.get(n));
                pdQuery.put("SPECIAL_LINE_NAME", pd.getString("SPECIAL_LINE_NAME"));
                pdQuery.put("TRAIN_NUMBER_ID", pd.getString("TRAIN_NUMBER_ID"));
                PageData classesPd = (PageData) dao.findForObject("Classes_listMapper.findSingleClasses", pdQuery);
                //比较当前线路箱子数与minNum是否相同,如果不同则将该班次箱子数量改为minNum
                int pdContaineNum = (Integer) classesPd.get("CONTAINE_NUM");
                if (number != pdContaineNum) {
                    classesPd.put("CONTAINE_NUM", number);
                    //classes_listService.updateContaineNum(classesList.get(0));
                    dao.update("Classes_listMapper.updateContaineNum", classesPd);
                }
            }
        }
    }

    /**
     * 查询当前节点所有的子节点:如果当前节点是子节点,那么就查询兄弟节点(包括自己)
     * @param cityName
     * @param specialLineName
     * @return
     * @throws Exception
     */
    private List<String> getChildSpecialLineList(String cityName,String specialLineName) throws Exception{
        PageData queryPd = new PageData();
        queryPd.put("CITY_NAME",cityName);
        queryPd.put("SPECIAL_LINE_NAME",specialLineName);
        List<PageData> ChildSpecialLineList = (List<PageData>)dao.findForList("SpecialLineMapper.findBySuperiorSpecialLineAndParentCity", queryPd);
        List<String> cityNameList = new ArrayList<>();
        cityNameList.add(cityName);
        for(PageData pd:ChildSpecialLineList){
            cityNameList.add(pd.getString("CITY_NAME"));
        }
        return cityNameList;
    }

    /**
     * 查看当前城市的父节点,如果不存在就是当前值
     * @param majorCityList
     * @param cityName
     * @param specialLineName
     * @return
     */
    private String getParentCity(List<String> majorCityList,String cityName,String specialLineName) throws Exception{
        if(!majorCityList.contains(cityName)){
            PageData queryPd = new PageData();
            queryPd.put("CITY_NAME",cityName);
            queryPd.put("SUPERIOR_SPECIAL_LINE",specialLineName);
            PageData spd = (PageData)dao.findForObject("SpecialLineMapper.findBySuperiorSpecialLineAndCityName", queryPd);
            cityName = spd.getString("PARENT_CITY");
        }
        return cityName;
    }

    /**
     * 修改起点到终点之间所有的值
     *
     * @param majorList
     * @param specialLineArray
     * @param startCity
     * @param endCity
     */
    private void changeArray(List<PageData> majorList, int[] specialLineArray, String startCity, String endCity, int num) {
        int sIndex = getIndex(majorList, startCity);
        int eIndex = getIndex(majorList, endCity);
        if (majorList != null && specialLineArray != null) {
            if (sIndex < eIndex) {
                for (int i = sIndex; i < eIndex; i++) {
                    specialLineArray[i] += num;
                }
            }
        }
    }

    /**
     * 获取专线节点位置
     *
     * @param majorList
     * @param cityName
     * @return
     */
    private int getIndex(List<PageData> majorList, String cityName) {
        if (majorList != null) {
            for (int i = 0; i < majorList.size(); i++) {
                if (majorList.get(i).getString("CITY_NAME").equals(cityName)) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * 获取节点之间最少箱子数量
     *
     * @param specialLineArray
     * @param startCity
     * @param endCity
     * @return
     */
    private int minContaineNum(List<PageData> majorList, int[] specialLineArray, String startCity, String endCity) {
        int sIndex = getIndex(majorList, startCity);
        int eIndex = getIndex(majorList, endCity);
        int min = specialLineArray[sIndex];
        if (sIndex < eIndex) {
            for (int i = sIndex + 1; i < eIndex; i++) {
                if (min > specialLineArray[i])
                    min = specialLineArray[i];
            }
        }
        return min;
    }

    /**
     * 减去囊括此班次相关班次的箱子数量
     *
     * @param num         倘若为负数就是减箱,正数为加箱
     * @param classListId
     * @throws Exception
     */
    public void changeContaineNum(String classListId, int num) throws Exception {
        //修改包含班次箱子的数量
        PageData pd = new PageData();
        //pd.put("CLASSES_LIST_ID","885d9c5644b547b2874b49cc9acdc4f3");
        pd.put("CLASSES_LIST_ID", classListId);
        //通过班次id获取该班次具体信息
        //classes_listService.findById(pd);
        pd = (PageData) dao.findForObject("Classes_listMapper.findById", pd);
        //获取班次起点和终点
        String startCity = pd.getString("START_CITY");
        String endCity = pd.getString("END_CITY");
        String specialLineName = pd.getString("SPECIAL_LINE_NAME");
        String trainNumberId = pd.getString("TRAIN_NUMBER_ID");

        //通过专线名获取该干线上的所有点
        List<PageData> majorList = (List<PageData>) dao.findForList("SpecialLineMapper.findByName", pd);
        //通过专线名和车次id获取这个车次上所有的班次
        //List<PageData> allClassesList = (List<PageData>) dao.findForList("Classes_listMapper.findClassesByTrainNumber", pd);
        //存储干线上所有的点
        List<String> majorCityList = new ArrayList<>();
        //存储干线上起点之前的点
        List<String> preCityList = new ArrayList<>();
        //存储干线上终点之后的点
        List<String> nextCityList = new ArrayList<>();


        //将主线上的点都存入majorCityList
        for (int i = 0; i < majorList.size(); i++) {
            String majorCity = majorList.get(i).getString("CITY_NAME");
            majorCityList.add(majorCity);
        }

        int i = 0;
        //获取该班次起点之前(包括起点)的所有地点
        for (; i < majorCityList.size(); i++) {
            String preCityName = majorCityList.get(i);
            if (preCityName.equals(startCity)) {
                preCityList.add(preCityName);
                break;
            }
        }

        for (; i < majorCityList.size(); i++) {
            if (majorCityList.get(i).equals(endCity)) {
                break;
            }
        }
        //获取该班次终点以后(包括终点)的所有地点
        for (; i < majorCityList.size(); i++) {
            nextCityList.add(majorCityList.get(i));
        }

        //将所有专线放入数组中,并查出改点的箱子数
        int[] specialLineArray = new int[majorCityList.size() - 1];
        //查询各个最短节点的箱子的数量,
        for (int k = 0; k < majorCityList.size() - 1; k++) {
            String sCity = majorCityList.get(k);
            String eCity = majorCityList.get(k + 1);
            PageData queryPd = new PageData();
            queryPd.put("START_CITY", sCity);
            queryPd.put("END_CITY", eCity);
            queryPd.put("SPECIAL_LINE_NAME", specialLineName);
            queryPd.put("TRAIN_NUMBER_ID", trainNumberId);
            //List<PageData> classesList = classes_listService.classesListByConditionMiddle(queryPd);
            PageData classesList = (PageData) dao.findForObject("Classes_listMapper.findSingleClasses", queryPd);
            if (!classesList.isEmpty()) {
                int containeNum = (Integer) classesList.get("CONTAINE_NUM");
                //map.put(sCity, containeNum);
                specialLineArray[k] = containeNum;
            } else {
                //当这个班次停运,那么会出现缺省,用一个远大于车次能装载的箱子数代替,由于每次都是取最小的箱子数,所以不会影响结果
                specialLineArray[k] = 10000;
            }
        }

        //判断当前班次的起始点是否是干线上的点,如果不是就替换为主线上的点
        startCity = getParentCity(majorCityList,startCity,specialLineName);
        endCity = getParentCity(majorCityList,endCity,specialLineName);

        //修改后台传递过来的班次的箱子数量
        //修改当前班次影响的专线点的数量
        changeArray(majorList, specialLineArray, startCity, endCity, num);



        //修改与当前班次相关的班次的箱子的数量
        for (int j = 0; j < majorCityList.size() - 1; j++) {
            String startCityName = majorCityList.get(j);
            for (int k = j + 1; k < majorCityList.size(); k++) {
                String endCityName = majorCityList.get(k);
                if ((preCityList.contains(startCityName)) && preCityList.contains(endCityName) || (nextCityList.contains(startCityName) && nextCityList.contains(endCityName))) {
                    continue;
                }
                int currntNumber = minContaineNum(majorList, specialLineArray, startCityName, endCityName);
                changeClassesContaineNum(startCityName,endCityName,pd,currntNumber);
            }
        }
    }
}

扩展情况

如过是这种又该怎么搞,采用图来做吗?

更近一步,还可能是这样的。

依次类推更加复杂的情况。

总结与思考

这个项目收获最大的就是这个减箱子接口的开发,通过这个接口的开发我发现自己刚开始写的代码很烂,同时逻辑经常考虑得不够周到,总是边测边改,很影响效率。思考了很久,发现还是得画流程图,通过图思考,能更好的发现逻辑漏洞!!

代码整体逻辑写完后,当然也不要忘记抽取重复的代码封装成方法,这点可以看看jdk的一些源码,慢慢的就会耳濡目染,知道该怎么封装方法了。当然最好是去看《重构-改善既有代码设计》进行系统的学习,这个书我还没有看,但是已经在计划中了!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值