1. 程式人生 > 實用技巧 >淘寶SKU組合查詢演算法實現

淘寶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 varstartTime =newDate().getTime(); //屬性集 varkeys = [ ['10'], ['20','21','22','23','24'], ['30','31','32','33','34','35','36','37','38'], ['40'] ]; //後臺讀取結果集 vardata = { "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 } } //儲存最後的組合結果資訊 varSKUResult = {}; //獲得物件的key functiongetObjKeys(obj) { if(obj !== Object(obj))thrownewTypeError('Invalid object'); varkeys = []; for(varkeyinobj) if(Object.prototype.hasOwnProperty.call(obj, key)) keys[keys.length] = key; returnkeys; } //把組合的key放入結果集SKUResult functionadd2SKUResult(combArrItem, sku) { varkey = 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] }; } } //初始化得到結果集 functioninitSKU() { vari, j, skuKeys = getObjKeys(data); for(i = 0; i < skuKeys.length; i++) { varskuKey = skuKeys[i];//一條SKU資訊key varsku = data[skuKey];//一條SKU資訊value varskuKeyAttrs = skuKey.split(";");//SKU資訊key屬性值陣列 skuKeyAttrs.sort(function(value1, value2) { returnparseInt(value1) - parseInt(value2); }); //對每個SKU資訊key屬性值進行拆分組合 varcombArr = combInArray(skuKeyAttrs); for(j = 0; j < combArr.length; j++) { add2SKUResult(combArr[j], sku); } //結果集接放入SKUResult SKUResult[skuKeyAttrs.join(";")] = { count:sku.count, prices:[sku.price] } } } /** * 從陣列中生成指定長度的組合 */ functionarrayCombine(targetArr) { if(!targetArr || !targetArr.length) { return[]; } varlen = targetArr.length; varresultArrs = []; // 所有組合 for(varn = 1; n < len; n++) { varflagArrs = getFlagArrs(len, n); while(flagArrs.length) { varflagArr = flagArrs.shift(); varcombArr = []; for(vari = 0; i < len; i++) { flagArr[i] && combArr.push(targetArr[i]); } resultArrs.push(combArr); } } returnresultArrs; } /** * 獲得從m中取n的所有組合 */ functiongetFlagArrs(m, n) { if(!n || n < 1) { return[]; } varresultArrs = [], 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; varaTmp = flagArr.concat(); resultArrs.push(aTmp); if(aTmp.slice(-n).join("").indexOf('0') == -1) { isEnd =true; } break; } flagArr[i] == 1 && leftCnt++; } } returnresultArrs; } //初始化使用者選擇事件 $(function() { initSKU(); varendTime =newDate().getTime(); $('#init_time').text('init sku time: '+ (endTime - startTime) +" ms"); $('.sku').each(function() { varself = $(this); varattr_id = self.attr('attr_id'); if(!SKUResult[attr_id]) { self.attr('disabled','disabled'); } }).click(function() { varself = $(this); //選中自己,兄弟節點取消選中 self.toggleClass('bh-sku-selected').siblings().removeClass('bh-sku-selected'); //已經選擇的節點 varselectedObjs = $('.bh-sku-selected'); if(selectedObjs.length) { //獲得組合key價格 varselectedIds = []; selectedObjs.each(function() { selectedIds.push($(this).attr('attr_id')); }); selectedIds.sort(function(value1, value2) { returnparseInt(value1) - parseInt(value2); }); varlen = selectedIds.length; varprices = SKUResult[selectedIds.join(';')].prices; varmaxPrice = Math.max.apply(Math, prices); varminPrice = Math.min.apply(Math, prices); $('#price').text(maxPrice > minPrice ? minPrice +"-"+ maxPrice : maxPrice); //用已選中的節點驗證待測試節點 underTestObjs $(".sku").not(selectedObjs).not(self).each(function() { varsiblingsSelectedObj = $(this).siblings('.bh-sku-selected'); vartestAttrIds = [];//從選中節點中去掉選中的兄弟節點 if(siblingsSelectedObj.length) { varsiblingsSelectedObjId = siblingsSelectedObj.attr('attr_id'); for(vari = 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) { returnparseInt(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
使用者選擇了itemB,使用陣列selected=['B']表示,selected可以為空陣列
演算法過程
1. 迴圈所有sku屬性forEach(result, (curitems, attr)->),使curitems等於屬性對應的所有元素,attr等於屬性id。
2. 克隆資料attrSelected = selected
3. 判斷屬性attr中是否有元素在陣列attrSelected中,如果存在,從attrSelected去掉存在的元素
4. 迴圈屬性下的元素forEach(curitems, (item)->,使得item等於單個屬性的值
5. 把attrSelecteditem組合成sku
6. 迴圈result,判斷第五組成的sku在result中是否存在,如果存在,退出迴圈4,返回true,進入步驟8
7. 當前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的子集。

現在處理字串就轉換為處理乘法演算法了,有了以上的分析,我們可以整理下演算法過程:

  1. 資料預處理,生成一組隨機數,把所有item一一對應一個質數,把item組合轉換為一幾個 質數的積
  2. 根據使用者已經選擇的item進行掃描所有的item,如果item已經被選中,則退出,如果沒有, 則和所有已經選擇的item進行相乘(特別注意,以選中的item需要去掉和當前匹配的item 在同一個類目中的item,因為一個組合不可能出現兩個類目相同的item) ,這個乘機就是 上文中的集合B
  3. 把集合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

  1. 第一次分解ab,a = 10, ab = 10, b = 10
  2. 第二次分解aB, a = a + 10 = 20, aB = 10, B = 10
  3. 第三次分解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判斷。此外,第二種演算法獲得的結果比起第一種更具有價值,第二種方式直接取得組合對應的數目,價格資訊,而第一種只是判斷是否可以組合,從實際應用角度而言,第二種方式還是剩下不少事的。

感覺只要善於去發現,還能能夠找到一些有意思的解決問題思路的。