• 剑指Offer38


    题目:

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

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

    思路:

    一个很直观的思路,对于任意一个字符串,例如"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数组去标记的话,改完其实已经和官方给的题解在思路上基本如出一辙了,没什么大的区别,因此就不改了罢。

    转载请注明出处:https://dilthey.cnblogs.com/
  • 相关阅读:
    使用RedisTemplate的操作类访问Redis(转载)
    Redis集群方案(来自网络)
    Scale-up and Scale-out(转载)
    数据结构和算法分析 优先队列
    数据结构和算法分析 排序
    linux下的常用命令
    Java HashMap的工作原理(转载)
    二叉查找树(转载)
    Redis实现之对象(三)
    Redis实现之对象(二)
  • 原文地址:https://www.cnblogs.com/dilthey/p/14919557.html
Copyright © 2020-2023  润新知