淘宝SKU组合查询算法实现

前端有多少事情可以做,能做到多好。一直在关注各大公司UED方面的知识,他们也代表了前端的力量,而且也很乐意和大家分享,把应用到项目的知识归类整理,再写成博客搬到网上来,充实这前端的内容,也是为想追寻和学习的人提供了场所,为想接触到一些前沿的知识提供了去处,感谢有这么一群人。大的科技公司基本都有自己的前端部门或团队,在网上也能看到他们的动态,像淘宝、阿里巴巴、腾讯、百度等等。

前段时间在淘宝UED官网上看到一篇SKU组合查询算法探索,当时看过以后只是感觉挺牛的,而且讲的很具体,实现步骤和代码都想说的很详细,几种算法以及算法的复杂度都很有深入的分析,挺佩服这种专研精神的,当时只是隐约的感觉到这个算法在解决电商的商品拆分属性选择中可能会用到,但是具体的实现细节也没进行尝试。

后来公司正好要做一个项目,而且用的就是淘宝商品数据结构,商品详情页是属性选择也和淘宝的很类似,当时就想到了那篇文章,于是有反复看来两三遍,试了一下上面说的第二种算法(已经给出了源码),实现起来也不麻烦,虽然例子中给出的第二种算法得到的结果只有商品数量,但是经过修改也可以得到商品的价格,本打算这样就可以直接用的项目中好了。但是在看到第二种算法的优化后(没有提供源码),就想按照这种方式来实现,也是最初萌发出来的想法一致。

第二种算法会有大量的组合,它是基于原始属性值的结果组合和递归,而不是基于结果集的。其实第二种算法的优化,基于结果集的算法实现起来也不麻烦,原理就是把结果集的SKU中key值进行更小拆分组合,把拆分和组合后的结果信息放到SKUResult里面,这样在初始化一次完成,后面的选择可以根据这个结果集使用。把组合范围减少到key里面,这样能够搜索范围避免递归,而且得到的每个小的组合属性值的结果有用信息很丰富,数量和价格都包括其中。

但是又过了一段时间以后,项目被搁浅了,也不知道以后能用上不能了,写的示例也搁了许久,再不拿出来晾晾估计都该长毛变味了。

示例如下

测试地址: http://jsfiddle.net/tianshaojie/aGggS/

下载地址:http://files.cnblogs.com/purediy/sku-20140802.rar

主要JS代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376

var startTime = new Date().getTime();
//属性集
var keys = [
        ['10'],
        ['20','21','22','23','24'],
        ['30','31','32','33','34','35','36','37','38'],
        ['40']
        ];
          
//后台读取结果集
var data = {
    "10;24;31;40": {
        price:366,
        count:46
    },
    "10;24;32;40": {
        price:406,
        count:66
    },
    "10;24;33;40": {
        price:416,
        count:77
    },
    "10;24;34;40": {
        price:456,
        count:9
    },
    "10;24;35;40": {
        price:371,
        count:33
    },
    "10;24;36;40": {
        price:411,
        count:79
    },
    "10;24;37;40": {
        price:421,
        count:87
    },
    "10;24;38;40": {
        price:461,
        count:9
    },
    "10;24;30;40": {
        price:356,
        count:59
    },
    "10;23;31;40": {
        price:366,
        count:50
    },
    "10;23;32;40": {
        price:406,
        count:9
    },
    "10;23;33;40": {
        price:416,
        count:90
    },
    "10;23;34;40": {
        price:456,
        count:10
    },
    "10;23;35;40": {
        price:371,
        count:79
    },
    "10;23;36;40": {
        price:411,
        count:90
    },
    "10;23;37;40": {
        price:421,
        count:10
    },
    "10;23;38;40": {
        price:461,
        count:9
    },
    "10;23;30;40": {
        price:356,
        count:46
    },
    "10;22;31;40": {
        price:356,
        count:27
    },
    "10;22;32;40": {
        price:396,
        count:38
    },
    "10;22;33;40": {
        price:406,
        count:42
    },
    "10;22;34;40": {
        price:446,
        count:50
    },
    "10;22;35;40": {
        price:361,
        count:25
    },
    "10;22;36;40": {
        price:401,
        count:40
    },
    "10;22;37;40": {
        price:411,
        count:43
    },
    "10;22;38;40": {
        price:451,
        count:42
    },
    "10;21;31;40": {
        price:366,
        count:79
    },
    "10;21;32;40": {
        price:406,
        count:79
    },
    "10;21;33;40": {
        price:416,
        count:10
    },
    "10;21;34;40": {
        price:456,
        count:10
    },
    "10;21;35;40": {
        price:371,
        count:87
    },
    "10;21;36;40": {
        price:411,
        count:10
    },
    "10;21;37;40": {
        price:421,
        count:10
    },
    "10;21;38;40": {
        price:461,
        count:80
    },
    "10;21;30;40": {
        price:356,
        count:43
    },
    "10;20;31;40": {
        price:356,
        count:46
    },
    "10;20;32;40": {
        price:396,
        count:49
    },
    "10;20;33;40": {
        price:406,
        count:65
    },
    "10;20;34;40": {
        price:446,
        count:10
    },
    "10;20;35;40": {
        price:361,
        count:34
    },
    "10;20;36;40": {
        price:401,
        count:41
    },
    "10;20;37;40": {
        price:411,
        count:36
    },
    "10;20;38;40": {
        price:451,
        count:42
    },
    "10;20;30;40": {
        price:346,
        count: 3
    }
}
//保存最后的组合结果信息
var SKUResult = {};
//获得对象的key
function getObjKeys(obj) {
    if (obj !== Object(obj)) throw new TypeError('Invalid object');
    var keys = [];
    for (var key in obj)
        if (Object.prototype.hasOwnProperty.call(obj, key))
            keys[keys.length] = key;
    return keys;
}
 
//把组合的key放入结果集SKUResult
function add2SKUResult(combArrItem, sku) {
    var key = combArrItem.join(";");
    if(SKUResult[key]) {//SKU信息key属性·
        SKUResult[key].count += sku.count;
        SKUResult[key].prices.push(sku.price);
    else {
        SKUResult[key] = {
            count : sku.count,
            prices : [sku.price]
        };
    }
}
 
//初始化得到结果集
function initSKU() {
    var i, j, skuKeys = getObjKeys(data);
    for(i = 0; i < skuKeys.length; i++) {
        var skuKey = skuKeys[i];//一条SKU信息key
        var sku = data[skuKey]; //一条SKU信息value
        var skuKeyAttrs = skuKey.split(";"); //SKU信息key属性值数组
        skuKeyAttrs.sort(function(value1, value2) {
            return parseInt(value1) - parseInt(value2);
        });
 
        //对每个SKU信息key属性值进行拆分组合
        var combArr = combInArray(skuKeyAttrs);
        for(j = 0; j < combArr.length; j++) {
            add2SKUResult(combArr[j], sku);
        }
 
        //结果集接放入SKUResult
        SKUResult[skuKeyAttrs.join(";")] = {
            count:sku.count,
            prices:[sku.price]
        }
    }
}
 
/**
 * 从数组中生成指定长度的组合
 */
function arrayCombine(targetArr) {
    if(!targetArr || !targetArr.length) {
        return [];
    }
 
    var len = targetArr.length;
    var resultArrs = [];
 
    // 所有组合
    for(var n = 1; n < len; n++) {
        var flagArrs = getFlagArrs(len, n);
        while(flagArrs.length) {
            var flagArr = flagArrs.shift();
            var combArr = [];
            for(var i = 0; i < len; i++) {
                flagArr[i] && combArr.push(targetArr[i]);
            }
            resultArrs.push(combArr);
        }
    }
     
    return resultArrs;
}
 
 
/**
 * 获得从m中取n的所有组合
 */
function getFlagArrs(m, n) {
    if(!n || n < 1) {
        return [];
    }
 
    var resultArrs = [],
        flagArr = [],
        isEnd = false,
        i, j, leftCnt;
 
    for (i = 0; i < m; i++) {
        flagArr[i] = i < n ? 1 : 0;
    }
 
    resultArrs.push(flagArr.concat());
 
    while (!isEnd) {
        leftCnt = 0;
        for (i = 0; i < m - 1; i++) {
            if (flagArr[i] == 1 && flagArr[i+1] == 0) {
                for(j = 0; j < i; j++) {
                    flagArr[j] = j < leftCnt ? 1 : 0;
                }
                flagArr[i] = 0;
                flagArr[i+1] = 1;
                var aTmp = flagArr.concat();
                resultArrs.push(aTmp);
                if(aTmp.slice(-n).join("").indexOf('0') == -1) {
                    isEnd = true;
                }
                break;
            }
            flagArr[i] == 1 && leftCnt++;
        }
    }
    return resultArrs;
}
 
 
//初始化用户选择事件
$(function() {
    initSKU();
    var endTime = new Date().getTime();
    $('#init_time').text('init sku time: ' + (endTime - startTime) + " ms");
    $('.sku').each(function() {
        var self = $(this);
        var attr_id = self.attr('attr_id');
        if(!SKUResult[attr_id]) {
            self.attr('disabled''disabled');
        }
    }).click(function() {
        var self = $(this);
 
        //选中自己,兄弟节点取消选中
        self.toggleClass('bh-sku-selected').siblings().removeClass('bh-sku-selected');
         
        //已经选择的节点
        var selectedObjs = $('.bh-sku-selected');
 
        if(selectedObjs.length) {
            //获得组合key价格
            var selectedIds = [];
            selectedObjs.each(function() {
                selectedIds.push($(this).attr('attr_id'));
            });
            selectedIds.sort(function(value1, value2) {
                return parseInt(value1) - parseInt(value2);
            });
            var len = selectedIds.length;
            var prices = SKUResult[selectedIds.join(';')].prices;
            var maxPrice = Math.max.apply(Math, prices);
            var minPrice = Math.min.apply(Math, prices);
            $('#price').text(maxPrice > minPrice ? minPrice + "-" + maxPrice : maxPrice);
             
            //用已选中的节点验证待测试节点 underTestObjs
            $(".sku").not(selectedObjs).not(self).each(function() {
                var siblingsSelectedObj = $(this).siblings('.bh-sku-selected');
                var testAttrIds = [];//从选中节点中去掉选中的兄弟节点
                if(siblingsSelectedObj.length) {
                    var siblingsSelectedObjId = siblingsSelectedObj.attr('attr_id');
                    for(var i = 0; i < len; i++) {
                        (selectedIds[i] != siblingsSelectedObjId) && testAttrIds.push(selectedIds[i]);
                    }
                else {
                    testAttrIds = selectedIds.concat();
                }
                testAttrIds = testAttrIds.concat($(this).attr('attr_id'));
                testAttrIds.sort(function(value1, value2) {
                    return parseInt(value1) - parseInt(value2);
                });
                if(!SKUResult[testAttrIds.join(';')]) {
                    $(this).attr('disabled''disabled').removeClass('bh-sku-selected');
                else {
                    $(this).removeAttr('disabled');
                }
            });
        else {
            //设置默认价格
            $('#price').text('--');
            //设置属性状态
            $('.sku').each(function() {
                SKUResult[$(this).attr('attr_id')] ? $(this).removeAttr('disabled') : $(this).attr('disabled''disabled').removeClass('bh-sku-selected');
            })
        }
    });
});

收获

JavaScript中的对象属性访问是最快的了

在前端领域,很少会遇到算法问题,这不能说不是一种遗憾。不过,随着前端处理的任务越来越复杂和重要,偶尔,也能遇到一些算法上的问题。本文,所要讨论的,就是这样一样问题。

什么是SKU

问题来自垂直导购线周会的一次讨论,sku组合查询,这个题目比较俗,是我自己取得。首先,看下什么是sku,来自维基百科的解释:

最小存货单位(Stock Keeping Unit)在连锁零售门店中有时称单品为一个SKU,定义为保存库存控制的最小可用单位,例如纺织品中一个SKU通常表示规格、颜色、款式。

让我们假设在淘宝上,有这么一个手机,如下表格所示:

颜色 容量 保修期限 屏幕大小 电池容量
红色 4G 1 month 3.7 1500mAh
白色 8G 3 month 4 1900mAh
黑色 16G 6 month 4.3 2100mAh
黄色 64G 1 year   2500mAh
蓝色 128G      

sku: 白色 + 16G + 3 month + 3.7 + 2100mAh就这么一款可以提供5种颜色,5种容量,4种保修期限, 3种屏幕尺寸,4种电池容量的手机,我们假设它存在,叫xphone。表格中,加粗的5种属性,组合在一起,构成一个sku。现在,应该清楚什么是sku了吧。可以把xphone的规格参数看成一个JS的构造器,每一个sku,对xphone函数进行实例化,返回的一个对象就是一个sku。不过,这和一部手机的概念有一些区别,一个sku对应多个手机,sku是描述手机的最小单位,比如说学校,在学校里面最小教学单位是班级,那么一个班级可以看做一个sku。

问题描述

下面,为了描述问题,我首先假设一个产品属性组合2×2,用[[a, A], [b, B]]表示,那么,sku组合为[ab, Ab, Ab, AB],也是2×2,4个sku。现在我们知道sku对应的数目和价格,依然用js对象来描述为:

  {
    ab: {amount: 10, price: 20}
    aB: {amount: 10, price: 30}
    AB: {amount: 10, price: 40}
  }

这个的数据说明了,Ab是没有货存的,ab, aB, AB分别有10个货源在。那么,当用户对商品进行选择的时候,如果首先选择A,那么,b应该显示为不可选择状态,因为Ab是没有货的。同样,如果选择了b,那么A应为灰掉,因为Ab还是没有值的。可能的几种状态如下:

初始状态
属性1:  
属性2:  

   
1. 选中b,A禁止
属性1:  
属性2:  

   
2. 选中A,b禁止
属性1:  
属性2:  

   
3. 选中AB,价格为40
属性1:  
属性2:  

问题:用户选择某个属性后,如何判断哪些属性是可以被选择的。当sku属性只是2×2的时候,还是很容易计算的。但是,如果情况变得复杂,比如4x4x4x5这样的情况,要判断用户的那些行为是可行的,还是会复杂很多的。下面看算法实现吧,还是用2×2这种最简单的形式作为参考。为了方便描述,下面使用result = {ab: ...}表示sku对应的价格和数目的数据对象,使用item表示一个sku属性下的一个元素,items = [[a, A], [b, B]]表示所有sku属性元素。

算法演示

首先来一个演示吧,仅支持高级浏览器。对于第一算法,使用正则匹配,不是很完善,有些不准,仅仅是演示,正则表达式写的不好,不用在意。

下面灰色按钮表示不可选,白色表示可选,红色为选中状态。演示框最下面是可用的sku组合。

 

第一种算法[正则]:

共进行300次运算,耗时3ms
属性1:
属性2:
属性3:
属性4:
属性5:
属性6:
属性7:

第一种算法优化方式[除法]:

共进行349次运算,耗时0ms. result乘积最大为75724742108887
属性1:
属性2:
属性3:
属性4:
属性5:
属性6:
属性7:

可选择的路线: 
19:29:59:107:151:191:239

 
7:53:73:109:149:179:227

 
3:43:83:103:137:197:241

 
5:41:67:97:157:181:229

 
13:47:79:113:173:199:251

 
11:37:61:127:167:223:263

 
7:41:71:113:163:179:263

 
5:31:67:127:157:193:229

 
11:29:83:101:139:211:239

 
19:47:59:97:167:197:233

 
2:43:89:103:149:199:241

 
3:23:89:109:163:191:239

 
17:37:61:97:139:179:233

 
7:43:67:103:137:193:227

 
13:53:73:107:167:197:241

 
11:41:71:131:157:211:257

 
5:31:83:113:173:199:263

 
19:47:79:127:151:223:251

 
3:31:89:97:151:197:229

 
2:43:59:101:167:193:227

 
13:41:73:107:173:199:233

 
11:43:61:109:157:193:229

 
5:37:73:113:137:199:241

 
17:23:79:97:151:181:251

 
7:31:67:101:149:191:239

 
3:41:59:107:167:197:263

第一种算法

初始条件已知所有sku属性的数组items和sku所对应的价格信息result用户选择了item B,使用数组selected=['B']表示,selected可以为空数组算法过程1. 循环所有sku属性forEach(result, (curitems, attr)->),使curitems等于属性对应的所有元素,attr等于属性id。2. 克隆数据attrSelected = selected3. 判断属性attr中是否有元素在数组attrSelected中,如果存在,从attrSelected去掉存在的元素4. 循环属性下的元素forEach(curitems, (item)->,使得item等于单个属性的值5. 把 attrSelecteditem组合成sku6. 循环result,判断第五组成的sku在result中是否存在,如果存在,退出循环4,返回true,进入步骤87. 当前item设置为灰色,标志不可选择8. 当前item为可选属性元素9. 循环4和循环1完成,所有item状态标注完成,算法结束

这个方式是最普通的算法实现了,非常直接,一个一个判断所有的item是否可以被选中,判断依据是itemselected的元素组合的sku是否在result数组中存在。在我们上面的例子中,在初始化的情况下,用户没有选中任何元素,那么循环过程,只需要判断a, b, A, Bselected是否存在。如果,用户选中了b,那么循环过程中,依次判断的sku组合是ab, Ab, B,存在的sku组合是ab, aB, AB,所以因为Ab组合没有能知道,所以,A需要标注为不可点。组合sku判断的时候,需要注意的是,因为B和选中的b在同一个属性中,所以组合的时候,需要去掉b,然后组合成B,这是第3步所主要完成的过程。

这样的算法,很简单,但很繁琐,循环嵌套循环,可以简单分析一下算法复杂度。如果sku属性组合元素的总和数用m表示,结果数据长度为n,那么每次选择后,需要的算法大致步骤是m * n。这似乎不是很复杂,m * n而已,不过,每次判断一个sku组合是否和result中的组合匹配,却不是一个简单的过程,实际上,这可以看做是一个字符串匹配的一个算法了,最简单的还是使用正则匹配,m * n次正则匹配,这样就不怎么快了吧。正则表达式很不稳定,万一sku组合中有一些特殊字符,就可能导致一个正则匹配没能匹配到我们想要的表达式。

第一种算法的优化

经过讨论,第一种算法,有了优化的算法思路。 就第一种算法而言,正则匹配不够优雅,而且比较慢,而我们想要做的事情是比较一个组合是否包含于另外一个组合,用数学语言来描述,就是一个集合是否是另一个集合的子集,怎么来做这样的快速判断呢。

现在问题可以简化为:假设一个集合A{a, b, c}和另外一个集合B{a, e},如何快速判断B是否是A的子集。这个问题比较简单的方法是用B中所有元素依次和A中的元素进行比较,还是简单而粗暴的方式,比正则稍微快一些。对于集合中的元素,它们都以唯一的,通过这样的特性,我们可以把所有字母转换为一个质数,那么 集合A可以表示为集合元素(质数)的积,B同样, B是否是A的子集,这个只需要将B除以A,看看是否可以整除 ,如果可以那么说明,B是A的子集。

现在处理字符串就转换为处理乘法算法了,有了以上的分析,我们可以整理下算法过程:

数据预处理,生成一组随机数,把所有item一一对应一个质数,把item组合转换为一几个 质数的积
根据用户已经选择的item进行扫描所有的item,如果item已经被选中,则退出,如果没有, 则和所有已经选择的item进行相乘(特别注意,以选中的item需要去掉和当前匹配的item 在同一个类目中的item,因为一个组合不可能出现两个类目相同的item) ,这个乘机就是 上文中的集合B
把集合B依次和sku组合构成的积(相当于上文中的集合A)进行相除,比较,如果整除,则 退出,当前匹配的sku可以被选中,如果一直到最好还没有匹配上,则不能被整除。

这样优化了一下看起来比较简单的思路,但是实现起来却一点都不容易,代码在这里。算法也算简化了不少,不过这个预处理过程还是比较麻烦的,而且实际上,和第一种方案的解决的算法复杂度差不多,只是比较的时候使用的是乘除法,而第一种是正则匹配罢了。

第二种算法

后来又过了一周,这个问题被当成一个方案来继续讨论了。大家此时差不多都无话可说了,算法都有实现了,似乎没有什么其他可说的了。就在这个问题就如此结束的时候,正豪站出来了,说不管是第一种还是第一种方案的优化方案,每次用户进行选择,都需要重复计算一遍,这样实在太麻烦了。每次都对所有spu进行扫描,这样不是很好,能不能有其他的方式呢,能否更加直接判断出一个sku是否可以被选择呢。前面的算法,一个sku是否可以被选择,需要依次循环sku 组合的所有元素才可以判断的,这样的过程一定需要吗?

第三种算法就这样诞生了,考虑到JavaScript中的对象属性访问是最快的了,那么对于如果能够直接从一个对象中读取到以选择的sku和需要匹配的sku组合对应的数目,那这样的算法简直就是不用时间啊。下面来详细描述。

下面把问题初始条件假设如下:

初始状态,选中A1
属性1:    
属性2:   
属性3:   

假如已经选中item为A1,那么现在要计算B1是否可以被选择,那么如果我们能够直接获取到A1和B1组合的所有商品数目,那么就能知道B1是否可以被选择了。A1和B1的组合是这样计算的,在上面描述的问题空间中,A1和B1的组合,可能有以下几种: A1+B1+C1, A1+B1+C2,A1+B1+C3。这些组合就可以直接从已知的sku组合中获取信息啦,同样是对象属性查找,快得不得了。示例如下:

A1选中状态下,判断B1是否可用,只需要查找A1 B1
  =     +     +   

A1+B1+C1这样的组合,结果可以可以直接从result中获得数据结果。

实际上, 对于任何一个sku和其他sku的组合都是可以通过同样的方式递归查找来实现获取其组合后的商品数目。这样的算法最大的优势是,计算过程是可以缓存的,比如计算A1是否可以被选中,那么肯定需要计算除A1+B1组合的数目,A1的数目是由A1+B1,A1+B2,A1+B3三个子集构成,这三个子集又可以拆分为更细的组合,然后这些所有的组合对应的商品数目都可以获取到了,下次需要判断A1+B2组合,则无需重复计算了。此外,我们可以清晰的获取组合相关的信息,比如某个sku下面可以有的商品数目。

算法实现这里jsfiddle

复杂度分析

第二种算法思路非常有趣,使用动态规划法,将原问题分解为相似的子问题,在求解的过程中通过子问题的解求出原问题的解。而且,最终判断一个item是否可以被选择,直接从对象中查找,属于字典查找算法了,应该是很快。但是,乍一看,还是有些问题,递归查找,数据贮存在变量中,这些都是通过空间来换取时间的做法,递归会堆栈溢出吗?查找次数到底多少?

第一个种算法的复杂度还是很容易计算的,首先假设一个n * n的矩阵构成sku属性,比如10×10表示,有10个属性,每个属性有10个元素。假设可选择的result长度是m,那么,第一种算法的复杂度大概是 n * n * m,这样的算法还是很快的。只是,如果每一个步骤,都使用正则表达式匹配,根据上面的演示,确实会有一些些慢,不过正则表达式的是模糊匹配,可能不是那么稳定。不过除法方式判断需要生成足够的质数,当几个数的乘积太大的时候,可能导致计算机无法运算,所有,使用第1种算法的优化算法,也是有一定限制的。js里面,能够处理的最大数字大概是19位,这个范围内可以操作的范围还是比较大的,这个就不做推算了。此外,通用可以引入负数,这样就可以把质数的范围增大一倍,计算量也小一些,可以处理更大的输入规模了。

第二种算法复杂度,同样对于n * n的数据输入,从第一排算起,第一排第一个A1,组合为A1 + B1, A1 + B2 …函数递归进入第二层,第二层从第一个B1开始,组合为A1 + B1+ C1, A1 + B1 + C2 …进入第三层,以此类推,函数每增加一层,需要的计算量是上一层的n倍,总数是 n + n2 + n3 + … + nn,这个数目是非常庞大了,算法复杂度用nn来描述了,如果是10×10的sku属性组合,初始化需要100亿次计算,有些吓人了,这还需要一个同样庞大的内存数组。

第二种算法的优化

经过上面的算法分析,似乎第二种算法是错误的,无法执行。不过,仔细想想,第二种方法第一初始化的时候算法复杂度非常高,几乎是浏览器无法承受的。但是,一旦数据初始化完成,后面的过程就非常简单了,同样对于n * n规模的输入,每次用户选择,这个时候,需要进行的操作是把所有数据遍历一遍,然后直接查询是否存可以被选中。算法复杂度是n * n。比起上面第一种算法的优化算法要快,现在主要的问题是,初始化如果使用自上而下,不断拆分问题,这样运算复杂度指数级增加,不过,算法本身是可行的,数据初始化过程,还是需要进一步优化。

第二种算法,把问题一层一层拆分,查找过程分解太过于琐碎,有很多的组合,是完全不可能存在的,算法非常浪费。如果,直接从获得的result数组中读取数据组合,只需要把result循环一遍,所有可能的组合就都可以计算出来了。举个例子,从最上面的2×2的result中,我们知道result对象

    ab: {amount: 10, price: 20}
    aB: {amount: 10, price: 30}
    AB: {amount: 10, price: 40}

计算过程,循环result

第一次分解ab,a = 10, ab = 10, b = 10
第二次分解aB, a = a + 10 = 20, aB = 10, B = 10
第三次分解AB, A = 10, AB = 10, B = B + 10 = 20

三次循环,得到一个新的数据结构var map = {a: 20, ab: 10, b: 10, aB: 10, AB: 10, A: 10, B: 10}通过这个对象,就可以判断任何情况了。比如,初始化的时候,需要查找a, b, c,d,直接查找map对象中是否存在a, b, c, d。如果选择了a,那么需要判断aB, ab,统一直接查找的方式。

经过这样的优化,初始化的时候计算量也不大,这样第二种算法的实现就可以很好的完成任务了。可能这个map对象,可能还是会有点大。

结论

总的来说,比较好的方式是第一种算法的优化(也就是除法判断)和第二种算法。各自有其特点,都有其特色之处,除法判断把 字符串匹配转换为数字运算 ,第二种算法使用 字典查找 。并且都能 快速准确 的计算出结果。

从算法速度来说,第一种算法复杂度是n * n * m,当然需要一个比较繁琐负责的质数对应转换过程,第二种算法复杂度是 n * n,其初始化过程比较复杂,最初的方式是nn,经过优化,可以提高到n!,n的阶乘。从理论上而言,nn或者n!都是不可用的算法了,就实际情况而言,sku组合大多在,6×6以下,第二种算法还是非常快的。

从算法本身而言,第二种算法想法非常奇妙,容易理解,实现代码优雅。只是初始化比较慢,在初始化可以接受的情况下,还是非常推荐的,比如淘宝线上的sku判断。此外,第二种算法获得的结果比起第一种更具有价值,第二种方式直接取得组合对应的数目,价格信息,而第一种只是判断是否可以组合,从实际应用角度而言,第二种方式还是剩下不少事的。

感觉只要善于去发现,还能能够找到一些有意思的解决问题思路的。

 
 

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注