30 行代码实现 iOS 商城商品多规格 SKU 组合算法

来源:Pixabay

0x00 前言

最近公司的一个项目,要增加商城功能,并且可以让用户自己创建商品,可以是单规格的,也可以是多规格的。所以这里分享一下商品的多个规格,生成所有 SKU 组合是怎么实现的。写的时候也没想去网上找现成的,想自己试着实现下,感觉不会很难。

0x01 理清思路

如果是单组的情况,比如只有一个规格:颜色 ,这个规格有几个 SKU: [黑色,白色,红色] ,那么就不需要组合,所以先判断长度,为 1 就直接用,然后继续处理规格数量大于 1 的情况。

我们可以通过模拟几组数据,来找找规律。

先看两组的情况,比如:
规格A:[a0,a1,a2]
规格B:[b0,b1,b2]

先从 a0 开始,跟规格 B 里面的 3 个分别组合:
a0b0,a0b1,a0b2

然后是 a1:a1b0,a1b1,a1b2

最后是 a2:a2b0,a2b1,a2b2

再看三组的情况:
规格A:[a0,a1,a2]
规格B:[b0,b1,b2]
规格C:[c0,c1,c2]

还是 a0 开始,先跟b0组合,接着跟规格C里面的三个分别组合:
a0b0c0,a0b0c1,a0b0c2

然后返回上一层的规格B,用a0和b1组合之后再跟规格C里面的三个分别再组合:
a0b1c0,a0b1c1,a0b1c2

以此类推:
a0b2c0,a0b2c1,a0b2c2

当规格B里面的所有元素都组合完之后,再返回上一层,也就是规格A,下标加1,重复前面的操作,先跟b0组合,接着跟规格C里面的三个分别再组合:
a1b0c0,a1b0c1,a1b0c2

然后再以此类推,这样,我们大概就可以看出一些规律了。

0x02 寻找规律

既然是寻找规律,那么就不定死规格的数量了,假设有 N 个规格,而且每个规格里面的元素数量也不相同。

首先,先拎出第一个规格的数组 N0,作为一个起始,遍历里面的元素,也就是 for 循环,先用下标为 0 的元素 N0[0],跟下一个规格的数组 N1 的第一个元素 N1[0] 组合,如果还有下一层 N2,则继续跟 N2 的第一个元素 N2[0] 组合,直到最后一个规格的数组的第一个元素 N(n-1)[0],到这里,第一个 SKU 组合就完成了。

第二个组合,前面都是一样的,但是最后一个规格的数组,元素变成了第二个,也就是下标加 1。
第三个组合,也还是一样的,最后一个规格的数组,元素变成了第三个,也就是下标再加 1。
.
.
.
直到最后一个规格 N(n-1) 的数组,元素到了最后一个,遍历结束。

然后返回到上一个规格 N(n-2),下标加 1,然后和最后一个规格 N(n-1) 的各个元素分别再组合,组合结束之后,再次返回到 N(n-2),下标再加 1,直到 N(n-2) 的元素也到最后一个,相同的,再返回到上一个规格 N(n-3),直到 n = 1 时,元素也遍历完最后一个。

至此,N0 的第一次 for 循环结束,继续后面的循环。

下面来贴上代码,实操下。

0x03 算法代码

以下是算法,Swift 编写,可以复制到 Xcode 的 Playground 中运行

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
// 传入一个二维数组,元素不一定是 String,可以是一个对象,对象里面存有规格的元素数组就行,可以自行扩展更改
func groupingSKU(withSpecs specs: [[String]]) {
// 声明一个数组,用来存放所有 SKU 的组合
var list = [String]()
// 先过滤一遍,没有元素的规格不需要组合
let enumerateList = specs.filter { $0.count > 0 }
if 1 == enumerateList.count { // 如果只有一个规格,就直接添加所有的元素
list.append(contentsOf: enumerateList.first!)
// 打印组合数据
debugPrint("group: \(list)")
} else if enumerateList.count > 1 { // 如果规格数大于 1,说明至少有两组规格,下面开始算法的核心逻辑
// 用 for 循环来遍历第一个规格的元素
for fn in enumerateList.first! {
var level = 1 // 设置当前的层级为 1,也就是第二个规格(下标从 0 开始)
var group = [fn] // 声明当前 SKU 组合的数据,先加入第一个规格的第一个元素
// 创建一个下标数据,保存当前循环内,遍历到的每个层(规格)的元素下标,第一个其实一直都为 0,不会去改,但是 level 从 1 开始,为了方便,就当一个占位元素,不用每次 level - 1
var indexs: [Int] = Array(repeating: 0, count: enumerateList.count)
while level < enumerateList.count { // while 循环从第二个规格开始遍历剩余的所有规格
let index = indexs[level] // 取出当前层级遍历的下标
if index >= enumerateList[level].count { break } // 如果这个下标大于等于这个层级的元素数量,说明这个层级遍历完了,则跳出 while 循环
group.append(enumerateList[level][index]) // 加入当前层级的当前下标的元素
if level < (enumerateList.count - 1) { // 当前不是最后一层,下面还有层数
level += 1 // 层级加 1,进入下一层级
} else { // 如果是最后一层
indexs[level] = index + 1 // 更新当前层级遍历的下标,下次从下一个开始取
debugPrint("group: \(group)") // 打印此次的 SKU 组合数据
list.append(group.joined(separator: ",")) // 把 SKU 组合拼接好放入 SKU 组合数组中
group = [fn] // 重置 SKU 组合数组,重新从第一个开始

if (index + 1) >= enumerateList[level].count { // 如果当前下标是最后一层的最后一个元素,则将上一层下标加 1,使其从下一个元素开始
if 1 == level { break } // 如果最后一层是第二层,说明遍历完成,跳出 while 循环,开始下一次 for 循环

let preIndex = indexs[level - 1] // 取出上一层的下标
indexs[level - 1] = preIndex + 1 // 设置为下一个元素的下标
indexs[level] = 0 // 重置最后一层的下标为 0,从第一个元素开始
}

level = 1 // 将层级改为第二层
}
}
}
}

// 计算正确的 SKU 组合数量
var count = enumerateList.count > 0 ? 1 : 0
for specs in enumerateList {
count *= specs.count
}
// 打印算法生成的组合数量和正确的组合数量
debugPrint("lsit.count = \(list.count), should: \(count)")
// 打印算法生成的所有 SKU 组合数据
debugPrint("lsit = \(list)")
}

每步都有注释,如果不算注释,空行,还有 log,实际的代码行数也就 30 行。

下面来测试一下这个算法,来写几个测试用例。

0x04 测试

测试用例0,没有数据:

1
groupingSKU(withSpecs: [[]])

打印:

1
2
"list.count = 0, should: 0"
"list = []"

测试用例1,只有一组数据:

1
groupingSKU(withSpecs: [["S", "M", "L", "XL", "XXL", "XXXL"]])

打印:

1
2
3
"group: ["S", "M", "L", "XL", "XXL", "XXXL"]"
"list.count = 6, should: 6"
"list = ["S", "M", "L", "XL", "XXL", "XXXL"]"

测试用例2,两组数据:

1
groupingSKU(withSpecs: [["S", "M", "L"], ["黑色", "白色", "红色"]])

打印:

1
2
3
4
5
6
7
8
9
10
11
"group: ["S", "黑色"]"
"group: ["S", "白色"]"
"group: ["S", "红色"]"
"group: ["M", "黑色"]"
"group: ["M", "白色"]"
"group: ["M", "红色"]"
"group: ["L", "黑色"]"
"group: ["L", "白色"]"
"group: ["L", "红色"]"
"list.count = 9, should: 9"
"list = ["S,黑色", "S,白色", "S,红色", "M,黑色", "M,白色", "M,红色", "L,黑色", "L,白色", "L,红色"]"

测试用例3,多组数据:

1
groupingSKU(withSpecs: [["S", "M", "L", "XL"], ["黑色", "白色", "红色"], ["一件", "两件", "三件", "四件"]])

打印:

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
"group: ["S", "黑色", "一件"]"
"group: ["S", "黑色", "两件"]"
"group: ["S", "黑色", "三件"]"
"group: ["S", "黑色", "四件"]"
"group: ["S", "白色", "一件"]"
"group: ["S", "白色", "两件"]"
"group: ["S", "白色", "三件"]"
"group: ["S", "白色", "四件"]"
"group: ["S", "红色", "一件"]"
"group: ["S", "红色", "两件"]"
"group: ["S", "红色", "三件"]"
"group: ["S", "红色", "四件"]"
"group: ["M", "黑色", "一件"]"
"group: ["M", "黑色", "两件"]"
"group: ["M", "黑色", "三件"]"
"group: ["M", "黑色", "四件"]"
"group: ["M", "白色", "一件"]"
"group: ["M", "白色", "两件"]"
"group: ["M", "白色", "三件"]"
"group: ["M", "白色", "四件"]"
"group: ["M", "红色", "一件"]"
"group: ["M", "红色", "两件"]"
"group: ["M", "红色", "三件"]"
"group: ["M", "红色", "四件"]"
"group: ["L", "黑色", "一件"]"
"group: ["L", "黑色", "两件"]"
"group: ["L", "黑色", "三件"]"
"group: ["L", "黑色", "四件"]"
"group: ["L", "白色", "一件"]"
"group: ["L", "白色", "两件"]"
"group: ["L", "白色", "三件"]"
"group: ["L", "白色", "四件"]"
"group: ["L", "红色", "一件"]"
"group: ["L", "红色", "两件"]"
"group: ["L", "红色", "三件"]"
"group: ["L", "红色", "四件"]"
"group: ["XL", "黑色", "一件"]"
"group: ["XL", "黑色", "两件"]"
"group: ["XL", "黑色", "三件"]"
"group: ["XL", "黑色", "四件"]"
"group: ["XL", "白色", "一件"]"
"group: ["XL", "白色", "两件"]"
"group: ["XL", "白色", "三件"]"
"group: ["XL", "白色", "四件"]"
"group: ["XL", "红色", "一件"]"
"group: ["XL", "红色", "两件"]"
"group: ["XL", "红色", "三件"]"
"group: ["XL", "红色", "四件"]"
"list.count = 48, should: 48"
"list = ["S,黑色,一件", "S,黑色,两件", "S,黑色,三件", "S,黑色,四件", "S,白色,一件", "S,白色,两件", "S,白色,三件", "S,白色,四件", "S,红色,一件", "S,红色,两件", "S,红色,三件", "S,红色,四件", "M,黑色,一件", "M,黑色,两件", "M,黑色,三件", "M,黑色,四件", "M,白色,一件", "M,白色,两件", "M,白色,三件", "M,白色,四件", "M,红色,一件", "M,红色,两件", "M,红色,三件", "M,红色,四件", "L,黑色,一件", "L,黑色,两件", "L,黑色,三件", "L,黑色,四件", "L,白色,一件", "L,白色,两件", "L,白色,三件", "L,白色,四件", "L,红色,一件", "L,红色,两件", "L,红色,三件", "L,红色,四件", "XL,黑色,一件", "XL,黑色,两件", "XL,黑色,三件", "XL,黑色,四件", "XL,白色,一件", "XL,白色,两件", "XL,白色,三件", "XL,白色,四件", "XL,红色,一件", "XL,红色,两件", "XL,红色,三件", "XL,红色,四件"]"

测试用例4,多组数据,单个元素:

1
groupingSKU(withSpecs: [["S"], ["黑色"], ["一件"]])

打印:

1
2
3
"group: ["S", "黑色", "一件"]"
"list.count = 1, should: 1"
"list = ["S,黑色,一件"]"

测试用例5,多组数据,存在空元素的情况:

1
groupingSKU(withSpecs: [["S"], [], ["一件"]])

打印:

1
2
3
"group: ["S", "一件"]"
"list.count = 1, should: 1"
"list = ["S,一件"]"

测试全部通过。💯