SKU算法探讨(Android版)

两年前看过一篇淘宝UED开源的sku算法(js实现),当时并没有静下心来仔细研究一下,只觉得乍一看好难。这两天无意中github搜了一下,想找找java实现版,还真被我找到了。于是静下心来好好讨教了一番算法的高深。

在一番推敲之后终于看透其真谛,遂决心组织一下语言,记录我所理解的内容。

核心类SKU.java



public class Sku {

    /**
     * 算法入口
     *
     * @param initData 所有库存的hashMap组合
     * @return 拆分所有组合产生的所有情况(生成客户端自己的字典)
     */
    public static Map skuCollection(Map initData) {
        //用户返回数据
        HashMap result = new HashMap<>();
        // 遍历所有库存
        for (String subKey : initData.keySet()) {
            BaseSkuModel skuModel = initData.get(subKey);
            //根据;拆分key的组合
            String[] skuKeyAttrs = subKey.split(";");

            //获取所有的组合
            ArrayList> combArr = combInArray(skuKeyAttrs);

            // 对应所有组合添加到结果集里面
            for (int i = 0; i < combArr.size(); i++) {
                add2SKUResult(result, combArr.get(i), skuModel);
            }

            // 将原始的库存组合也添加进入结果集里面
            String key = TextUtils.join(";", skuKeyAttrs);
            result.put(key, skuModel);
        }
        return result;
    }

    /**
     * 获取所有的组合放到ArrayList里面
     *
     * @param skuKeyAttrs 单个key被; 拆分的数组
     * @return ArrayList
     */
    private static ArrayList> combInArray(String[] skuKeyAttrs) {
        if (skuKeyAttrs == null || skuKeyAttrs.length <= 0)
            return null;
        int len = skuKeyAttrs.length;
        ArrayList> aResult = new ArrayList<>();
        for (int n = 1; n < len; n++) {
            ArrayList aaFlags = getCombFlags(len, n);
            for (int i = 0; i < aaFlags.size(); i++) {
                Integer[] aFlag = aaFlags.get(i);
                ArrayList aComb = new ArrayList<>();
                for (int j = 0; j < aFlag.length; j++) {
                    if (aFlag[j] == 1) {
                        aComb.add(skuKeyAttrs[j]);
                    }
                }
                aResult.add(aComb);
            }
        }
        return aResult;
    }

    
    private static ArrayList getCombFlags(int len, int n) {
        if (n <= 0) {
            return new ArrayList<>();
        }
        ArrayList aResult = new ArrayList<>();
        Integer[] aFlag = new Integer[len];
        boolean bNext = true;
        int iCnt1 = 0;
        //初始化
        for (int i = 0; i < len; i++) {
            aFlag[i] = i < n ? 1 : 0;
        }
        aResult.add(aFlag.clone());
        while (bNext) {
            iCnt1 = 0;
            for (int i = 0; i < len - 1; i++) {
                if (aFlag[i] == 1 && aFlag[i + 1] == 0) {
                    for (int j = 0; j < i; j++) {
                        aFlag[j] = j < iCnt1 ? 1 : 0;
                    }
                    aFlag[i] = 0;
                    aFlag[i + 1] = 1;
                    Integer[] aTmp = aFlag.clone();
                    aResult.add(aTmp);
                    if (!TextUtils.join("", aTmp).substring(len - n).contains("0")) {
                        bNext = false;
                    }
                    break;
                }
                if (aFlag[i] == 1) {
                    iCnt1++;
                }
            }
        }
        return aResult;
    }

    /**
     * 添加到数据集合
     *
     * @param result
     * @param newKeyList
     * @param skuModel
     */
    private static void add2SKUResult(HashMap result, ArrayList newKeyList, BaseSkuModel skuModel) {
        String key = TextUtils.join(";", newKeyList);
        if (result.keySet().contains(key)) {
            result.get(key).setStock(result.get(key).getStock() + skuModel.getStock());
            result.get(key).setPrice(skuModel.getPrice());
        } else {
            result.put(key, new BaseSkuModel(skuModel.getPrice(), skuModel.getStock()));
        }
    }

}


首先说下大体思路

  1. 通过后台获取类似
{
"1;4":{"price":10,"count":10}
"1;5":{"price":10,"count":20}
"1;7":{"price":10,"count":30}
}

这样的数据结构。客户端根据数据自行生成字典放在HashMap里面

  1. 我们要做的就是把上面的map中的key做拆分再组成新的map,以此来得到所有属性组合,以及对应sku对象(自定义一个包含price和count的对象)
    补充:
    假设后台给到属性组合[3,4,2],通过算法我们要得到[[3],[4],[2],[3,4],[3,2],[4,2]],然后把该二阶数组里的每个数组当做key来组成新的map。即add2SKUResult()方法做的事情。

核心方法combInArray(String[] skuKeyAttrs)


该方法做的事情概括下来就是获得在m个数中取n个的所有组合。
听起来很简单?
实则不然。(敲黑板)

下面我们用具体的某个数组来解释整个算法,看起来会更清晰一点。

以属性集合[3,4,2]为例

入参skuKeyAttrs=[3,4,2]

for (int n = 1; n < len; n++) {
            ArrayList aaFlags = getCombFlags(len, n);
            for (int i = 0; i < aaFlags.size(); i++) {
                Integer[] aFlag = aaFlags.get(i);
                ArrayList aComb = new ArrayList<>();
                for (int j = 0; j < aFlag.length; j++) {
                    if (aFlag[j] == 1) {
                        aComb.add(skuKeyAttrs[j]);
                    }
                }
                aResult.add(aComb);
            }
        }
  1. 从下标1开始循环到数组末位,getCombFlags()用来"标记"skuKeyAttrs"n个数的组合结果"。 这句话有点难懂(博主也觉得很难用语言描述 - -),下面详述。
  2. 循环getCombFlags()的结果集--二阶数组aaFlags,循环其中每个数组aFlag,当aFlag中有值=1时,取skuKeyAttrs[当前下标]存入结果集
if (aFlag[j] == 1) {
    aComb.add(skuKeyAttrs[j]);
}

那么我们先来理解getCombFlags()这个方法,理解了这个方法,那么毫不夸张的说整个类也就理解了90%了。

getCombFlags

这里就直接以注释来解释了

ArrayList aResult = new ArrayList<>();
        Integer[] aFlag = new Integer[len];
        boolean bNext = true;
        /*
        * 连续的"1"个数计数器
        * 它的值=连续"1"的个数-1
        * 
        * */
        int iCnt1 = 0;
        //初始化
        /*
        * n=1:
        * aFlag=[1,0,0]
        * n=2:
        * aFlag=[1,1,0]
        * 
        * */
        for (int i = 0; i < len; i++) {
            aFlag[i] = i < n ? 1 : 0;
        }
        aResult.add(aFlag.clone());
        while (bNext) {
            iCnt1 = 0;
            
                /*
                * 当满足i位=1,i+1位=0时需要把i位的1往后移一位并且把i位的值置为0
                * 来获得新的组合方式
                * 
                * */
            for (int i = 0; i < len - 1; i++) {
                if (aFlag[i] == 1 && aFlag[i + 1] == 0) {
                    /*
                    * 分离连续"1"
                    * */
                    for (int j = 0; j < i; j++) {
                        aFlag[j] = j < iCnt1 ? 1 : 0;
                    }
                    
                    //初始化时已经把第一种情况加入aResult
                    /*
                    * 这里相当于把第i位和第i+1位互换
                    * (因为数组值里只有0和1)
                    * 
                    * 以aFlag=[1,0,0]初始值为例
                    * i=0:
                    * aFlag=[0,1,0]
                    * */
                    aFlag[i] = 0;
                    aFlag[i + 1] = 1;
                    
                    Integer[] aTmp = aFlag.clone();
                    aResult.add(aTmp);
                    /*
                    * 去掉前len-n个字符,留下的字符中如果不包含0
                    * 那么说明我们把1都变换到末位了,即取完了n个数的所有组合。
                    * 
                    * */
                    if (!TextUtils.join("", aTmp).substring(len - n).contains("0")) {
                        bNext = false;
                    }
                    break;
                }
                if (aFlag[i] == 1) {
                    iCnt1++;
                }
            }
        }

可能看完注释你还是有点懵。我们再来整理一遍。
当len=3,n=1,我们返回的数组是这样的[[1,0,0],[0,1,0],[0,0,1]]。那么在combInArray中会取到[[3],[4],[2]]
当len=3,n=2,我们返回的数组是这样的[[1,1,0],[1,0,1],[0,1,1]]。那么在combInArray中会取到[[3,4],[3,2],[4,2]]

不难发现getCombFlags()告诉了combInArray用哪些下标来组成最终的集合。

至此,终于理解了getCombFlags()中每一句的目的。相信其他方法都不难理解。本篇也不再过多解释了。
我很想搞明白这样的思路是如何产生的,不得不感叹算法的巧妙,也希望看到本篇的同学不吝赐教。

致谢


感谢 N.Sun同学翻译成java
github项目地址

感谢各位同学耐心看完!

以上

你可能感兴趣的:(SKU算法探讨(Android版))