上次写了全排列的递归实现 ,简单分析了下时间和空间复杂度。元素个数比较多的时候递归实现的开销导致它几乎没有实用性了吧。学习过一些前辈总结的非递归实现后,觉得有必要把它记录下来。这里尽量讲清楚些,也算是对自己文字表达能力的训练吧。
全排列概念: 给定n个元素,输出所有不同的排列情况。
非递归原理 找到全排列中字典序最小的一个排列,然后找到字典序只 比当前排列大的下一个排列,依次查找下去,一直到没有新的排列大于当前排列。
非递归实现算法 这里用一个int数组作为例子辅助说明。 [4, 6, 7, 2, 3, 1, 9]
对数组进行升序排序。 得到全排列中字典序最小的一个排列:[1, 2, 3, 4 ,6, 7, 9]
从后往前遍历,找到从最后一个元素往前最大的升序子串。假设该子串为 [k, n - 1],那么也可以理解为k到n-1是递减的。
如果k == 0,就例子而言,是这样的:[9, 7, 6, 4, 3, 2, 1]。那么其实我们已经找到字典序最大的一个排列,可以结束查找了。
如果k > 0,比方说[1, 2, 3, 4, 9, 7, 6],k = 5(值是9)。显然还有字典序更大的排列,转3
将[k, n - 1]区间中大于arr[k - 1]的数中最小的一个与 k - 1 位置的数对换,比方说[1, 2, 3, 4, 9, 7, 6] 变成了 [1, 2, 3, 6, 9, 7, 4],这里6是区间中满足条件的数。再将区间[k, n -1]做升序排列,变成[1, 2, 3, 6, 4, 7, 9]。 这里[1, 2, 3, 4, 9, 7, 6] 的下一个排列就是[1, 2, 3, 6, 4, 7, 9]。
重复2,3直到k == 0。
对算法的简单推理和证明 由于我们是从字典序最小的排列开始,一直到字典序最大的排列结束。那么我们只要证明:上面这个算法,它对一个排列,每次总是能得到字典序只 比当前排列大的一个新的排列。就可以证明它的完备性了。 依然用[1, 2, 3, 4, 9, 7, 6] 辅助讲解。 对于这样的一个区间,我们把它划分成3个大区间。 A = [1, 2, 3], arr[0]~arr[k - 2] B = [4], arr[k - 1] C = [9, 7, 6], arr[k]~arr[n -1] C 代表从最后一个元素往前数最大的升序子串[k, n - 1],B 是区间[k, n - 1]左边那个数–>arr[k - 1], A 是 [0, k - 2]。A,B,C串起来是完整的一个排列。
再令到 D = B + C = [4, 9, 7, 6],区间[k - 1, n - 1]。注意A和D串起来跟A,B, C串起来是同一个排列。
那么我们通过算法中的步骤2,找到D这个区间。 现在我们先来证明为什么只需要调整D区间,不需要调整A区间就能获得一个比原排列大的排列。 D区间中,第一个数是arr[k - 1],我们总能在C区间中找到至少一个数arr[k+i],比arr[k - 1]要大。那么把arr[k+i]与arr[k - 1]对调的话,就能在D区间上得到一个更大的排列。 为什么不能调整到A区间的元素呢?因为我们希望得到更大的字典序,所以只能对A调整使得A这一部分的字典序大于它原来的排列。但这种方式得到的排列,字典序必然大于只调整D区间的元素,这里读者自己简单推理下就明白了。 就例子而言:我们希望得到的排列是这样的 :[1, 2, 3, ?, ?, ?, ? ] ,?号备选的数是:4, 6, 7, 9
然后来证明为何将C区间中的比arr[k - 1]大的数中的最小的一个与arr[k - 1]对调。 下面请时刻注意arr[k - 1]是D区间的开头,并且通常说到“排列”二字,是A区间不变,对D区间进行调整的排列。 D区间的元素可以分为3种类型,顺便说明下哪种类型能放到arr[k - 1]这个位置
小于arr[k - 1]的元素,这部分元素中的任一个调整到D区间的开头,得到的排列(D区间)都是比arr[k - 1]开头的排列要小的。不用考虑了。
等于arr[k - 1]的元素,注意我们是怎么找到C区间的,就可以知道D区间当前的排列其实是以arr[k - 1]开头的排列中最大的一个,不存在A不变,D区间以arr[k - 1]开头的更大的排列。
大于arr[k - 1]的元素,如果是希望找到最接近原排列的下一个排列,当然是找这其中最小的一个元素放到arr[k - 1]上啦。就例子来说,这个数是[6], 就例子而言:我们希望得到的排列是这样的:[1, 2, 3, 6, ?, ?, ?],?号备选的数是:4, 7 , 9
现在我们已经把范围缩小到一定程度了。(其实你往前回顾可以看到我一直在缩小可能的区间,使得候选的排列越来越少,并且他们总是最接近原排列的)。 很显然只要把备选的部分[4, 7, 9]作升序排列并到尾部,就可以得到以[1, 2, 3, 6]开头的最小的排列了。 最后结果就是[1, 2, 3, 6, 4, 7, 9]。是字典序大于[1, 2, 3, 4, 9, 7, 6]的排列中最小的一个。
证毕。
上代码 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
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std ;
void print_arr (vector <int >& arr, int n) {
int i;
for (i = 0 ; i < n; i++) {
cout << arr[i] << " " ;
}
cout << endl ;
}
int main () {
int n;
int i, k;
int tem;
vector <int > arr;
while (cin >> n) {
arr.resize(n);
for (i = 0 ; i < n; i++)
cin >> arr[i];
sort(arr.begin(), arr.end());
print_arr(arr, n);
while (true ) {
for (k = n - 2 ; k >= 0 ; k--) {
if (arr[k] < arr[k + 1 ]) {
break ;
}
}
if (k < 0 ) {
break ;
}
k += 1 ;
for (i = n - 1 ; i >= k; i--) {
if (arr[i] > arr[k - 1 ]) {
break ;
}
}
swap(arr[k - 1 ], arr[i]);
for (i = k; i <= (k + (n - 1 - k) / 2 ); i++) {
swap(arr[i], arr[n - 1 + k - i]);
};
print_arr(arr, n);
}
}
return 0 ;
}
对比和分析 注意到非递归求全排列,是不会出现重复的排列的。
非递归实现全排列: 空间复杂度:O(n!) 时间复杂度:O(?) < O(n!n) 说实话这种情况我不会求,先给个范围,做个记号,后面回来修改。 为什么小于 O(n!n)呢?找到最长递减序列,最坏的情况也不过是最后一个数到第一个 –> O(n),逆序也是O(n)。何况并不总是这样,因此不会超过O(n!n)。
递归实现全排列: 空间复杂度:O(n!) 时间复杂度:O(n!nlogn) 去重的情况
非递归没有函数栈,这个优势是很巨大的,同时理论时间复杂度更小。是一种更好的求全排列的方法。
终。