剑指Offer38.字符串的排列

  • 剑指Offer38.字符串的排列

  • 题目:
    对于一个可能有重复字符的字符串s,得到它的全排列;
    全排列需要不重不漏;

  • 思路:
    1.库函数next_permutation:
    该库函数不管s有没有重复元素,都可以用;

class Solution {
    
    
public:
    vector<string> permutation(string s) {
    
    
        vector<string> res;
        sort(s.begin(),s.end());
        do{
    
    
            res.push_back(s);
        }while(next_permutation(s.begin(),s.end()));
        return res;


    }
};

2.树层剪枝:
用state代替常规的used,进一步优化空间复杂度;
递归函数dfs作用是:遍历s,尝试将每个元素放到path的该位置上,与之对应的state表示的是s的哪些元素用过了;

class Solution {
    
    
private:
    vector<string> ans;
    string path;
    void dfs(string& s, int state) {
    
    //用int state代替vector<bool> used
        if (path.size() == s.size()) {
    
    
            ans.push_back(path);
            return;
        }
        for (int i = 0; i < s.size(); ++i) {
    
    
        	// used[i - 1] == true,说明同一树支nums[i - 1]使用过
            // used[i - 1] == false,说明同一树层nums[i - 1]使用过
            // 如果同一树层nums[i - 1]使用过则直接跳过
            if (i > 0 && s[i] == s[i - 1] && !(state >> (i - 1) & 1)) continue;//树层剪枝
            if (!(state >> i & 1)) {
    
    
                path.push_back(s[i]);
                dfs(s, state + (1 << i));//state的更新放在递归参数上,就不用回溯了
                path.pop_back();//这种push_back就必须回溯,因为确实会对后面造成影响;
            }
        }
    }
public:
    vector<string> permutation(string s) {
    
    
        sort(s.begin(), s.end());
        dfs(s, 0);

        return ans;
    }
};

3.(yxc无敌解法)回溯:
与方法2的差别在于递归函数dfs作用是:固定s的一个元素,去遍历path的每个空位,与之对应的是state表示的是path哪些位置已经填了数了

class Solution {
    
    
public:
    vector<string> ans;
    string path;
    //当前函数要确定s[u]放的位置,从start处开始找,当前的排列状态为state(例如s有n个元素,则用n位二进制state的某位为0代表空位,1地表该位置已经放了数了)
    void dfs(string& s, int u, int start, int state) {
    
    //从start处开始找s[u]该放的位置,目前的的排列状态是state
        if (u == s.size()) {
    
    //说明s[0]~s[s.size() - 1]都已经确定好位置了,即已经得到了一种排列方式
            ans.push_back(path);
            return;
        }
        //对于元素不同情况下的全排列,循环都该从0开始,然后依次枚举visted数组中的空位;
        //但本题的字符串s是可能有重复字符的,相同字符只要所占位置相同,就会导致path重复,例如s[0]==s[1]==1,则s[0]放在下标0处s[1]放在下标1处,跟二者换换位置得到的全排列是一样的;
        //我们可以先sort,让相同字符都相邻,并规定:若s[u] == s[u-1],则放完s[u - 1]后在放s[u]时,只能选取s[u-1]后面的空位来防止重复,例如s[0]==s[1]==1,若s[0]放在0处,s[1]可选的位置从1开始,不能放在0处,这样就防止了s[1]在0处s[0]在1处这种情况;
        if (!u || s[u] != s[u - 1]) start = 0;//当前元素s[u]不是重复元素, 就跟常规全排列 一样从0开始找要放的位置;若s[u] == s[u-1],则s[u]可选的位置只能从s[u-1]后面开始
        for (int i = start; i < s.size(); ++i) {
    
    //寻找s[u]可以放的位置
            if (!(state >> i & 1)) {
    
    //i位置是空位
                path[i] = s[u];
                dfs(s, u + 1, i + 1, state + (1 << i));
                //这里之所以不用回溯,因为state + (1 << i)作为参数传入下一层递归,而未改变当前层的state,因此虽然i位置确定填了s[u],但++i后的state仍认为i位置是空位,因此后续在需要填时会直接覆盖s[u]
            }
        }
    }
    vector<string> permutation(string s) {
    
    
        path.resize(s.size());//因为在递归函数中,用path[i]对其赋值,而不是push_back
        sort(s.begin(), s.end());//让相同元素相邻
        dfs(s, 0, 0, 0);

        return ans;
    }
};
  • 总结:
    ① 方法2,3的区别:方法2固定的是path位置,去往这里填元素;方法3固定的是s的一个元素,去选择它放的位置;
    ② 用int state 替代 vector used很巧妙:state >> i & 1用于判断state的下标 i 处是否为1;state += (1 << i )表示 i 位置现在不再是空位;
    ③ 递归函数中使用到了path[ i ] = 。。。,因此需要提前path.resize(。。。)对其初始化;

猜你喜欢

转载自blog.csdn.net/jiuri1005/article/details/114109528