剑指Offer38 - 字符串的排列 - [DFS]

题目:

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

 

思路:

一个很直观的思路,对于任意一个字符串,例如"abcde",我首先枚举选定首字符,例如先选定了首字符是"a",那剩下部分的怎么permutation的事情就交给函数permutation("bcde")好了。

一旦我得到了permutation("bcde")的结果,我只要把所有的"bcde"的排列前面都拼接上一个字符"a"即可。

然后,选定首字符是"b",剩下的工作交给permutation("acde")。

再然后,选定首字符是"c",剩下的工作交给permutation("abde")。

以上一个主干思路。

 

然后需要注意的一个小的问题是,permutation结果不能出现重复,上面的思路在遇到"aacbb"这种字符串里有重复字符的时候会导致permutation结果重复。

显然,我们只需要选定首字符时做一下筛选,首字符选"a"只选一次,选"b"的时候也只选一次,即可。

这个的话,考虑字符串不一定只有小写字母,因此可以先对"aacbb"做一次排序,得到"aabbc",然后遍历选取首字母的时候跳过重复的即可。

 

Code:

class Solution
{
public:
    vector<string> permutation(string s)
    {
        vector<string> res = {};
        if (s.empty())
            return res;
        
        sort(s.begin(), s.end());
        return perm(s);
    }

    vector<string> perm(const string &s)
    {
        vector<string> res = {};
        if (s.length() == 1)
        {
            res.push_back(s);
            return res;
        }

        char previous = s[0] + 1;
        for (int i = 0; i < s.length(); i++)
        {
            if (s[i] == previous)
                continue;
            
            string other = s.substr(0, i) + s.substr(i + 1);
            vector<string> temp_result = perm(other);
            for (int j = 0; j < temp_result.size(); j++)
                res.push_back(s[i] + temp_result[j]);
            temp_result.clear();
            
            previous = s[i];
        }
        return res;
    }
};

 

时间复杂度:

在最开始需要做一个$O(n \log n)$的排序。

后面的递归,记permutation长度为n的字符串的时间复杂度为$f(n)$,

有 $f(n) = n \cdot f(n - 1)$,根据 $f(1) = 1$,易得 $f(n) = n!$。

所以时间复杂度 $O(n \log n + n!) = O(n!)$。

 


 

思路2:

参考官方的题解,通过一个传引用的string& perm来实时维护目前的“填字符”进度。

最开始是空的字符串,表示还未开始填字符,然后遍历尝试往其中填入第一个字符,被选中填入的字符要打上标记,

当换到下一个填入时记得把之前那个pop掉,并且取消标记。

 

当然,避免重复还是上面的老办法,用一个previous变量记录一下。

 

Code:

class Solution
{
public:
    int n;              // 输入字符串的长度
    vector<string> res; // 存储permutation结果
    vector<bool> vis;   // 标记数组

    vector<string> permutation(string s)
    {
        res.clear();

        if (s.empty())
            return res;

        n = s.length();
        vis.resize(n);
        vis.clear();

        sort(s.begin(), s.end());

        string start = "";
        dfs(s, start);
        return res;
    }

    void dfs(const string &s, string &perm)
    {
        if (perm.length() == n)
        {
            // 此时的perm已经是一个可行的排列
            res.push_back(perm);
            return;
        }

        // perm.length() < n
        // 还需要继续往perm后添加字符
        char previous = s[n - 1] + 1; // 使其避免和第一个能被用的s[i]一样即可
        for (int i = 0; i < n; i++)
        {
            if (vis[i]) // 已经用过的显然要直接跳过
                continue;

            if (previous == s[i]) // 之前已经出现过则不能再用
                continue;
            else
                previous = s[i];

            vis[i] = true;
            perm.push_back(s[i]);
            dfs(s, perm);
            perm.pop_back();
            vis[i] = false;
        }
        return;
    }
};

 

时间复杂度:

每次产生一个可行的排列需要 $O(n)$,可行的排列个数 $O(n!)$,时间复杂度 $O(n \cdot n!)$。

讲道理,这个做法和我自己写那个的时间复杂度是一样的,但是我那个耗时和内存消耗都挺大。

应该是函数直接返回vector,以及常数大的缘故。

 


 

因此,我尝试了把返回vector<string>改成传引用:

class Solution
{
public:
    vector<string> permutation(string s)
    {
        vector<string> res = {};
        if (s.empty())
            return res;
        
        sort(s.begin(), s.end());
        perm(s, res);
        return res;
    }

    void perm(const string &s, vector<string> &res)
    {
        if (s.length() == 1)
        {
            res.push_back(s);
            return;
        }

        char previous = s[0] + 1;
        for (int i = 0; i < s.length(); i++)
        {
            if (s[i] == previous)
                continue;
            
            string other = s.substr(0, i) + s.substr(i + 1);
            int pre_size = res.size();
            perm(other, res);
            for (int j = pre_size; j < res.size(); j++)
                res[j] = s[i] + res[j];
            
            previous = s[i];
        }

        return;
    }
};

可以看到,内存消耗已经降下来了,但是耗时还是很大,应该是都花在遍历vector上面了。

然后我只好给vector<string>预分配一定的空间来提升速度:

class Solution
{
public:
    vector<string> permutation(string s)
    {
        vector<string> res = {};
        if (s.empty())
            return res;
        
        int sz_for_rsz = s.length();
        for (int i = 1; i <= s.length(); i++)
            sz_for_rsz *= i;
        res.reserve(sz_for_rsz);

        sort(s.begin(), s.end());
        perm(s, res);
        return res;
    }

    void perm(const string &s, vector<string> &res)
    {
        if (s.length() == 1)
        {
            res.push_back(s);
            return;
        }

        char previous = s[0] + 1;
        for (int i = 0; i < s.length(); i++)
        {
            if (s[i] == previous)
                continue;
            
            string other = s.substr(0, i) + s.substr(i + 1);
            int pre_size = res.size();
            perm(other, res);
            for (int j = pre_size; j < res.size(); j++)
                res[j] = s[i] + res[j];
            
            previous = s[i];
        }

        return;
    }
};

运行结果:

看来我这个预分配的空间卡的还算可以,没有涨特别多的内存消耗,耗时也是进一步缩短了。

那最后导致常数大的原因,应该就只剩下字符串的拼接和取子串的操作了。

因此,我又简单改了一下,把 res[j] = s[i] + res[j]; 变成 res[j] += s[i]; ,因为实际上对于permutation来说,反转一下字符串没有任何区别:

class Solution
{
public:
    vector<string> permutation(string s)
    {
        vector<string> res = {};
        if (s.empty())
            return res;
        
        int sz_for_rsz = s.length();
        for (int i = 1; i <= s.length(); i++)
            sz_for_rsz *= i;
        res.reserve(sz_for_rsz);

        sort(s.begin(), s.end());
        perm(s, res);
        return res;
    }

    void perm(const string &s, vector<string> &res)
    {
        if (s.length() == 1)
        {
            res.push_back(s);
            return;
        }

        char previous = s[0] + 1;
        for (int i = 0; i < s.length(); i++)
        {
            if (s[i] == previous)
                continue;
            
            string other = s.substr(0, i) + s.substr(i + 1);
            int pre_size = res.size();
            perm(other, res);
            for (int j = pre_size; j < res.size(); j++)
                res[j] += s[i];
            
            previous = s[i];
        }

        return;
    }
};
View Code

 

算是差不多了,不改准备再改了,再改的话就要对substr() + substr()那一句动刀子了,这样的话要改动的代码量就很大了,而且用vis数组去标记的话,改完其实已经和官方给的题解在思路上基本如出一辙了,没什么大的区别,因此就不改了罢。

 

posted @ 2021-06-22 17:33  Dilthey  阅读(61)  评论(0编辑  收藏  举报