赞
踩
题目描述
输入一个长度为 n n n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。
例如:输入字符串 ABC \text{ABC} ABC ,则输出由字符 A,B,C \text{A,B,C} A,B,C 所能排列出来的所有字符串 ABC \text{ABC} ABC , ACB \text{ACB} ACB , BAC \text{BAC} BAC , BCA \text{BCA} BCA , CBA \text{CBA} CBA 和 CAB \text{CAB} CAB。
数据范围: n < 10 n < 10 n<10
要求:空间复杂度 O ( n ! ) O(n!) O(n!),时间复杂度 O ( n ! ) O(n!) O(n!)
输入描述:
输入一个字符串,长度不超过 10 10 10 , 字符只包括大小写字母。
输入:
"ab"
返回值:
["ab","ba"]
返回[“ba”,“ab”]也是正确的
输入:
"aab"
返回值:
["aab","aba","baa"]
输入:
"abc"
返回值:
["abc","acb","bac","bca","cab","cba"]
输入:
""
返回值:
[]
分析
首先说明一下,这道题是牛客上刷的笔试题,核心代码模式,不是ACM模式。也就是说,它会给你下面这样一个接口:
话说牛客这在线编辑器做的还是不错的,可以设置 Vim 编辑,还自带 solarized light主题。虽然我更想要 solarized dark,大概是下面这个样子:
好吧下面分析这道题。
首先想到的就是 STL 中的 next_permutation \text{next\_permutation} next_permutation,直接获取字典序中下一个排列。使用之前需要对原序列排序,这样才能获取所有的排列。
这个方法是不需要判重的,因为 next_permutation \text{next\_permutation} next_permutation 它获取下一个排列,自然与上一个不重复。
class Solution {
public:
vector<string> Permutation(string str) {
vector<string> ret;
sort(str.begin(),str.end());
do{
ret.push_back(str);
}while(next_permutation(str.begin(),str.end()));
return ret;
}
};
另外提一下, next_permutation \text{next\_permutation} next_permutation 调用一次的复杂度最优为 O ( 1 ) O(1) O(1),最差为 O ( n ) O(n) O(n) , n n n 为序列的长度。但是如果连续调用若干次该函数,取遍所有的排列,那么它的均摊时间复杂度为 O ( 1 ) O(1) O(1)。
至于为啥均摊O(1),这个我不会证明,只有一个感性的认知。具体每次调用next_permutation复杂度是多少,它直接受序列从尾往前非递减子序列长度影响。我举了几个全排列的例子,发现所有排列中这个子序列总长度是一个 1 + 1 2 \frac{1}{2} 21 + 1 4 \frac{1}{4} 41 + … 的模型,也就是均摊下来每个排列尾部这个序列长度大概是2,是个小常数。后面会放出next_permutation源码与我写的my_nextPermutation,大家可以再琢磨琢磨。
提交了上面的代码后,我又想到逆康托展开。也可以处理排列问题,本着复习的目的,用逆康托展开做了遍:
按我的理解,逆康托展开它和进制转换有点相似 好吧也不是很相似。例如:
排列 | 映射的数字 |
---|---|
123 | 0 |
132 | 1 |
213 | 2 |
231 | 3 |
312 | 4 |
321 | 5 |
那么假设已知 “映射的数字” 是 3 3 3 ,如何得到原排列?
首先确定第一个数字,因为后两位数字有 2 ! 2! 2! 种组合, ⌊ 3 2 ! ⌋ = 1 \lfloor \frac{3}{2!} \rfloor=1 ⌊2!3⌋=1,所以第一个数字要取当前能取的 “第1小” 的数字。当前 1 , 2 , 3 1,2,3 1,2,3 都可以取,那么 “第1小” 的数字就是 ‘2’(这里的第1小指的是下标,第0小的数字才是’1’),排列的第一位数字得到。
那么现在递归一下,相当于在 1,3 这两个数字的排列里面,找 “映射的数字” 是 3 % 2 ! = 1 3\%2!=1 3%2!=1 的原排列。
那么现在要确定排列的第二位数字,后 1 1 1 位数字有 1 ! 1! 1! 种组合, ⌊ 1 1 ! ⌋ = 1 \lfloor \frac{1}{1!} \rfloor=1 ⌊1!1⌋=1,现在又要取当前可取的数字中 “第1小” 的数字。当前可以取 1 , 3 1,3 1,3 ,那么这一位就取 ‘3’.
继续递归,在 1 这个数字的排列里面(只有它自己一种排列),找 “映射的数字” 是 1 % 1 ! = 0 1\%1!=0 1%1!=0 的原排列。
现在要确定排列的第三位数字,后 0 0 0 位数字有 0 ! = 1 0!=1 0!=1 种组合, ⌊ 0 0 ! ⌋ = 0 \lfloor \frac{0}{0!} \rfloor=0 ⌊0!0⌋=0,那么取当前可取的数字种 “第0小” 的数字。所以取 ‘1’.
逆康托展开大概就是这么个过程吧。可能我分析的还不如直接看代码琢磨
当然也有(正)康托展开,将一个排列映射为一个数字。正展开和逆展开原理一样,大家可以自己探索探索。
class Solution { public: int f[15]={1}; unordered_set<string> st; string get(int x,int n,string str){ string ret; for(int i=n;i>=1;i--){ int t=x/f[i-1]; ret.push_back(str[t]); str.erase(str.begin()+t); x%=f[i-1]; } return ret; } vector<string> Permutation(string str) { vector<string> ret; for(int i=1;i<=10;i++) f[i]=f[i-1]*i; for(int i=0;i<f[str.size()];i++){ string t=get(i,str.size(),str); if(!st.count(t)) st.insert(t),ret.push_back(t); } return ret; } };
逆康托展开一次的复杂度是 O ( n 2 ) O(n^2) O(n2),这样看来比起 next_permutation \text{next\_permutation} next_permutation 差远了。但是康托展开有另外的用途,例如给一个序列 s s s,求字典序排在它之后 m ≤ 1 0 18 m\le10^{18} m≤1018 个的序列。这个问题就更适合用康托展开来解决。
然后我在牛客题解中又看到有人手写了个 my_nextPermutation \text{my\_nextPermutation} my_nextPermutation 来做这道题,妙啊。其实我一直很好奇 STL 中 next_permutation \text{next\_permutation} next_permutation 的实现,看了写题解那位大佬的代码,并不是很懂。在被室友拉出去买饭排队的途中,决定自己也动动脑,然后发现所有求下一个排列的操作,无非就是个 1 , 4 , 3 , 2 1,4,3,2 1,4,3,2 的模型。尾部往前是一个非递减的序列,直到某处出现递减,然后 swap \text{swap} swap , reverse \text{reverse} reverse 就行了。
吃完饭后写了下面的代码:
class Solution { public: bool my_nextPermutation(string &s){ int l=s.size()-2,r=s.size()-1; while(l>=0&&s[l]>=s[l+1]) l--; if(l<0) return false; while(s[r]<=s[l]) r--; swap(s[l],s[r]); reverse(s.begin()+l+1,s.end()); return true; } vector<string> Permutation(string str) { vector<string> ret; sort(str.begin(),str.end()); do{ ret.push_back(str); }while(my_nextPermutation(str)); return ret; } };
其实在敲代码的过程中,我怀疑可能出现 reverse(itl,itr)
而 itr<itl
的问题,但后来发现根本没有这种情况。
我估计即使出现这种问题,reverse 也不会出错。于是顺带搜了搜 reverse 的源码:
template <class _RandomAccessIter>
_STLP_INLINE_LOOP void
__reverse(_RandomAccessIter __first, _RandomAccessIter __last, const random_access_iterator_tag &) {
for (; __first < __last; ++__first)
_STLP_STD::iter_swap(__first, --__last);
}
果然如果 itr<itl
,根本不会进循环,但是这仅适用于随机访问迭代器。该函数还有如下的重载版本:
template <class _BidirectionalIter>
_STLP_INLINE_LOOP void
__reverse(_BidirectionalIter __first, _BidirectionalIter __last, const bidirectional_iterator_tag &) {
for (; __first != __last && __first != --__last; ++__first)
_STLP_STD::iter_swap(__first,__last);
}
那么对于地址不是连续存放的容器,确实应该注意这个问题。
再看看代码逻辑,STL 源码果然不寻常,仅仅是简单的 reverse 写的都这么有水准。
既然都手搓了个 my_nextPermutation \text{my\_nextPermutation} my_nextPermutation ,那肯定是要对比一下 STL 源码来看看:
template<typename _BidirectionalIterator, typename _Compare> bool __next_permutation(_BidirectionalIterator __first, _BidirectionalIterator __last, _Compare __comp) { if (__first == __last) // 传入内容为空,直接返回false return false; _BidirectionalIterator __i = __first; // 仅要求双向迭代器 ++__i; if (__i == __last) // 仅包含一个元素,返回false return false; __i = __last; --__i; // __i 指向最后一个元素 for(;;) { _BidirectionalIterator __ii = __i; // --__i; // __i + 1 == __i , 这两个迭代器负责找到第一个非递增点 if (__comp(__i, __ii)) // 获得非递增点 { _BidirectionalIterator __j = __last; while (!__comp(__i, --__j)) // 获取第一个比 __i 大的元素 {} std::iter_swap(__i, __j); // 交换 std::__reverse(__ii, __last, // 逆序 std::__iterator_category(__first)); return true; } if (__i == __first) // 查找到了first, 此时所有元素全部递增,所以此时序列最大 { std::__reverse(__first, __last, std::__iterator_category(__first)); // 最大序列逆序,会成为最小序列 return false; } } }
总的来说逻辑和上面我的实现一模一样,先倒着找到第一个非递增点
i
i
i,再倒着找到第一个大于
∗
i
*i
∗i 的
∗
j
*j
∗j。swap(i,j)
,reverse(i+1,end())
;
最后呢,我觉得这道题的本意还是希望我们用 dfs \text{dfs} dfs 来做。如果用 bool \text{bool} bool 数组记录每个元素有没有使用过,那么复杂度 O ( n n ) O(n^n) O(nn)。
还有更好的方法,用 swap \text{swap} swap 将已经使用过的元素全部移动到序列前 x x x 位,这样就不用每次都扫描全部的序列了。代码如下:
一定要用 unordered_map \text{unordered\_map} unordered_map 判个重。
class Solution { public: unordered_set<string> st; int len; void dfs(string& s,int step){ if(step>=len) return (void)st.insert(s); for(int i=step;i<len;i++){ swap(s[step],s[i]); dfs(s,step+1); swap(s[step],s[i]); } } vector<string> Permutation(string str) { len=str.size(); dfs(str,0); return vector<string>(st.begin(),st.end()); } };
在做这道题之前,我是万万没想到一道递归的中等题能牵扯出这么多东西。在做题的过程中,不断联系到其他的知识点,也就依次把它们复习一遍,同时也学到了很多新的知识。例如 next_permutation \text{next\_permutation} next_permutation 的实现与复杂度也是今天才了解。
最近几个月开始准备找工作,不刷 ACM 博客也就没啥可更新的,总不能做道反转链表的题都发文章吧。当然近期也积累了很多面试有关的笔记,但我写的笔记仅仅是作为自己复习时的备忘录,里面的内容比较杂,不全面,甚至不正确,也就没发在博客上。好吧希望之后复习顺利,能做自己感兴趣的工作。
参考资料:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。