递归——全排列问题(Full Permutation)
废话少说,首先看定义:
全排列(Full Permutation),一般把1~n这n个整数按某个顺序摆放的结果称为这n个整数的一个排列 ,而全排列指这n个整数能形成的所有排列。例如对1、2、3这三个整数来说, (1, 2, 3)、(1, 3, 2)、(2, 1, 3)、(2, 3, 1)、(3, 1, 2)、(3, 2, 1)就是1~3的全排列。
现在需要实现按字典序从小到大的顺序输出 1~n 的全排列,其中 (a1, a2, …, an) 的字典序小于 (b1, b2, …, bn) 是指存在一个 i ,使得 a1=b1, a2=b2, …, a(i-1) = b(i-1)、ai<bi 成立。因此上面n=3的例子就是按字典序从小到大的顺序给出的。
按字典序排列用大白话的意思就是:任取排列结果中的两个元素作比较,例如(2, 1, 3)和(2, 3, 1),两个元素的第一位相等都是2(比较下一位),第二位 1 < 3,所以按从小到大的顺序排列时, (2, 1, 3)一定排在(2, 3, 1)前面。
用递归的角度去考虑,如果把问题描述成
”输出1~n这n个整数的全排列“
那么它就可以被分为若干个子问题:
”输出以1开头的全排列“、”输出以2开头的全排列“ … ”输出以n开头的全排列“。
在实现代码前,我们需梳理一下重要逻辑:
- 我们需要一个int型数组 P, 来存放当前的排列;
- 排列中的元素不能重复,例如1~3的全排列,是不能出现(1,2,2)这样的元素的,每个数字能且仅能出现一次。于是我们可以设定一个bool型的数组 Table[x],数组的下标代表当前的数字,默认值为false,当整数x已经在当前排列中使用过的时候,我们就将 Table[x] 的值设置为true。
- 当我们按顺序往P的第1位到第n位中填入数字的时候,不妨假设当前已经填好了P[1] ~ P[index-1],正准备填P[index]。显然我们需要for循环遍历1~n,来确定当前的数字x是否在已经枚举的数字中出现过(即Table[x] == false)。若出现过,我们就跳过这个数字去看下一个数字Table[x+1],若没有出现过,就把他填入P[index],同时将Table[x]的值置为true,然后进入递归去处理第index+1位;当排列的全部递归完成时,再将Table[x]的所有值还原为false,以便下一次for循环的判断。
- 在递归进行的最后的时候,我们需要规定一个跳出的边界的条件。当P的index到达 n+1 时,说明P的第1~n位都已经填好了,此时便可跳出递归。
如果你已经理解了上述的逻辑推理,可以试着自己先用代码实现一下,如果还未理解,一定要多看几遍,自己思考之后再往下看。
注释很重要,请务必认真阅读。
#include <cstdio>
const int maxn = 11;
int n; //计算1~n的全排列
int P[maxn]; //P为当前排列
bool table[maxn] = {false}; //table记录整数x是否已经在P中
void generateP(int index) {
//首先判断是否已经到达了递归边界
if(index == n + 1) { //递归边界,已经处理完成排列的1~n位
for(int i = 1; i <= n; i++) {
printf("%d", P[i]); //输出当前排列
}
printf("\n");
return;
}
//如若还没有到达边界,开始遍历table数组
for(int x = 1; x <= n; x++) {
if(table[x] == false) { //判断当前数字x是否已被使用,若没有
P[index] = x; //令P的第index位为x,即=把x加入当前排列
table[x] = true; //标记数字x已被使用
generateP(index + 1); //递归调用,处理排列的第index+1号位
table[x] = false; //全部梳理完后,递归依次弹出,还原x的状态
}
}
}
int main() {
n = 3; //欲输出1~3的全排列
generateP(1); //从P[1]开始填(递归调用)
return 0;
}
如果你有认真阅读上述的逻辑推理,相信你一定写的出和我类似的代码。
每日进步一点点,
我们下篇再见,
共勉。