1. 程式人生 > >專案開發感悟——演算法篇

專案開發感悟——演算法篇

最近在做一個物流的專案,自己負責線路配置這個模組還有部分介面的開發。

線路配置裡面有三個模組:專線列表、班次模板、班次列表。

專線列表主要是記錄區域車次停靠點,班次模板主要是用於批量生產班次,班次列表是實際業務邏輯要用到的資料,也是呈現給使用者看的資料。

業務流程:

     後臺方面:

在專線列表裡面新增區域點,通過區域點,可以一鍵生成班次模板,通過班次模板可以一鍵生成班次列表;

譬如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的一些原始碼,慢慢的就會耳濡目染,知道該怎麼封裝方法了。當然最好是去看《重構-改善既有程式碼設計》進行系統的學習,這個書我還沒有看,但是已經在計劃中了!!