SKU算法探讨(Android版)

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

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

核心类SKU.java



public class Sku {

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

            //获取所有的组合
            ArrayList<ArrayList<String>> 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<ArrayList<String>> combInArray(String[] skuKeyAttrs) {
        if (skuKeyAttrs == null || skuKeyAttrs.length <= 0)
            return null;
        int len = skuKeyAttrs.length;
        ArrayList<ArrayList<String>> aResult = new ArrayList<>();
        for (int n = 1; n < len; n++) {
            ArrayList<Integer[]> aaFlags = getCombFlags(len, n);
            for (int i = 0; i < aaFlags.size(); i++) {
                Integer[] aFlag = aaFlags.get(i);
                ArrayList<String> 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<Integer[]> getCombFlags(int len, int n) {
        if (n <= 0) {
            return new ArrayList<>();
        }
        ArrayList<Integer[]> 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<String, BaseSkuModel> result, ArrayList<String> 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<Integer[]> aaFlags = getCombFlags(len, n);
            for (int i = 0; i < aaFlags.size(); i++) {
                Integer[] aFlag = aaFlags.get(i);
                ArrayList<String> 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<Integer[]> 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项目地址

感谢各位同学耐心看完!

以上

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容