排列组合
排列组合是学生时代常见的数学基础理论,在计算机领域,他们也属于离散数学。
广义的组合数学(英语:Combinatorics)就是离散数学,狭义的组合数学是组合计数、图论、代数结构、数理逻辑等的总称。
在组合数学中,排列一词的传统意义是一个有序序列,其中元素不重复,但可能有阙漏。例如1,2,4,3可以称为1,2,3,4,5,6的一个排列,但是其中不含5,6。此时通常会标明为“从n个对象取r个对象的排列”。
学习离散数学的过程中,[计数] 对于排列组合是一个很有趣的研究方向。
• 排列:与元素的次序有关。
• 组合:与元素的次序无关。
📐 数学公式
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 阶乘函数(符号:!) ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 表示将一系列递减的自然数相乘。 例子: 4! = 4 × 3 × 2 × 1 = 24 7! = 7 × 6 × 5 × 4 × 3 × 2 × 1 = 5,040 1! = 1 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 排列的计数 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ▶ 设 A 是有 n 个元素的集合,则有 2^n 个子集 ▶ 如果 1 ≤ r ≤ n, 那么 n 个对象中每次取 r 个的排列数是 n • (n-1) • (n-2) •...• (n-r+1) ▶ 设 A 是 {a,b,c}, A 的可能排列为 3! = 3 × 2 × 1 = 6 种 ▶ 简化公式(排列公式)为: nPr = n! ÷ (n-r)! ▶ 在一个有n个对象的集合中,第 1 个对象出现 k[1] 次,第 2 个对象出现 k[2] 次,则该集合所形成的不同排列数为 n! ÷ ( k[1]! • k[2]! •...• k[t]! ) (其中 k[1] + k[2] + ... + k[t] = n) ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 组合的计数 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ▶ 设 A = {1,2,3,4}, 每次取三个时,A 的不同组合可以是: A[1] = {2,3,4} = {2,3,1} = {1,3,2} = {3,1,2} = {3,2,1} A[2] = {1,2,4} = {2,4,1} = {1,4,2} = {4,1,2} = {4,2,1} A[3] = ... ▶ 设 A 是一个集合且 |A| = n, 0 ≤ r ≤ n, 那么每次取 r 个时 A 的元素的组合数为: nCr = n! ÷ [ r! • (n-r)! ] 每次取 r 个时,A 的组合数不依赖于 A。 ▶ 例子:从一副 52 张纸牌中分发 5 张,则有 51! ÷ (5!47!) = 2598960 种组合方法 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 鸽巢原理(存在性证明) ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 如果你学过离散数学,就会学到这个原理的证明。如果 n 只鸽子被分配到 m 个鸽巢里,且 m < n, 那么至少有 1 个鸽巢含有 2 只或者更多只鸽子。 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 概率基础 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 样本空间 ( 硬币正反面作为样本排列: A = {HH, HT, TH, TT} ) 对事件赋予概率 “等可能” 的结果 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 递归关系 ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 比如斐波那契数列
📌 JavaScript
参考代码:index.js
/** * 元素的排列方式 * @param {Array} items - 需要组合的元素,使用数组表示 * @returns {Array} 所有组合的数组集合 */ export function permutations(items) { const results = []; /* 递归组合 */ function permute(arr, memo = []) { let currentArr; for (let i = 0; i < arr.length; i++) { /* 每次以被删除的元素作为数组初始值 */ currentArr = arr.splice(i, 1); if (arr.length === 0) { results.push(memo.concat(currentArr)); } /* 克隆一个被删除元素后的新数组, 直接将 arr 结果传入 permute() 函数也是可以的 */ const arrRes = arr.slice(); permute(arrRes, memo.concat(currentArr)); /* 注意:splice后的数组,会覆盖原来的数组,造成for或者forEach循环的次数减少, 因此我们要将删除的元素重新push进去 */ arr.splice(i, 0, currentArr[0]); } return results; } permute(items); return results; } /** * 元素的组合方式(取出元素没有考虑重复性) * @param {Array} arr - 输入数组 * @param {Number} quantity - 要取出的元素数量(用来组合) * @param {Number} index - arr 中的当前索引 * @param {Array} tempArr - 临时数组 * @param {Number} tempArrIndex - tempArr 中的当前索引 * @param {Array} result - 存储每次分组后的结果 * @returns {Array} 所有组合的数组集合 */ function combinationUtil(arr, quantity, index, tempArr, tempArrIndex, result) { const { length } = arr; /* 打印每组的结果 */ if (index == quantity) { const res = []; for (let j = 0; j < quantity; j++) { res.push(tempArr[j]); } result.push(res); return; } /* 当没有更多元素可以放入 tempArr 时 */ if (tempArrIndex >= length) { return; } /* 包含当前元素,原数组向下移位 */ tempArr[index] = arr[tempArrIndex]; combinationUtil(arr, quantity, index + 1, tempArr, tempArrIndex + 1, result); /* 排除当前元素,临时数组向下移位 */ combinationUtil(arr, quantity, index, tempArr, tempArrIndex + 1, result); return result; } export function combinations(arr, quantity) { const tempArr = new Array(quantity); return combinationUtil(arr, quantity, 0, tempArr, 0, []); }
测试:test.js
import { permutations, combinations } from './index'; /* 排列 [1, 2, 3] 这3个元素,它们有多少种排列方法(它们是排列是有序的) */ console.log(permutations([1, 2, 3])); /* 输出 [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ] */ /* [1, 2, 3, 4]四个元素取出2个,它们的组合为 */ console.log(combinations([1, 2, 3, 4], 2)); /* 输出: [ [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4] ] */