背景:
實際工作中有很多需要樹狀結構來表示某些數據關係,比如省市區,商品的幾級類目,組織架構等。
繼承關係驅動的設計
比較常規的設計是使用一個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