原文:https://keelii.github.io/2016/12/22/sku-multi-dimensional-attributes-state-algorithm/javascript
這個問題來源於選擇商品屬性的場景。好比咱們買衣服、鞋子這類物件,通常都須要咱們選擇合適的顏色、尺碼等屬性html
先了解一下 sku 的學術概念吧前端
最小庫存管理單元(Stock Keeping Unit, SKU)是一個會計學名詞,定義爲庫存管理中的最小可用單元,例如紡織品中一個SKU一般表示規格、顏色、款式,而在連鎖零售門店中有時稱單品爲一個SKU。最小庫存管理單元能夠區分不一樣商品銷售的最小單元,是科學管理商品的採購、銷售、物流和財務管理以及POS和MIS系統的數據統計的需求,一般對應一個管理信息系統的編碼。 —— form wikipedia 最小存貨單位java
簡單的結合上面的實例來講: sku 就是你上購物網站買到的最終商品,對應的上圖中已選擇的屬性是:顏色 黑色 - 尺碼 37git
我先看看後端數據結構通常是這樣的,一個線性數組,每一個元素是一個描述當前 sku 的 map,好比:github
[ { "顏色": "紅", "尺碼": "大", "型號": "A", "skuId": "3158054" }, { "顏色": "白", "尺碼": "中", "型號": "B", "skuId": "3133859" }, { "顏色": "藍", "尺碼": "小", "型號": "C", "skuId": "3516833" } ]
前端展現的時候顯然須要 group 一下,按不一樣的屬性分組,目的就是讓用戶按屬性的維度去選擇,group 後的數據大概是這樣的:算法
{ "顏色": ["紅", "白", "藍"], "尺碼": ["大", "中", "小"], "型號": ["A", "B", "C"] }
對應的在網頁上大概是這樣的 UIchrome
這個時候,就會有一個問題,這些元子屬性能組成的集合(用戶的選擇路徑) 遠遠大於 真正能夠組成的集合,好比上面的屬性集合能夠組合成一個 笛卡爾積,即。能夠組合成如下序列:json
[ ["紅", "大", "A"], // ✔ ["紅", "大", "B"], ["紅", "大", "C"], ["紅", "中", "A"], ["紅", "中", "B"], ["紅", "中", "C"], ["紅", "小", "A"], ["紅", "小", "B"], ["紅", "小", "C"], ["白", "大", "A"], ["白", "大", "B"], ["白", "大", "C"], ["白", "中", "A"], ["白", "中", "B"], // ✔ ["白", "中", "C"], ["白", "小", "A"], ["白", "小", "B"], ["白", "小", "C"], ["藍", "大", "A"], ["藍", "大", "B"], ["藍", "大", "C"], ["藍", "中", "A"], ["藍", "中", "B"], ["藍", "中", "C"], ["藍", "小", "A"], ["藍", "小", "B"], ["藍", "小", "C"] // ✔ ]
根據公式能夠知道,一個由 3 個元素,每一個元素是有 3 個元素的子集構成的集合,能組成的笛卡爾積一共有 3 的 3 次冪,也就是 27 種,然而源數據只能夠造成 3 種組合後端
這種狀況下最好能提早判斷出來不可選的路徑並置灰,告訴用戶,不然會形成誤解
看下圖,若是咱們定義紅色爲當前選中的商品的屬性,即當前選中商品爲 紅-大-A
,這個時候如何確認其它非已選屬性是否能夠組成可選路徑?
規則是這樣的: 假設當前用戶想選 白-大-A
,恰好這個選擇路徑是不存在的,那麼咱們就把 白
置灰
以此類推,若是要確認 藍
屬性是否可用,須要查找 藍-大-A
路徑是否存在
...
根據上面的邏輯代碼實現思路就有了:
遍歷全部非已選元素:"白", "藍", "中", "小", "B", "C"
遍歷全部屬性行: "顏色", "尺碼", "型號"
取: a) 當前元素 b) 非當前元素所在的其它屬性已選元素,造成一個路徑
判斷此路徑是否存在,若是不存在將當前元素置灰
看來問題彷佛已經解決了,然而 ...
咱們忽略了一個很是重要的問題:上例中雖然 白
元素置灰,可是實際上 白
是能夠被點擊的!由於用戶能夠選擇 白-中-B
路徑
若是用戶點擊了 白
狀況就變得複雜了不少,咱們假設用戶 只選擇了一個元素 白
,此時如何判斷其它未選元素是否可選?
即:如何肯定 "大", "中", "小", "A", "B", "C"
須要置灰? 注意咱們並不須要確認 "紅","藍"
是否可選,由於屬性裏面的元素都是 單選,當前的屬性裏任何元素均可選的
咱們先 縮小問題範圍:當前狀況下(只有一個 白
已選)如何肯定尺碼 "大"
須要置灰? 你可能會想到根據咱們之間的邏輯,須要分別查找:
白 - 大 - A
白 - 大 - B
白 - 大 - C
他們都不存在的時候把尺碼 大
置灰,問題彷佛也能夠解決。其實這樣是不對的,由於 型號沒有被選擇過,因此只須要知道 白-大
是否可選便可
同時還有一個問題,若是已選的個數不肯定並且維度能夠增長到不肯定呢?
這種狀況下若是還按以前的算法,即便實現也很是複雜。這時候就要考慮換一種思惟方式
以前咱們都是反向思考,找出不可選應該置灰的元素。咱們如今正向的考慮,如何肯定屬性是否可選。並且多維的狀況下用戶能夠跳着選。好比:用戶選了兩個元素 白,B
圖1
咱們再回過頭來看下 原始存在的數據
[ { "顏色": "紅", "尺碼": "大", "型號": "A", "skuId": "3158054" }, { "顏色": "白", "尺碼": "中", "型號": "B", "skuId": "3133859" }, { "顏色": "藍", "尺碼": "小", "型號": "C", "skuId": "3516833" } ] // 即 [ [ "紅", "大", "A" ], // 存在 [ "白", "中", "B" ], // 存在 [ "藍", "小", "C" ] // 存在 ]
顯然:若是第一條數據 "紅", "大", "A"
存在,那麼下面這些子組合 確定都存在:
紅
大
A
紅 - 大
紅 - A
大 - A
紅 - 大 - A
同理:若是第二條數據 "白", "中", "B"
存在,那麼下面這些子組合 確定都存在:
白
中
B
白 - 中
白 - B
中 - B
白 - 中 - B
...
咱們提早把 全部存在的路徑中的子組合 算出來,算法上叫取集合全部子集,數學上叫 冪集, 造成一個全部存在的路徑表,算法以下:
/** * 取得集合的全部子集「冪集」 arr = [1,2,3] i = 0, ps = [[]]: j = 0; j < ps.length => j < 1: i=0, j=0 ps.push(ps[0].concat(arr[0])) => ps.push([].concat(1)) => [1] ps = [[], [1]] i = 1, ps = [[], [1]] : j = 0; j < ps.length => j < 2 i=1, j=0 ps.push(ps[0].concat(arr[1])) => ps.push([].concat(2)) => [2] i=1, j=1 ps.push(ps[1].concat(arr[1])) => ps.push([1].concat(2)) => [1,2] ps = [[], [1], [2], [1,2]] i = 2, ps = [[], [1], [2], [1,2]] j = 0; j < ps.length => j < 4 i=2, j=0 ps.push(ps[0].concat(arr[2])) => ps.push([3]) => [3] i=2, j=1 ps.push(ps[1].concat(arr[2])) => ps.push([1, 3]) => [1, 3] i=2, j=2 ps.push(ps[2].concat(arr[2])) => ps.push([2, 3]) => [2, 3] i=2, j=3 ps.push(ps[3].concat(arr[2])) => ps.push([2, 3]) => [1, 2, 3] ps = [[], [1], [2], [1,2], [3], [1, 3], [2, 3], [1, 2, 3]] */ function powerset(arr) { var ps = [[]]; for (var i=0; i < arr.length; i++) { for (var j = 0, len = ps.length; j < len; j++) { ps.push(ps[j].concat(arr[i])); } } return ps; }
有了這個存在的子集集合,再回頭看 圖1 舉例:
圖1
如何肯定 紅
可選? 只須要肯定 紅-B
可選
如何肯定 中
可選? 須要肯定 白-中-B
可選
如何肯定 2G
可選? 須要肯定 白-B-2G
可選
算法描述以下:
遍歷全部非已選元素
遍歷全部屬性行
取: a) 當前元素 b) 非當前元素所在的其它屬性已選元素(若是當前屬性中沒已選元素,則跳過),造成一個路徑
判斷此路徑是否存在(在全部存在的路徑表中查詢),若是不存在將當前元素置灰
以最開始的後端數據爲例,生成的全部可選路徑表以下:
注意路徑用分割符號「-」分開是爲了查找路徑時方便,不用遍歷
{ "": { "skus": ["3158054", "3133859", "3516833"] }, "紅": { "skus": ["3158054"] }, "大": { "skus": ["3158054"] }, "紅-大": { "skus": ["3158054"] }, "A": { "skus": ["3158054"] }, "紅-A": { "skus": ["3158054"] }, "大-A": { "skus": ["3158054"] }, "紅-大-A": { "skus": ["3158054"] }, "白": { "skus": ["3133859"] }, "中": { "skus": ["3133859"] }, "白-中": { "skus": ["3133859"] }, "B": { "skus": ["3133859"] }, "白-B": { "skus": ["3133859"] }, "中-B": { "skus": ["3133859"] }, "白-中-B": { "skus": ["3133859"] }, "藍": { "skus": ["3516833"] }, "小": { "skus": ["3516833"] }, "藍-小": { "skus": ["3516833"] }, "C": { "skus": ["3516833"] }, "藍-C": { "skus": ["3516833"] }, "小-C": { "skus": ["3516833"] }, "藍-小-C": { "skus": ["3516833"] } }
爲了更清楚的說明這個算法,再上一張圖來解釋下吧:
因此根據上面的邏輯得出,計算狀態後的界面應該是這樣的:
如今這種狀況下若是用戶點擊 尺碼 中
應該怎麼交互呢?
由於當前狀況下路徑 紅-中-A
並不存在,若是點擊 中
,那麼除了尺碼 中
以外其它的屬性中 至少有一個 屬性和 中
的路徑搭配是不存在的
交互方面需求是:若是不存在就高亮當前屬性行,使用戶必須選擇到能夠和 中
組合存在的屬性。並且用戶之間選擇過的屬性要作一次緩存
因此當點擊不存在的屬性時交互流程是這樣的:
不管當前屬性存不存在,先高亮(選中)當前屬性
清除其它全部已選屬性
更新當前狀態(只選當前屬性)下的其它屬性可選狀態
遍歷非當前屬性行的其它屬性查找對應的在緩存中的已選屬性
若是緩存中對應的屬性存在(可選),則默認選中緩存屬性並 再次更新 其它可選狀態。不存在,則高亮當前屬性行(深色背景)
這個過程的流程圖大概是這樣的,點進不存在的屬性就會進入「單選流程」
假設後端數據是這樣的:
[ { "顏色": "紅", "尺碼": "大", "型號": "A", "skuId": "3158054" }, { "顏色": "白", "尺碼": "大", "型號": "A", "skuId": "3158054" }, // 多加了一條 { "顏色": "白", "尺碼": "中", "型號": "B", "skuId": "3133859" }, { "顏色": "藍", "尺碼": "小", "型號": "C", "skuId": "3516833" } ]
當前選中狀態是:白-大-A
若是用戶點擊 中
。這個時候 白-中
是存在的,可是 中-A
並不存在,因此保留顏色 白
,高亮型號屬性行:
因而可知和 白-中
能搭配存在型號只有 B
,而緩存的做用就是爲了少讓用戶選一次顏色 白
到這裏,基本上主要的功能就實現了。好比庫存邏輯處理方式也和不存屬性同樣,就再也不贅述。惟一須要注意的地方是求冪集的複雜度問題
冪集算法的時間複雜度是 O(2^n)
,也就是說每條數據上面的屬性(維度)越多,複雜度越高。sku 數據的多少並不重要,由於是常數級的線性增加,而維度是指數級的增加
{1} 2^1 = 2 => {},{1} {1,2} 2^2 = 4 => {},{1},{2},{1,2} {1,2,3} 2^3 = 8 => {},{1},{2},{3},{1,2},{1,3},{2,3},{1,2,3} ...
在 chrome 裏面簡單跑了幾個用例,可見這個算法很是低效,若是要使用這個算法,必須控制維度在合理範圍內,並且不只僅算法時間複雜度很高,生成最後的路徑表也會很是大,相應的佔用內存也很高。
舉個例子:若是有一個 10 維的 sku,那麼最終生成的路徑表會有 2^10 個(1024) key/value
最終 demo 能夠查看這個:
sku 多維屬性狀態判斷
相關資料:
sku組合查詢算法探索