面试题28:字符串的排列

面试题28:字符串的排列

题目描述:
输入一个字符串,返回该字符串中字符的全排列。
题目分析:
再来看这道题时,分外“眼红”,当年此题竟然没有做出来。
这道题,可以说用分治的思想,将字符串分成两个子问题,子问题1太小不需要再求解,只需要求解子问题2。当然,也可以理解为递归,《算法导论》讲动态规划章节时,有以这种朴素递归为入口来叙述。

1. 递归解法

思路分析1:
递归,将字符串分为两部分,第一个字符和剩余部分的字符串,先确定第一个字符求出对应的全排列,再将第一个字符逐个和后面的字符交换。
该题的关键是:

  1. 递归结束的条件;
  2. 如果有重复的字符,如何处理,避免交换;
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] 的下一个排列的描述:

  1. 求i = max{j | p[j] < p[j + 1]}(找最后一个正序)
  2. 求j = max{k | p[i] < p[k]}(找最后一个大于p[i]的p[k])
    (找大于p[i]的是因为小于p[i]的序列都已经求过)
  3. 交换 p[i] 与 p[j]得到 p[1] … p[i-1] p[j] p[i+1] … p[j-1] p[i] p[j+1] … p[n]
  4. 反转 p[j] 后面的数得到 p[1] … p[i-1] p[j] p[n] … p[j+1] p[i] p[j-1] … p[i+1]

给一个具体的例子来解释上述4步:
求排列8347521的下一个排列:

  1. 求i,第一个正序47,i = 3
  2. 求j,最后一个大于4的是5,j = 5
  3. 交换p[i]和p[j]得,8357421
  4. 将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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值