面试题28:字符串的排列

来源:互联网 发布:c 棋牌游戏源码 编辑:程序博客网 时间:2024/05/16 17: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