给定含有N个互不相同元素的序列,输出这N个元素的所有排列(全排列)。
以字符串"abc"为例,全排列包括"abc",“acb”,“bac”,“bca”,“cab”,“cba"这6个结果。
从排列组合来计算6=3!=3*2*1,也就是我们依次选择3个元素:
- 第一个元素有3种选择(a,b,c),假设选择c
- 第二个元素只有2种选择(a,b),假设选择b
- 第三个元素只有1种选择(a)
递归
首先我们采用递归(+回溯)的方式实现。
假设我们已经完成N-1个元素的全排列,那么怎么完成N个元素的全排列?
由上面的简介可知,N个元素的全排列结果数目等于N! = N*(N-1)!。
也就是说遍历任选N个元素中的一个+
剩下N-1个元素全排列。
版本一
代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
void permuteV1(char arr[], int size, int selIdx)
{
if (selIdx >= size)
{
for (int idx=0; idx<size; idx++)
{
cout<<arr[idx];
}
cout<<endl;
return;
}
for (int idx=selIdx; idx<size; idx++)
{
// selIdx之前为已选择元素,之后(包括selIdx)为待选择元素
// 从待选择元素从选一个交换到selIdx,产生新的全排列
swap(arr[selIdx], arr[idx]);
permuteV1(arr, size, selIdx+1);
// 上面选择了idx元素,接下来选择下一个待选择元素(idx+1),首先需要撤销选择idx元素
swap(arr[selIdx], arr[idx]);
}
}
|
对于"abc",上述代码输出结果为"abc","acb","bac","bca","cba","cab"。
细心的朋友可能会发现先输出"cba",然后再输出"cab"。
是的,上述代码输出次序并不是按照 字典序 。
版本二
在序列本身有序(升序)的前提下,把上面的swap改成如下操作即可实现字典序输出:
- 把[selIdx, idx)区间的元素整体后移一位
- 再把idx元素放到selIdx位置
代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
void selElem(char arr[], int selIdx, int idx)
{
int offset = idx-selIdx;
char backup = arr[idx];
memmove(arr+selIdx+1, arr+selIdx, offset);
arr[selIdx] = backup;
}
void unselElem(char arr[], int selIdx, int idx)
{
int offset = idx-selIdx;
char backup = arr[selIdx];
memmove(arr+selIdx, arr+selIdx+1, offset);
arr[idx] = backup;
}
void permuteV2(char arr[], int size, int selIdx)
{
if (selIdx >= size)
{
for (int idx=0; idx<size; idx++)
{
cout<<arr[idx];
}
cout<<endl;
return;
}
for (int idx=selIdx; idx<size; idx++)
{
// selIdx之前为已选择元素,之后(包括selIdx)为待选择元素
// 从待选择元素从选一个移到selIdx,产生新的排列
selElem(arr, selIdx, idx);
permuteV2(arr, size, selIdx+1);
// 上面选择了idx元素,接下来选择下一个待选择元素(idx+1),首先需要撤销选择idx元素
unselElem(arr, selIdx, idx);
}
}
|
当然从效率上来说,相比swap交换,上述代码效率有所降低。
非递归
要以非递归方式实现全排列,首先要解决的是根据当前排列得到下一个排列。
另外需要注意的是全排列中所有排列也是有顺序的,比较自然的想法就是某种意义上的"升序”。
以"abc"序列来说,“bac"应该在"bca"之前,因为这两个排列都以b开头,由后面的a和c决定顺序。
字典序
上述顺序其实就是 字典序,简单来说就是以字母顺序依次比较序列中元素,直到决出顺序。
那么如何得到字典序的下一个排列?算法如下:
- 从排列末尾寻找相邻升序子序列seq[i]<seq[i+1],如果不存在说明是最后的排列
- 从排列末尾寻找seq[i]<seq[j] (由上述步骤可知j一定存在)
- 交换seq[i]与seq[j]
- 反转seq[i+1]之后的元素
其实i+1元素前面是升序,后面是降序。
降序说明子序列已经遍历完,i元素需要更新更大的值,所以从右边选取。
更新完i元素之后反转是因为要开始新的一轮子序列全排列(降序反转后为升序)。
代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
int hasNextPermutation(char arr[], int size)
{
int i = size-2;
int j = 0;
// find seq[i]<seq[i+1]
while (0<=i)
{
if (arr[i] < arr[i+1])
{
break;
}
i--;
}
// not found
if (0>i)
{
return 0;
}
// find seq[j]>seq[i]
j=size-1;
while (arr[i] >= arr[j])
{
j--;
}
swap(arr[i], arr[j]);
reverse(arr+i+1, arr+size);
return 1;
}
void permuteIter(char arr[], int size)
{
do
{
for (int idx=0; idx<size; idx++)
{
cout<<arr[idx];
}
cout<<endl;
} while (hasNextPermutation(arr, size));
}
|
STL之next_permutation
实际上STL已经提供了这样的算法,通过next_permutation可以获取下一个排列。
示例代码如下:
1
2
3
4
5
6
7
|
void permuteStl(string& str)
{
do
{
cout<<str<<endl;
} while(next_permutation(str.begin(), str.end()));
}
|
变种
上述讨论的序列中元素是互不相同的,其中一种变种就是允许重复元素。
由于非递归采用的是字典序保持"升序”,所以不存在问题。
而递归方法会重复打印,需要调整为在待选择元素中选择下一个与当前选择不同的元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
void permuteV2dup(char arr[], int size, int selIdx)
{
if (selIdx >= size)
{
for (int idx=0; idx<size; idx++)
{
cout<<arr[idx];
}
cout<<" ";
return;
}
for (int idx=selIdx; idx<size; idx++)
{
// selIdx之前为已选择元素,之后(包括selIdx)为待选择元素
// 从待选择元素从选一个移到selIdx,产生新的排列
selElem(arr, selIdx, idx);
permuteV2dup(arr, size, selIdx+1);
// 上面选择了idx元素,接下来选择下一个待选择元素(idx+1),首先需要撤销选择idx元素
unselElem(arr, selIdx, idx);
// 选择下一个与当前选择不同的元素
while (idx+1<size && arr[idx]==arr[idx+1])
{
idx++;
}
}
}
|