面试题28:字符串的排列
题目描述:
输入一个字符串,返回该字符串中字符的全排列。
题目分析:
再来看这道题时,分外“眼红”,当年此题竟然没有做出来。
这道题,可以说用分治的思想,将字符串分成两个子问题,子问题1太小不需要再求解,只需要求解子问题2。当然,也可以理解为递归,《算法导论》讲动态规划章节时,有以这种朴素递归为入口来叙述。
1. 递归解法
思路分析1:
递归,将字符串分为两部分,第一个字符和剩余部分的字符串,先确定第一个字符求出对应的全排列,再将第一个字符逐个和后面的字符交换。
该题的关键是:
- 递归结束的条件;
- 如果有重复的字符,如何处理,避免交换;
class Solution {
public:
void Permu(vector<string> &res, string &str, int left) {
/* string字符串对象也会加'\0',作为字符串的结尾 */
if (str[left] == '\0') {
res.push_back(str);
return;
}
for (int mov = left; str[mov] != '\0'; mov ++) {
/*
* if判断去重,如果是重复字符则不需要交换,
* 但第一个字符还是要交换的。
*/
if (mov == left || str[mov] != str[left]) {
swap(str[mov], str[left]);
Permu(res, str, left + 1);
swap(str[mov], str[left]);
}
}
}
vector<string> Permutation(string str) {
vector<string> res;
if (str.length() < 1)
return res;
Permu(res, str, 0);
/* 牛客网要求按字典序输出,所以多加一个排序 */
sort(res.begin(), res.end());
return res;
}
};
2. 非递归解法
思路分析2:
按照字典序排序的思想逐一生成所有序列。
求 p[1…n] 的下一个排列的描述:
- 求i = max{j | p[j] < p[j + 1]}(找最后一个正序)
- 求j = max{k | p[i] < p[k]}(找最后一个大于p[i]的p[k])
(找大于p[i]的是因为小于p[i]的序列都已经求过) - 交换 p[i] 与 p[j]得到 p[1] … p[i-1] p[j] p[i+1] … p[j-1] p[i] p[j+1] … p[n]
- 反转 p[j] 后面的数得到 p[1] … p[i-1] p[j] p[n] … p[j+1] p[i] p[j-1] … p[i+1]
给一个具体的例子来解释上述4步:
求排列8347521的下一个排列:
- 求i,第一个正序47,i = 3
- 求j,最后一个大于4的是5,j = 5
- 交换p[i]和p[j]得,8357421
- 将p[j]后面的数反转得,8351247
代码如下:
class Solution {
public:
vector<string> Permutation(string str) {
vector<string> res;
int len = str.size();
if (len == 0)
return res;
sort(str.begin(), str.end());
int i, j;
while (1) {
res.push_back(str);
for (i = len - 2; i >= 0; i --) {
if (str[i] < str[i + 1]) {
for (j = len - 1; j > i; j --)
if (str[j] > str[i])
break;
swap(str[i], str[j]);
ReverseStr(str, i + 1, len - 1);
break;
}
}
if (i == -1)
break;
}
return res;
}
void ReverseStr(string &str, int left, int right) {
while (left < right) {
swap(str[left], str[right]);
++ left;
-- right;
}
}
};
非递归实现参考:
http://liangjiabin.com/blog/2015/04/leetcode-permutation-generation-algorithm.html (有改动)
3. 该题的引申—康托公式
忘了是XXX公司的笔试题,给定一个字符串S,再给一个字符串T,求字符串T是字符串S是按照字典序输出的第几个字符串。
常规解法:依次求出字符串S的字典序的序列,统计个数,分别和字符串T比较,如果相等,返回统计的个数。
应用康托公式就会简单许多。
但是康托公式展开不能有重复值。
3.1 康托公式和康托展开
康托公式:
X=a[n](n-1)!+a[n-1](n-2)!+…+a[i]*(i-1)!+…+a[1]*0!
其中,a[i]为整数,并且0<= a[i] < i, 1<=i<=n。
康托展开和逆展开:
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
举例解释康托展开的过程:
例如,3 5 7 4 1 2 9 6 8 展开为 94。
因为X=2*8!+3*7!+4*6!+2*5!+0*4!+0*3!+2*2!+0*1!+0*0!=94.
解释:
排列的第一位是3,比3小的数有两个,以这样的数开始的排列有8!个,因此第一项为2*8!
排列的第二位是5,比5小的数有1、2、3、4,由于3已经出现,因此共有3个比5小的数,这样的排列有7!个,因此第二项为3*7!
以此类推,直至0*0!
3.2 康托展开
题目:给定字符串S=“abcdefgh”,任意给定一个字符串S的排列T,求T是S的字典序顺序的第几个排列。
思路分析:根据康托公式计算康托值,从第一位开始s[0],比s[0]小的有k个,则k * 7!,依次计算得出康托值。
代码如下:
/* 求小于t[i]的字母的个数 */
int Less(string t, int index) {
int res = 0;
res = t[index] - 'a';
if (index == 0) {
return res;
} else {
for (int j = 0; j < index; j ++)
if (t[j] < t[index])
-- res;
}
return res;
}
int Fib(int n) {
if (n == 0 || n == 1)
return 1;
int f0 = 1;
int f1 = 1;
int f2;
for (int i = 2; i <= n; i ++) {
f2 = f0 + f1;
f0 = f1;
f1 = f2;
}
return f2;
}
/* 计算康托值 */
int Cantor(string t, int len) {
int i;
int count = 0;
for (i = 0; i < len; i ++) {
int val = Less(t, i) * Fib(len - i - 1);
count += val;
}
return count;
}
3.3 康托逆展开
题目:给定字符串S=“abcdefgh”和康托值n,求对应的字符串T。
思路分析:康托展开是一个双射,,自然可以通过康托值求字符串全排列中第n大的排列。
给一个例子就容易理解多了。
如n=5,x=96时:
首先用96-1得到95,说明x之前有95个排列.(将此数本身减去!)
用95去除4! 得到3余23,说明有3个数比第1位小,所以第一位是4.
用23去除3! 得到3余5,说明有3个数比第2位小,所以是4,但是4已出现过,因此是5.
用5去除2!得到2余1,类似地,这一位是3.
用1去除1!得到1余0,这一位是2.
最后一位只能是1.
所以这个数是45321.
参考:
[1] http://www.lxway.com/448528981.htm
[2] http://comzyh.com/blog/archives/92/