给定一组数据,列出所有可能出现的排列情况。
排列特点:内部元素是有序的,即 [1,2,3] 和 [3,2,1] 即使元素相同,但位置不同,也是不同的排列情况
常用的求全排列有如下两种方法:回溯法、字典排序
回溯法
回溯法(无重复元素)
题目:存在 n 个不重复元素,求 n 个元素的排列
举个例子来说明,假设存在编号为 1、2、3 的三个球,分别放入编号为 A、B、C 的三个位置,那么存在多少种放置方法?
-
第一步:可以将 1 放置于 A,那么还剩下 2、3
- 第二步:可以将 2 放置于 B,那么还剩下 3
- 第三步:可以将 3 放置于 C,此时完成一次排列
- 第二步:同样可以将 3 放置于 B,那么还剩下 2
- 第三步:可以将 2 放置于 C,此时完成一次排列
- 第二步:可以将 2 放置于 B,那么还剩下 3
-
第一步:可以将 2 放置于 A,那么还剩下 1、3
-
第二步:可以将 1 放置于 B,那么还剩下 3
-
第三步:可以将 3 放置于 C,此时完成一次排列
...
-
-
由此不难看出,对于每一个位置,每个球都可以去争夺一次,即第 A 个位置,由 1、2、3 颗球同时争夺;而对于第 B 个位置,就由剩余的球去争夺(例如 2、3 球);而第 C 个位置,就由最后一个球占据(例如 C)球
由上面的分析,可以得出下面的思路:
- 对于每个位置 i,每个球都可以去争夺,那么就 循环剩余的球,让每一颗球都匹配一次该位置
- 当有个球占据了 i 后,就该剩余的球去争夺下一个位置了 i+1,其实也就是回到了步骤 1
- 当所有球耗尽后,即完成一次排列
根据上面的思路编码
function fullArrangement(nums: number[]): number[][] {
const ans: number[][] = []; // 最终的排列结果
recursion(0);
return ans;
function recursion(emptyIndex: number): void {
// emptyIndex 表示当前空的格子索引,可以放入一个元素
// 当 emptyIndex到数组末尾时,由于此时只剩下一个元素和一个格子,只可能存在一种排列,即此时无需在进行循环了
if (emptyIndex === nums.length - 1) {
ans.push(nums.slice());
return;
}
// 把 emptyIndex 后续的每一位都和当前位进行交换,相当于把剩余的球每个都匹配一次该位置
for (let i = emptyIndex; i < nums.length; i++) {
swap(nums, emptyIndex, i);
recursion(emptyIndex + 1);
swap(nums, emptyIndex, i); // 排完之后,需要还原,再让其他的球来匹配该位置
}
}
}
function swap(nums: number[], i: number, j: number): void {
const s = nums[i];
nums[i] = nums[j];
nums[j] = s;
}
回溯(重复元素)
题目:存在 n 个重复的元素,求 n 个元素的全排列
举个例子来说明,假设存在编号为 1、1、2 的三个球,分别放入编号为 A、B、C 的三个位置,那么存在多少种放置方法?
如果还是按照上面的方法分析的话,会生成如下结果
可以很直观的看到,出现了重复的排列,例如[1,1,2]出现了两次,那么想个办法记录下第 i 个位置,已经放入过的元素,对于已经在该位置出现过的元素了,就不再重复计算了,例如记录 A 位置已经出现过 1 了,那么下次 1 再出现时,略过即可
根据上面的思路编码
function fullArrangement(nums: number[]): number[][] {
const ans: number[][] = []; // 最终的排列结果
recursion(0);
return ans;
function recursion(emptyIndex: number): void {
// emptyIndex 表示当前空的格子索引,可以放入一个元素
// 当 emptyIndex到数组末尾时,由于此时只剩下一个元素和一个格子,只可能存在一种排列,即此时无序在进行循环了
if (emptyIndex === nums.length - 1) {
ans.push(nums.slice());
return;
}
const repeatSet = new Set<number>(); // 记录当前第i个位置已经出现过的元素,对于重复元素,不做计算
// 把 emptyIndex 后续的每一位都和当前位进行交换,相当于把剩余的球每个都匹配一次该位置
for (let i = emptyIndex; i < nums.length; i++) {
if (repeatSet.has(nums[i])) continue;
swap(nums, emptyIndex, i);
recursion(emptyIndex + 1);
swap(nums, emptyIndex, i); // 排完之后,需要还原,再让其他的球来匹配该位置
repeatSet.add(nums[i]);
}
}
}
function swap(nums: number[], i: number, j: number): void {
const s = nums[i];
nums[i] = nums[j];
nums[j] = s;
}
字典排序
字典排序原理:每一个字符都对应一个值,就拿 [1,2,3] 来说,它的值就是 1、2、3,可以组成最小的数值为 123,最大的数值为 321,那么所有的组合可能性都是从 123 起始,直到 321 结束,寻找完这些值,即为全排列。
确定两个值的比较方式
既然值需要比较大小,那么肯定有比较的方式,对于此处来说,要求返回的一个数组,那么用数组来表示一次排列最为合适,但是数组没有办法向数字这样直接进行比较,所以封装一个如下的比较方法
// nums1 是否小于 nums2
function compareLT(nums1: number[], nums2: number[]): boolean {
// 逐级比较
for (let i = 0; i < nums1.length; i++) {
if (nums1[i] === nums2[i]) continue;
return nums1[i] < nums2[i];
}
return false;
}
确定值的递增方式
既然是寻找 [最小值,最大值] 的所有排列,那么肯定有值递增的方式,使用如下步骤来递增值,并以 [1,2,3,1] 来举例
-
从末位(index=nums.length-1)向前(index=0)寻找,寻找到第一个递减的值,记录该值的位置为 i,值为 n;从 index 为 3 往前寻找,当寻找到 index 为 1,value 为 2 时,找到第一个递减的值,此时 i = 1,n = 2
-
从 i + 1 位向后寻找,找到最后一个比 n 大的数,记为该值的位置为 j;从 index 为 2 向后寻找,当 index 为 2,value 为 3 时,发现了最后一个比 n 大的数,此时 j 为 2
-
交换 i,j 的值;交换 1、2 的位置 ==> [1,3,2,1]
-
翻转i+1到nums.length-1的所有值,即交换
-
i + 1 和 nums.length - 1 交换
-
i + 2 和 nums.length - 2 交换
...
从 index 为 2 开始,到 index 为 3 截止,进行翻转 ==> [1,3,1,2]
-
function stepAdd(nums: number[]): number[] {
const lastIndex = nums.length - 1;
let max = nums[lastIndex];
let firstCutIndex = -1; // 第一个递减数的索引
let lastBigNumberIndex = -1; // 最后一位比n大的树的索引
// 从后往前寻找递减的数
for (let i = lastIndex - 1; i >= 0; i--) {
if (nums[i] < max) {
firstCutIndex = i;
break;
} else {
max = Math.max(max, nums[i]);
}
}
if (firstCutIndex === -1) return nums; // 已经是最大了
// 否则从firstCutIndex开始寻找,比firstCutIndex大的数,记住是大的数中最后一位
for (let i = firstCutIndex + 1; i <= lastIndex; i++) {
if (nums[i] > nums[firstCutIndex]) {
lastBigNumberIndex = i;
}
}
// 调换 changeIndex和firstCutIndex的值
nums = nums.slice();
swap(nums, firstCutIndex, lastBigNumberIndex);
// 现在对cutIndex后的进行翻转
let i = firstCutIndex + 1,
j = lastIndex;
while (i < j) {
swap(nums, i, j);
i++;
j--;
}
return nums;
}
function swap(nums: number[], i: number, j: number): void {
const s = nums[i];
nums[i] = nums[j];
nums[j] = s;
}
已找出值比较的方法以及值递增的方法,则进行穷举,且由于是对元素进行比较,不会出现对元素重复计算
function fullArrangement(nums: number[]): number[][] {
const result: number[][] = [];
let value = nums.sort((a, b) => a - b);
const maxValue = value.slice().reverse();
while (compareLT(value, maxValue)) {
result.push(value);
value = stepAdd(value);
}
result.push(value);
return result;
}