最近在一些算法题中,遇到了不少次借助排列组合得到结果的情况。相较于数学中求得排列组合数量的问题,算法中常常需要得到所有的排列组合项,以便在一些枚举步骤中辅助求解。下面给出简化版的排列组合问题的定义:

排列问题:在 n 个数字中选择 m 个,其中 m <= n,考虑所选数字的顺序
组合问题:在 n 个数字中选择 m 个,其中 m <= n,不考虑所选数字的顺序

注:以下不考虑数字重复的情况,如果实际问题中需要考虑,可借助 HashMap 过滤。

1. 排列( Permutation )

1.1 参考思路

我们可以将问题这么看待:

从数组 s 中取 m 个数 = 取出数字 N + 从去除 N 的数组 {s - N} 中取 m - 1 个数

这是明显的递归思路,核心在于:

  1. 对于任意一层递归,数字 N 的选取按照固定顺序,这里采用从左到右,即按数组下标递增的顺序;
  2. 递归边界在于 {s - N} 中元素的个数为 1 时。

在编码时,我们需要为递归函数准备一些变量:

  1. 给定的待选数组;
  2. 可选个数;
  3. 前面递归层级中已经选择的 N,N',N''... 的元素集合;
  4. 用于存储可选类型的容器。

1.2 参考代码

按照上述参考思路,我们可以得到排列问题解法中,递归部分的代码:

/**
  * 递归函数
  *
  * @param count      可选个数
  * @param candidates 给定的待选数组
  * @param bag        前面递归层级中已经选择的 N,N',N&#39;&#39;... 的元素集合
  * @param container  用于存储可选类型的容器
  */
  def traverse(count: Int, candidates: ArrayBuffer[Int], bag: ArrayBuffer[Int], container: ArrayBuffer[ArrayBuffer[Int]]): Unit = {
    val candidatesLength = candidates.length
    if (candidatesLength >= count) {
        if (count.equals(1)) {
            for (item <- candidates) {
                val finalBag = bag.clone()
                finalBag += item
                container += finalBag
            }
        } else {
            for ((item, index) <- candidates.zipWithIndex) {
                val nextBag = bag.clone()
                nextBag += item
                val nextCandidates = candidates.slice(0, index) ++ candidates.slice(index + 1, candidatesLength)
                traverse(count - 1, nextCandidates, nextBag, container)
            }
        }
    }
}

则排列问题的入口方法为:

def getAllPermutations(count: Int, candidates: ArrayBuffer[Int]): ArrayBuffer[ArrayBuffer[Int]] = {
    val container = ArrayBuffer[ArrayBuffer[Int]]()
    traverse(count, candidates, ArrayBuffer[Int](), container)
    container
}

代码 Gist 地址:Select all permutations of N elements from the collection. · GitHub

2. 组合( Combination )

2.1 参考思路

由于不考虑数字的顺序,相较于排列问题,组合问题仅有一处不同,即:

从数组 s 中取 m 个数 = 取出数字 N + 从去除 N 的数组 {s - N} 中取 m - 1 个数。但当递归的上一层级由左至右遍历时,下一层中的 {s - N} 仅包含上一层级右侧的数。如下图:

在排列问题中,如果当前层级选择的是箭头所指向的数字,则其左侧和右侧的数字都会被当做 candidates 被传入下一层级:

涉及代码为:

val nextCandidates = candidates.slice(0, index) ++ candidates.slice(index + 1, candidatesLength)
traverse(count - 1, nextCandidates, nextBag, container)

而在组合问题中,只有右侧的数字会被传入下级( 当按照由左至右的顺序遍历时 ):

涉及代码为:

val nextCandidates = candidates.slice(index + 1, candidates.length)
traverse(count - 1, nextCandidates, nextBag, container)

2.2 参考代码

递归代码:

/**
  * 递归函数
  *
  * @param count      可选个数
  * @param candidates 给定的待选数组
  * @param bag        前面递归层级中已经选择的 N,N',N&#39;&#39;... 的元素集合
  * @param container  用于存储可选类型的容器
  */
def traverse(count: Int, candidates: ArrayBuffer[Int], bag: ArrayBuffer[Int], container: ArrayBuffer[ArrayBuffer[Int]]): Unit = {
    val candidatesLength = candidates.length
    if (candidatesLength >= count) {
        if (count.equals(1)) {
            for (item <- candidates) {
                val finalBag = bag.clone()
                finalBag += item
                container += finalBag
            }
        } else {
            for ((item, index) <- candidates.zipWithIndex) {
                val nextBag = bag.clone()
                nextBag += item
                val nextCandidates = candidates.slice(0, index) ++ candidates.slice(index + 1, candidatesLength)
                traverse(count - 1, nextCandidates, nextBag, container)
            }
        }
    }
}

入口方法:

def getAllPermutations(count: Int, candidates: ArrayBuffer[Int]): ArrayBuffer[ArrayBuffer[Int]] = {
    val container = ArrayBuffer[ArrayBuffer[Int]]()
    traverse(count, candidates, ArrayBuffer[Int](), container)
    container
}

代码 Gist 地址:Select all combinations of N elements from the collection. · GitHub

参考链接

  1. What is the correct way to get a subarray in Scala? - Stack Overflow

dailybird
1.1k 声望73 粉丝

I wanna.