组合算法(一)
组合算法在面试中可能会出相关的笔试题以此来考验面试者的功底,处理笔试题目以外我们还可以在一些应用场景中使用典型场景:
01. 介绍
场景应用
1. 属性组合
根据商品的属性规格计算可以组合的sku有多少种【什么是sku?这个不介绍了】如下
某某商品的属性规格(电脑)
颜色 = {银灰色,纯黑色,淡绿色}
CPU/运行内存 = {i7-8/16G, i7-8/8G, i5-8/16G, i5-8/8G}
存储空间 = {1T, 512G}
以上面三中规格属性组成具体的sku则就是;有如下24种
sku1 = {银灰色, i7-8/16G, 1T}
sku2 = {纯黑色, i7-8/16, 512G}
sku3 = {淡绿色, i5-8/16, 512G}
...
虽然我脑子告诉我们知道如何去计算但是,实际要用程序来写一脸懵,说的就是你
2. 随机礼品赠送
比如在20个礼品中选择10个礼品赠送给客户,这里就涉及到话题是给定一个元素列表从中选择n个元素进行排列组合;
等其他场景...
面试题
在面试题中,当然不乏有以上面的应用场景作为面试题的;还有的则为如下情况
给定一个字符串a,b,c,d
利用程序计算它的组合方式如abcd
,abdc
,adcb
...计算共有多少种组合方式;并且还可以输出,还要求时间复杂度最低....;
一般看到这样的笔试题目,心里就会说句:“好家伙脑子知道怎么排,但是下手有蒙圈了”
除了上面的题目外也可能存在给定一个数组或字符串从中随机选择几个元素进行排序等等,组合排序有很多..
等其他面试题...
优雅永不过时
我也遇到过,稍微冷静一下,咋们往下分解
02. 题1. 给定一个字符串abcd,求整体元素的组合类型
实现方式1
思路:在思路上选择前面的元素位置不进行变动,对后面的元素进行组合;将组合之后的结果与前面的元素拼接组合则完成组合排序;
如同;对abc进行组合,选中a不动对bc进行组合;也就是我们考虑两个元素的组合实现;
a := "bc"
var ret []string
for k, v := range a {
tmp = fmt.Sprintf("%s%s", data[:k], data[k+1:])
for _, j := range tmp {
ret = append(ret, string(v) + ""+ r )
}
}
fmt.Printf("ret = %v", ret)
在实现上我们可以通过如上代码实现;先对a循环,在循环完成之后可以通过切片的方式获取v的前后元素并写入到tmp中,再利用内部的循环完成对程序的组合
在实现b,c组合后如何与a进行组合?实际上我们只需要对b,c组合后的结果ret与a进行一次循环即可, 在实现前我们构思一下对于a,b,c的第一次循环的时候;是不是也需要
fmt.Sprintf("%s%s", data[:k], data[k+1:])
通过上面的方式截取前后元素呢?很明显需要,也同样需要具有一个ret记录组合结果因此我们可以只需要简单修改利用闭包即可实现
package _1_combinatorial
import "fmt"
type Permutation struct {
}
var count = 0
// 方案1
// 方法:采用递归的思想,选中一个元素不变然后不停的与其他元素进行组合
func (p Permutation) Recursion(data string) []string {
// 只有一种结果即可返回
if len(data) <= 1 {
return []string{data}
}
var ret []string
// 思路:保持前面的元素不变,先对后面的元素进行位置交换,在交换完成之后就会调整前面的元素
// 比如 a ,b ,c => 先分为 a [b, c] a 保持不变先对b和c进行组合
// 而b, c => 也可以再分为 b , [c] ; 发现c只有一个元素则无法再组合返回,最终等到第一个组合 bc 返回
// 这个时候程序下标移植c => 则程序为 [c], b; 则等到第二个组合cb;
// 当[b,c]的组合完成之后就可以把得到的组合[bc,cb]与a组合,这个时候程序的上级下标就移植b不动
for k, v := range data { // n
// 利用切片的特点 数组 arr = [a, b, c] ; 下标index = 1;
// 通过 arr[:index] 可以获取到 a
// 通过 arr[index+1:] 可以获取到 c
// 比如 data =》a,b,c ; 这时候 k = 0; 分割之后就是 [], [b,c]
// 利用递归再次进入程序之后data =》b,c; 这是 k1 = 0 ; 分割后为 [], [c] 继续进入循环因为只有一个元素则可以直接返回
// res = c;这个时候完成第一次拼接为 bc; 而结束之后 k1 = 1 ; 分割后 [],
// 切片细节
res := p.Recursion(fmt.Sprintf("%s%s", data[:k], data[k+1:]))
for _, r := range res{
ret = append(ret, string(v) + ""+ r )
}
}
return ret
}
声明:结构体对本案例没有影响是因为有多种方式实现统一归并为一个结构体;
如上代码中我们仅仅只需要增加对参数的判断个数是否小于等于1(就1个能组合啥?);递归的方式调度Recursion方法即可;最终吧组合好的结果统一return返回
时间复杂度为:O(n*n!)
实现方式2
在方式1中我们很成功的实现了对元素的组合,但是也同时存在一个比较大的问题;时间复杂比较糟糕;优化可以基于备忘录的方式来,将已经排序好的内容存储在指定元素中需要的时候判断之前是否有排序即可
func (p Permutation) RecursionMemo(data string)(ret []string) {
return p.recursionMemo(data,make(map[string][]string))
}
func (p Permutation) recursionMemo(data string, memo map[string][]string) []string {
// 只有一种结果即可返回
if len(data) <= 1 {
return []string{data}
}
var ret []string
// 思路:保持前面的元素不变,先对后面的元素进行位置交换,在交换完成之后就会调整前面的元素
// 比如 a ,b ,c => 先分为 a [b, c] a 保持不变先对b和c进行组合
// 而b, c => 也可以再分为 b , [c] ; 发现c只有一个元素则无法再组合返回,最终等到第一个组合 bc 返回
// 这个时候程序下标移植c => 则程序为 [c], b; 则等到第二个组合cb;
// 当[b,c]的组合完成之后就可以把得到的组合[bc,cb]与a组合,这个时候程序的上级下标就移植b不动
if res,ok := memo[data]; ok {
return res
}
for k, v := range data { // n
// 利用切片的特点 数组 arr = [a, b, c] ; 下标index = 1;
// 通过 arr[:index] 可以获取到 a
// 通过 arr[index+1:] 可以获取到 c
// 比如 data =》a,b,c ; 这时候 k = 0; 分割之后就是 [], [b,c]
// 利用递归再次进入程序之后data =》b,c; 这是 k1 = 0 ; 分割后为 [], [c] 继续进入循环因为只有一个元素则可以直接返回
// res = c;这个时候完成第一次拼接为 bc; 而结束之后 k1 = 1 ; 分割后 [],
res := p.recursionMemo(fmt.Sprintf("%s%s", data[:k], data[k+1:]), memo)
for _, r := range res{
ret = append(ret, string(v) + ""+ r )
}
}
memo[data] = ret
return ret
}
时间复杂度在O(n!)与O(n*n!)之间;对于很多递归求解的方式都可以采用备忘录的思路哟;
实现方式3
对于上面的组合排序我们可以在思维上可以逆向思考;因为在上面1,2的方法中均是选择某一个元素不动而进行后续元素的排序;
另一种思想就是选择传递一个字符串或数组,某一个下标a,让下标a与其后的数组中每位元素进行交换,每一次交换所产生的元素则即为新的元素并存储,在下一轮的下标a往后移动一位,并且对上一次所产生的所有元素进行同样的交换,以此类推直到整个交换到 数组长度-1 次即可
比如这里a,b,c,d四个元素开始选择下标0号元素与其他元素交换位置;在交换前我们会把本身传入的元素当前组合也进行记录(因为本身也是一种组合)
进行如上的交换过程;最终一轮交换后就是如下的结果
第二轮则下标0往后移动为1,然后对第一轮产生的结果,对每一个元素都进行如上的交换方式同样保存;直到交换到n-1次即可;如下为具体代码的实现
func (p Permutation)dynamic(str string) []string {
if len(str) <= 1 {
return []string{str}
}
ret := []string{str}
// 交换的次数为n-1次,因为最后一位元素没有可交换的对象
for i := 0; i < len(str)-1; i++ {
// 对没个组合元素进行同样的交换过程
for _, item := range ret {
// 每次交换元素的下一位开始进行交换
for j := i + 1; j < len(str); j++ {
s := []byte(item)
// 交换
s[i], s[j] = s[j], s[i]
// 存储每一轮交换元素之后的结果
ret = append(ret, string(s))
}
}
}
return ret
}
整体方案的时间复杂度则为O(n!)
后续再补充剩下的;下回继续分解
代码地址:https://gitee.com/dn-jinmin/a...
。