樹狀結構存儲和快速匹配

背景:

實際工作中有很多需要樹狀結構來表示某些數據關係,比如省市區,商品的幾級類目,組織架構等。

繼承關係驅動的設計

比較常規的設計是使用一個parent 字段來表示繼承關係,構建二維關係表。

這個方案的優點是:直觀簡單,非常容易理解,數據維護上成本也較低。但是缺點同樣明顯:查詢的效率太差,比如我要在代碼中構造出Food 這棵,需要先便利parent_id爲1 的數據,再根據返回的數據繼續便利下一級數據,有多少級就要查詢多少下。

左右值編碼的設計

就是每條記錄都有一個左值和右值,左右值是通過下面這套算法來計算的

這樣的設計帶來的好處就是對查詢非常友好,比如我要查詢Fruit 下所有的數據,只需要查詢左值>2且右值<11的數據,一次查詢就可以搞定。同樣反過來查也是一樣的,比如查詢Cherry 所有的父級,只要查詢左值<4且右值>5的數據,也是一次搞定。當然缺點也非常明顯就是每次數據的插入和刪除,成本都非常高,很可能涉及大部分數據的變更,因此適合偶爾變動的數據。因爲這裏重點不是講解數據庫所以,具體的實現可以參考:https://blog.csdn.net/monkey_d_meng/article/details/6647488 ,https://segmentfault.com/a/1190000000329012

進一步的延伸

日常中的一個需求是判斷某人是否在一顆組織架構樹中或者求2個或者多個供貨區域的交集。這裏用供貨區域來舉例,其他的情況類似。首先可以通過左右值法在內存中將2維的樹狀結構轉換爲1維的左右值數組,接着就是多個區間列表求交集的問題,這個也是經典的Leetcode算法,網上一搜一大堆,類似:

輸入:A = [[0,2],[5,10],[13,23],[24,25]], B = [[1,5],[8,12],[15,24],[25,26]]
輸出:[[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]
注意:輸入和所需的輸出都是區間對象組成的列表,而不是數組或列表。
剩下的問題就是如果將樹狀結構轉換成左右值表示了,假設這裏用省市區舉例,這裏有個前提假設每一級的行政區不會超過一個常規值,這裏假設是100,也就是不會有超過100個省,一個省最多不超過100個市,那麼就可以做這樣的設計,00(省)00(市)00(區),每一級佔用2位,這樣的話只要保證每一級是遞增的就可以形成一顆左右樹,類似下面這樣:

接下來就是怎麼在內存中進行省市區的映射

static Map<Integer, AreaX> areaXMap = new ConcurrentHashMap<>();
static AtomicInteger proviceInt = new AtomicInteger(0);
/**
     * 獲取省份
     *
     * @param s
     * @return
     */
    private static AreaX getProvice(String s) {
        Integer proviceId = Integer.valueOf(s);
        return areaXMap.computeIfAbsent(proviceId, id -> {
            String serial = String.valueOf(proviceInt.addAndGet(1));
            Integer left = Integer.valueOf(serial + "0000");
            Integer right = Integer.valueOf(serial + "9999");
            AreaX areaX = new AreaX(serial, left, right, id);
            areaX.setLevel(1);
            return areaX;
        });
    }

首先是省份的計算,這個比較簡單,只要保持遞增就可以了,這裏用了一個map用來緩存和保持遞增。

    static Map<Integer, AtomicInteger> proviceAutoMap = new ConcurrentHashMap<>();

  /**
     * 獲取城市
     *
     * @param cityStr
     * @param proviceStr
     * @return
     */
    private static AreaX getCity(String cityStr, String proviceStr) {
        AreaX provice = getProvice(proviceStr);
        Integer cityId = Integer.valueOf(cityStr);
        return areaXMap.computeIfAbsent(cityId, id -> {
            AtomicInteger proviceAuto = proviceAutoMap
                    .computeIfAbsent(provice.getId(), proviceId -> new AtomicInteger(0));
            String citySerial = provice.getSerial() + getFillTwoNum(proviceAuto.addAndGet(1));
            Integer left = Integer.valueOf(citySerial + "00");
            Integer right = Integer.valueOf(citySerial + "99");
            AreaX areaX = new AreaX(citySerial, left, right, id);
            areaX.setLevel(2);
            return areaX;
        });
    }

市的獲取就稍微麻煩了一點,因爲需要在一個省下面進行遞增。

static Map<Integer, AtomicInteger> cityAutoMap    = new ConcurrentHashMap<>();

private static AreaX getCounty(String[] areaDetailArray, String proviceStr) {
        AreaX city = getCity(areaDetailArray[1], proviceStr);
        Integer countyId = Integer.valueOf(areaDetailArray[2]);
        return areaXMap.computeIfAbsent(countyId, id -> {
            AtomicInteger cityAuto = cityAutoMap.computeIfAbsent(city.getId(), cityId -> new AtomicInteger(0));
            int nextInt = cityAuto.addAndGet(2);
            //這裏需要補位
            String countySerial = city.getSerial() + getFillTwoNum(nextInt);
            Integer left = Integer.valueOf(countySerial) - 1;
            Integer right = Integer.valueOf(countySerial);
            AreaX areaX = new AreaX(countySerial, left, right, id);
            areaX.setLevel(3);
            return areaX;
        });
    }

區的代碼跟市的計算非常類似,不過是序列的。

附上模型的設計

static class AreaX implements Comparable<AreaX> {

        /**
         * 序列
         */
        private String  serial;
        private Integer left;
        private Integer right;

        private Integer id;
        /**
         * 省市區(1,2,3)
         */
        private int     level;

        public AreaX(String serial, Integer left, Integer right, Integer id) {
            this.serial = serial;
            this.left = left;
            this.right = right;
            this.id = id;
        }

        public Integer getLeft() {
            return left;
        }

        public void setLeft(Integer left) {
            this.left = left;
        }

        public Integer getRight() {
            return right;
        }

        public void setRight(Integer right) {
            this.right = right;
        }

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public int getLevel() {
            return level;
        }

        public void setLevel(int level) {
            this.level = level;
        }

        public String getSerial() {
            return serial;
        }

        public void setSerial(String serial) {
            this.serial = serial;
        }

        @Override
        public int compareTo(AreaX obj) {
            if (obj == null || obj.getLeft() == null) {
                return 1;
            }
            return this.getLeft().compareTo(obj.getLeft());
        }
    }

最後就是求交集的方法

 public static List<AreaX> interval(List<AreaX> aList, List<AreaX> bList) {
        List<AreaX> resultList = Lists.newArrayList();
        int i = 0, j = 0;
        while (i < aList.size() && j < bList.size()) {

            AreaX ax = aList.get(i);
            AreaX bx = bList.get(j);
            int left = Math.max(ax.left, bx.left);
            int right = Math.min(ax.right, bx.right);
            //如果命中,就說明a包含b或者b包含a
            if (left <= right) {
                resultList.add(right == ax.right ? ax : bx);
            }
            if (ax.right == right) {
                i++;
            } else {
                j++;
            }
        }
        return resultList;
    }

雖然代碼比較簡單,但是參考了網上大量的實現,還是可以研究一下的(https://blog.csdn.net/qq_17550379/article/details/86774660)。

最後是大家喜聞樂見的性能比拼,用正規的JMH來壓測

Benchmark                  (N)  Mode  Cnt     Score      Error  Units
AreaUtilJmh.testNewUtil   2000  avgt    3   138.938 ±   44.454  ms/op
AreaUtilJmh.testNewUtil   4000  avgt    3   253.600 ±   71.476  ms/op
AreaUtilJmh.testNewUtil   8000  avgt    3   474.319 ±  194.974  ms/op
AreaUtilJmh.testNewUtil  16000  avgt    3   906.224 ±  111.002  ms/op
AreaUtilJmh.testOldUtil   2000  avgt    3   337.919 ±  161.870  ms/op
AreaUtilJmh.testOldUtil   4000  avgt    3   733.070 ± 1666.523  ms/op
AreaUtilJmh.testOldUtil   8000  avgt    3  1279.091 ±   78.746  ms/op
AreaUtilJmh.testOldUtil  16000  avgt    3  2541.113 ±  223.754  ms/op

可以看到新的計算方法比原有的性能提升50%+,還是比較可觀的。

同時我也測試一下另一個場景,就是在確定省市區的情況下判斷當前購貨區域是否可供

Benchmark                   (N)  Mode  Cnt   Score    Error  Units
AreaSupplyJmh.testNewUtil   200  avgt    3   5.124 ±  1.213  ms/op
AreaSupplyJmh.testNewUtil   400  avgt    3  12.238 ± 26.653  ms/op
AreaSupplyJmh.testNewUtil   800  avgt    3  23.606 ±  7.011  ms/op
AreaSupplyJmh.testNewUtil  1600  avgt    3  48.486 ± 35.257  ms/op
AreaSupplyJmh.testOldUtil   200  avgt    3   3.843 ±  0.767  ms/op
AreaSupplyJmh.testOldUtil   400  avgt    3   7.391 ±  3.248  ms/op
AreaSupplyJmh.testOldUtil   800  avgt    3  13.739 ±  2.655  ms/op
AreaSupplyJmh.testOldUtil  1600  avgt    3  27.077 ±  8.167  ms/op

這次的結果差不多反過來了,說明在確定省市區的情況下,單個條件的匹配還是字符串判斷更快。所以針對不同的場景設計不同的算法很重要,不一定設計巧妙的算法就適用任何場景,還是需要通過壓測來驗證。

具體代碼和壓測用例參考:https://github.com/ykdsg/MyJavaProject/tree/master/base/src/main/java/com/hz/yk/area/util

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章