最近发现求排列组合在大公司的笔试算法题中经常作为比较重要的一步出现,所以写篇文章好好整理一下。
首先,回顾一下高中知识。。。排列组合的公式。
接下来对排列、组合分别给出 Java 代码的实现,而且每个部分都会给出两个方面的实现。
排列会先给出求全排列数量的代码实现,然后给出求全排列结果的代码实现。
组合会先给出求所有组合数量的代码实现,然后给出求所有组合结果的代码实现。
排列
一、求全排列的数目
没什么好解释的,就是排列公式的 Java 实现。
// 求排列数 A(n,m) n>m
public static int A(int n, int m) {
int result = 1;
// 循环m次,如A(6,2)需要循环2次,6*5
for (int i = m; i > 0; i--) {
result *= n;
n--;// 下一次减一
}
return result;
}
二、求全排列的结果
这里直接拿 LeetCode 46 题举例。
LeetCode 46:全排列
这种要求枚举出所有可能结果的类型的题一般都是采用回溯思想来实现,初次接触的话,都会有点绕不过来,建议这种问题如果想不明白的话,就拿纸笔把递归树给画出来,思路会清晰很多。
代码实现,该注意的地方都写在注释里了:
class Solution {
private List<List<Integer>> res = new ArrayList<>();
//声明一个布尔数组,用来判断某个索引位置的数字是否被使用过了
private boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0) {
return res;
}
used = new boolean[nums.length];
List<Integer> preList = new ArrayList<>();
generatePermutation(nums, 0, preList);
return res;
}
/**
* 回溯
* @param nums 给定数组
* @param index 当前考察的索引位置
* @param preList 先前排列好的子序列
*/
private void generatePermutation(int[] nums,int index,List<Integer> preList) {
//index 等于给定数组的长度时,说明一种排列已经形成,直接将其加入成员变量 res 里
if (index == nums.length) {
//这里需要注意java的值传递
//此处必须使用重新创建对象的形式,否则 res 列表中存放的都是同一个引用
res.add(new ArrayList<>(preList));
return;
}
for(int i = 0; i < nums.length ;i++) {
if (!used[i]) {
preList.add(nums[i]);
used[i] = true;
generatePermutation(nums, index + 1, preList);
//一定要记得回溯状态
preList.remove(preList.size() - 1);
used[i] = false;
}
}
return;
}
}
组合
一、求组合的数目
跟排列一样,都是对公式的实现,不过组合这里有个小技巧可以对代码进行优化,那就是利用组合的互补率:C(n,m) = C(n,n - m) 来减少循环次数
求组合数的代码这里给出两个版本
版本一:根据 C(n,m) = A(n,m) / m!,又因为 m!其实就是 A(m,m),因为 0!就是 1 嘛,所以 C(n,m) = A(n,m) / A(m,m). 这样我们可以直接利用上面实现好的求全排列的公式。
public static int C(int n, int m){
// 应用组合数的互补率简化计算量
m = m > (n - m) ? (n - m) : m;
// 分子的排列数
int son = A(n, m);
// 分母的排列数
int mother = A(m, m);
return son / mother ;
}
版本二:也是根据 C(n,m) = A(n,m) / m!,但我们注意到,A(n,m) 的代码实现中,就是从 m 一直遍历到 1,所以在 A(n,m) 的代码实现中我们简单的加入对 m!的求解即可,不需要为了求组合数还得多写一个求全排列的函数。
public static int C(int n,int m) {
//分子
int son = 1;
//分母
int mother = 1;
// 应用组合数的互补率简化计算量
m = m > (n - m) ? (n - m) : m;
for(int i = m;i > 0; i--) {
//如果你还记得上面的求全排列数的实现,你应该会发现 son 就是在求 A(n,m)
son *= n;
mother *= i;
n--;
}
return son / mother;
}
二、求所有组合的结果
跟求全排列的结果一样,还是那句话,这种要求枚举出所有可能结果的类型的题一般都是采用回溯思想来实现。求组合实现起来比求全排列简单一些。
这里拿 LeetCode 77 举例。
LeetCode 77:组合
class Solution {
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
if (n <= 0 || k <= 0 || k > n) {
return res;
}
List<Integer> c = new ArrayList<>();
generateCombinations(n, k, 1, c);
return res;
}
/**
* 回溯求所有组合结果
* @param n
* @param k
* @param start 开始搜索新元素的位置
* @param c 当前已经找到的组合
*/
private void generateCombinations(int n,int k,int start,List<Integer> c) {
if (c.size() == k) {
//这里需要注意java的值传递
//此处必须使用重新创建对象的形式,否则 res 列表中存放的都是同一个引用
res.add(new ArrayList<>(c));
return;
}
//通过终止条件,进行剪枝优化,避免无效的递归
//c中还剩 k - c.size()个空位,所以[ i ... n]中至少要有k-c.size()个元素
//所以i最多为 n - (k - c.size()) + 1
for(int i = start;i <= n - (k - c.size()) + 1; i++) {
c.add(i);
generateCombinations(n, k, i + 1, c);
//记得回溯状态啊
c.remove(c.size() - 1);
}
}
}