剑指 Offer 38. 字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入: s = “abc”
输出: [“abc”,”acb”,”bac”,”bca”,”cab”,”cba”]
限制:
1 <= s 的长度 <= 8
[HTML_REMOVED]
解题思路
深度优先遍历和去重技巧的结合。
根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符摆放,先固定第 1
位字符( n
种情况)、再固定第 2
位字符(n-1
种情况)、… 、最后固定第n
位字符( 1
种情况)。
说的通俗一点就是把n
个数字放到n
个框里,有多少种放法,对于每个框,我们都有n
个数字,即n
个选择,但我们要判断手中的数字是否已经放入其他框了,故需要用到一个标记数组
。
上图是没有重复字符的情况,如果出现了重复字符怎么办,这就涉及到去重
了,有重复字符的情况如下图。
我们会发现abb’
与ab’b
,bab'
与b’ab
,bb'a
与b’ba
是重复的,为什么会出现这种情况?
我们可以先回忆一下初中就学过的掷骰子,掷两个骰子,出现(4,2)
和(2,4)
是视为两种不同的情况的,但是出现(3,3)
和(3,3)
则只视为一种情况,因为“第一个骰子点数是3
,第二个骰子点数也是3
”与“第二个骰子点数是3
,第一个骰子点数也是3
”是完全相同的结果。
引申到排列中,为了举例方便,这里用数字11'2345
- 第一个框放
1
,然后让1'2345
五个数字全排列放在其余位置 - 第一个框放
1'
,然后让12345
五个数字全排列放在其余位置
不难发现,情况一和二得到的排列结果将一模一样。道理和掷骰子一样,不管是把1
还是1'
放第一个框,它们终归都是1
,然后后面的框放的也同样都是12345
的全排列,那么最终产生的所有排列结果的集合肯定是一样的。
由此为了去重,我们可以定义一个规则,即对于相同数,我们人为定序,如上例子中11'2345
,我们指定重复数字首次要放入某一个框位
时,只能放重复数字的第一个,如这里只能放1
,放1'
的情况直接过滤,其他框位同理。
Java代码
class Solution {
private String[] res;
//最开始并不知道res最终的长度会是多少,故先把排列结果都放到list中
//之后转到res中即可
private List<String> list = new ArrayList<>();
public String[] permutation(String s) {
if(s == null || s.length() == 0) return new String[0];
boolean[] isVisited = new boolean[s.length()];
//由于要去重,故得先排序,因此将String转为char数组再排序
char[] chs = s.toCharArray();
Arrays.sort(chs);
dfs(chs,new StringBuilder(),isVisited);
//将结果放到数组中,因为返回结果需要的是String数组
res = new String[list.size()];
for(int i = 0;i < list.size();i++){
res[i] = list.get(i);
}
return res;
}
public void dfs(char[] chs,StringBuilder sb,boolean[] isVisited){
if(sb.length() == chs.length){
list.add(new StringBuilder(sb.toString()).toString());
return ;
}
for(int i = 0;i < chs.length;i++){
//全排列算法去重条件模板
//如果这个字符和前一个字符相同且前一个字符还没有用过,continue
//即我们规定重复字符首次要放入某一个框位时,只能放重复字符的第一个
//比如a,a',b是可以的,但是a',a,b和其重复,应该去重,故当要达到产生a',a,b那一层递归栈时
//由于a'与前一个字符a相同,且a还没有用过,所以continue,这刚好去重,是我们想要的结果
if(i > 0 && chs[i] == chs[i - 1] && !isVisited[i - 1]) continue;
if(!isVisited[i]){
sb.append(chs[i]);
isVisited[i] = true;
dfs(chs,sb,isVisited);
//恢复现场
sb.deleteCharAt(sb.length() - 1);
isVisited[i] = false;
}
}
}
}
这里的去重对于刚接触的同学来说确实不太好理解(我刚开始学的时候也绕了一天,绕明白后就感觉是常识了。。。),为了更好的理解,下面来两道扩展题,尤其是第二道题,实在没想明白去重时,可以跟着第二题的代码模拟一下代码执行过程,说不定突然理解了,我当时貌似就是这样绕过来的。
扩展题一:LeetCode 46. 全排列
给定一个 没有重复
数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解题思路
深度优先
即可,可以理解成把n
个数字放到n
个框里,有多少种放法,对于每个框,我们都有n
个数字,即n
个选择,但我们要判断手中的数字是否已经放入其他框了,故用到一个标记数组。
Java代码
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums == null) return res;
boolean[] isVisited = new boolean[nums.length];
dfs(res,new ArrayList<>(),nums,isVisited);
return res;
}
private void dfs(List<List<Integer>> res,List<Integer> list,int[] nums,boolean[] isVisited){
//总共就nums.length个框,当list.size() = nums.length时,说明手中数字都放完了
if(list.size() == nums.length){
res.add(new ArrayList<>(list));//添加的是list副本
return;
}
for(int i = 0;i<nums.length;i++){
if(!isVisited[i]){
list.add(nums[i]);
isVisited[i] = true;
dfs(res,list,nums,isVisited);
//回溯时,将list,isVisited还原为上一级栈的状态
list.remove(list.size()-1);
isVisited[i] = false;
}
}
}
}
扩展题二:LeetCode 47. 全排列 II
给定一个可包含重复数字
的序列nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
输入: nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
解题思路
与上题一样,但多个去重
,去重方式看代码注释处
,注意这里去重得先排序
(我之前自己有时也忘记了,然后在那里检查哪里出了错,0.0)。
Java代码
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums == null) return res;
Arrays.sort(nums);//要去重,故先排序
boolean[] isVisited = new boolean[nums.length];
dfs(res,new ArrayList<>(),nums,isVisited);
return res;
}
private void dfs(List<List<Integer>> res,List<Integer> list,int[] nums,boolean[] isVisited){
//总共就nums.length个框,当list.size() = nums.length时,说明手中数字都排完了
if(list.size() == nums.length){
res.add(new ArrayList<>(list));//添加的是list副本
return;
}
for(int i = 0;i<nums.length;i++){
//如果这个数和前一个数相同且前一个数还没有用过,continue
//即我们规定重复数字首次要放入某一个框位时,只能放重复数字的第一个
//比如1,1',2是可以的,但是1',1,2和其重复,应该去重,故当要达到产生1',1,2那一层递归栈时
//由于1'与前一个数1相同,且1还没有用过,所以continue,这刚好去重,是我们想要的结果
if(i>0 && nums[i] == nums[i-1] && !isVisited[i-1]) continue;
if(!isVisited[i]){
list.add(nums[i]);
isVisited[i] = true;
dfs(res,list,nums,isVisited);
//回溯时,将list,isVisited还原为上一级栈的状态
list.remove(list.size()-1);
isVisited[i] = false;
}
}
}
}
tql!
没懂为什么isVisited[i-1] == false 就去重了
tql支持(虽然我不学java)
你他娘的真是个傻吊
你在狗叫什么?
哈哈
你有病?
直接用HashSet去重不就好了吗
很强
很强