从N个元素的切片生成K个元素的算法

时间:2019-01-05 16:48:18

标签: algorithm go combinations

我正在尝试从Go中的this Stackoverflow question移植算法。我正在尝试使用的算法如下:给定任意长度的字符串切片和“深度”,找到原始切片中长度为深度的元素的所有组合。例如,如果给定一个包含A,B,C,D,E和F且深度为3的切片,则结果应为:

OleDbCommand cmd = new OleDbCommand("SELECT * FROM Address WHERE ID = 1;", conn); //query
OleDbDataReader cusReader = cmd.ExecuteReader();

while (cusReader.Read())
{
    ip = cusReader.GetValue(0).ToString();
}
serverIP = ip;
cusReader.Close();

TcpClient client = new TcpClient(serverIP, port);

我已经尝试在上述Go语言中实现一些建议的解决方案,但是不幸的是,我的Go语言技能还没有达到标准。几周前我才开始使用Go编程。

以下是损坏的代码,是尝试移植this implementation in Java的失败:

[A, B, C]
[A, B, D]
[A, B, E]
[A, B, F]
[A, C, D]
[A, C, E]
[A, C, F]
[A, D, E]
[A, D, F]
[A, E, F]
[B, C, D]
[B, C, E]
[B, C, F]
[B, D, E]
[B, D, F]
[B, E, F]
[C, D, E]
[C, D, F]
[C, E, F]
[D, E, F]

我不知道Java中的实现是否是最有效的方法,但它似乎相当有效,并且(我认为)移植到Go相对容易。如果有一种完全不同但更有效的方法,我很乐意接受。

1 个答案:

答案 0 :(得分:1)

注意:

实际上,对任何组合问题的正确答案是永远不要将所有可能的组合放入容器中,然后再对其进行处理。通常会有大量的组合,并且临时容器往往会用完所有可用的内存以用于仅将被引用一次的项目。原始的Java程序将处理步骤(在本例中为“打印组合”)埋在了生成函数的内部,实际上这也不是一个好的解决方案,因为它需要为每个不同的动作创建一个全新的生成器函数。

构造组合生成的一种方法是使用给定前一个组合的函数,该函数查找下一个组合。这种功能通常称为“迭代器”。如果提供了最后一个组合,则该函数返回一个返回值,指示没有更多可用组​​合。通常,所提供的组合是“就地”修改的,因此返回值只是一个布尔值,指示组合是否为最后一个组合。 (通常认为最佳做法是将提供的组合重置为第一个组合,并报告以前是最后一个组合。)该策略不适用于递归算法(例如您要移植的组合)。

许多语言都包含一些允许递归生成可能值的功能。例如,在Go中,您可以将迭代器编写为“ go例程”。尽管存在潜在的成本,但这可以产生非常精美的代码。

始终可以通过使用某种堆栈数据结构模拟调用堆栈来将递归函数重新实现为迭代函数。但是,结果很难理解,而且通常较慢(因为本机递归几乎总是比模拟递归快)。而且您也许可以找到一种非递归算法进行迭代(可能会更改迭代顺序)。

不过,在这里我不会做任何事情。下面的代码仅满足与原始代码相同的原型,并返回(可能是巨大的)结果切片,因为根本问题仅仅是递归函数的设计问题。


内部递归生成器的原型是

func GetCombos2(set []string,
                depth int, start int,
                element []string,
                results []string) []string

(为清楚起见,我添加了element的类型。)准确阐明此函数的作用很有用,这可能是这样的:

  

给出项目列表set,部分组合element(仍需要附加depth个项目和组合列表results),返回{ {1}}附加了以results表示的前缀开头的可能组合,其继续仅包含索引大于等于element的项目。组合以单调递增的索引顺序生成,并且要求前缀中的所有项目的索引都小于start

这有点大话,而且我不确定阅读它是否比代码更清楚。但这可能有助于理解正在发生的事情。在这里,我将只关注一小部分:

  

给出…start,…返回results并附加…[使用这些参数计算出的新组合]

这不是编写此递归的唯一可能方法。另一种方法是不要求results作为参数,而只是返回根据其他参数生成的组合列表。那会产生稍微简单的代码,但是由于生成并立即丢弃的部分结果的切片数量可能会慢很多。使用results之类的“累加器”参数是使递归更有效的常用技术。

此讨论的重要之处在于理解递归函数的返回值是什么。如果您使用“累加器”策略(带有results参数),则返回值是到目前为止找到的结果的整个列表,并且仅在添加新结果时才追加。如果您使用非累加器策略,那么当您发现一个新结果时,会立即将其返回,将其留给调用方以连接它从多个调用中收到的各种列表。

所以这两种策略看起来像这样:

累加器版本:

results

非累加器版本:

func GetCombos2(set []string, depth int, start int, element []string, results []string) []string {
    if depth == 0 {
        results = append(results, strings.Join(element, "")) 
    } else {
        for i := start; i <= len(set) - depth; i++ {
            element[len(element) - depth] = set[i]
            results = GetEnvCombos2(set, depth - 1, i + 1, element, results)
        }
    }
    return results
}

编辑:在写完这些之后,我意识到使用字符串数组func GetCombos2(set []string, depth int, start int, element []string) []string { if depth == 0 { return []string { strings.Join(element, "") } } else { var results []string for i := start; i <= len(set) - depth; i++ { element[len(element) - depth] = set[i] results = append(results, GetCombos2(set, depth - 1, i + 1, element)...) } return results } } 确实是一种Java主义,无法很好地转换为Go。 (或者也许是C-ism不好地翻译为Java。)无论如何,如果我们只传递表示前缀的elements,则函数会稍快一些,并且易于阅读,因此我们不需要这样做string。 (Go字符串是不可变的,因此在将它们放入结果切片之前无需复制它们。)

这会将代码简化为以下内容:

累加器版本(推荐,但是迭代器会更好):

Join

非累加器版本:

func GetCombos(set []string, depth int) []string {
    return GetCombosHelper(set, depth, 0, "", []string{})
}

func GetCombosHelper(set []string, depth int, start int, prefix string, accum []string) []string {
    if depth == 0 {
        return append(accum, prefix)
    } else {
        for i := start; i <= len(set) - depth; i++ {
            accum = GetCombosHelper(set, depth - 1, i + 1, prefix + set[i], accum)
        }
        return accum
    }
}

在我的笔记本电脑上,给定一组62个深度为6的元素(大写和小写字母加数字),非累加器版本花费了29.7秒(经过),累加器版本花费了13.4秒。两者都使用了约4.5 GB的内存,这对我来说似乎有点高,因为“只有” 61,474,519个六字符组合,并且每个组合的内存消耗达到了将近80字节的峰值内存使用量。