《剑指Offer》Java刷题 NO.27 字符串的排列(全排列、去重、字典序)
传送门:《剑指Offer刷题总目录》
时间:2020-03-30
题目:
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
输入描述:
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。
什么是字典序?
1.自然排序vs字典序vs字符串排序
自然排序就是字典序排序,不分大小写。字符串排序要分大小写的,就是大小写也要先后顺序。
2.单个字符vs多个字符(字符串)
'0' < '1' < '2' < ... < '9' < 'a' < 'b' < ... < 'z'
两个字符串比较大小
是按照从左到右的顺序进行比较,如果第1位相等,就比较第2位,直至有一位可以比较出大小来,则不再继续比较。
'ab' < 'ac'、'abc' < 'ac'、'abc' < 'abcd'
思路:全排列问题
1.递归算法(得出结果之后还要再进行一次排序,效率不高)
<1>基本思想是用第一个字符和后面的每个字符交换,然后固定第一个字符,用第二个字符和后面的每个字符交换,以此类推的递归;
<2>那么排列次数就是除了第一个字符以外其他字符全排quan列的次数加一;
<3>递归的出口就是子字符串只剩一个字符的时候
(图源:牛客网)
但是对于字符串中有重复字符
的情况就需要特殊处理,比如ABBCD,两个相同的字符和子序列最左字符交换的效果是相同的,都是把同一个字符固定在最左边然后对剩下的做全排列:B1(AB2CD)和B2(AB1CD)是相同的,所以这里采用:
方案1.
从第一个数字起,每个数分别与它后面非重复出现的数字交换。
- 用HashSet保存每一轮递归时的字符,没出现过的才交换字符位置并把字符放进HashSet
- 该方法得出结果之后需要再用Collections.sort()【很耗时】 排序以得到字典序的全排列
方案2:
先用TreeSet临时保存得到的String,可以保证排序和去重。
(TreeSet可以理解成数组实现的二叉搜索树的容器,可以直接用addAll添加到ArrayList中)
注意点: 递归回到上一层时要先把字符串复原
(两个字符交换回来,回溯)再进行下一次交换;
比较: TreeSet的add和addAll方法比较耗时,总体来讲两个方案差别不大
2.字典序全排列(从算法流程中能看出重复元素不影响排序)【效率较高】
寻找下一个排列的方法: (eg:当前排列为358754
)
复杂度为:O(n)+O(n)+O(n)=O(n)
总的时间复杂度为O(n*n!)
若是从小到大的字典序,我们希望下一个(肯定比当前值要大)和当前值的差值尽量小,并且两个之间不可能再存在其他值
- 从右向左找左邻值小于当前值的那一位(如果左邻大于当前值,根本不可能在该段内使得数字更大,所以继续往左找),左邻的索引记为
a
,值记为A
;此时A
的右边肯定是从左往右依次减小的;如果找到最前面也没找到,就说明当前值是最后一个排列,已经是最大值了 - 然后从右往左找第一个比
A
大的值,记为B
- 交换
A
和B
:378554
(还没完)(此时a
后面的数依旧是从大到小排列的)
4.接下来应该把a
右边的数字再从小到大排列:374558
(其实只需要倒序
一下即可)
Java代码
import java.time.Clock;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
/**
* @ClassName Permutation
* @Discription 输入一个字符串, 按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出
* 由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
* @Author lemon
* @Date 2020/3/18 21:03
**/
public class Permutation {
/**
*方法一:递归法(回溯法)
* 利用Collections.sort排序(效率较低)
*/
public ArrayList<String> permutationOne(String str) {
ArrayList<String> result = new ArrayList<>();
//判断str是否为空
if (str == null || str.length() == 0) {
return result;
}
permutationOneHelper(str.toCharArray(), 0, result);
//Collections.sort底层调用Arrays.sort,得到自然排序也就是字典序的list
Collections.sort(result);
return result;
}
private void permutationOneHelper(char[] chars, int index, ArrayList<String> result) {
//递归出口,子字符串只剩最后一个字符,直接把字符串加入result
if (index == chars.length - 1) {
result.add(String.valueOf(chars));
} else {
Set<Character> charSet = new HashSet<>();
for (int j = index; j < chars.length; j++) {
//只对从未出现过的(非重复)字符进行交换,比list.contains效率高
if (!charSet.contains(chars[j])) {
charSet.add(chars[j]);
swap(chars, index, j);
//对剩下的子字符串进行递归
permutationOneHelper(chars, index + 1, result);
//需要把字符串复原再进行下一次字符交换
swap(chars, j, index);
}
}
}
}
/**
*交换两个字符
*/
private void swap(char[] chars, int i, int j) {
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
/**
* 按格式打印ArrayList
*/
private void printArrayListOfString(ArrayList<String> list) {
StringBuilder data = new StringBuilder();
data.append("[");
for (String str : list) {
data.append(str);
data.append(",");
}
data.deleteCharAt(data.lastIndexOf(","));
data.append("]");
System.out.println(data.toString());
}
/**
*方法一改进版,利用TreeSet暂存
*/
public ArrayList<String> permutationTwo(String str) {
ArrayList<String> result = new ArrayList<>();
TreeSet<String> temp = new TreeSet<>();
//判断str是否为空
if (str == null || str.length() == 0) {
return result;
}
permutationTwoHelper(str.toCharArray(),0,temp);
result.addAll(temp);
return result;
}
private void permutationTwoHelper(char[] chars, int index, TreeSet<String> result) {
//递归出口,子字符串只剩最后一个字符,直接把字符串加入result
if (index == chars.length - 1) {
result.add(String.valueOf(chars));
} else {
Set<Character> charSet = new HashSet<>();
for (int j = index; j < chars.length; j++) {
//只对从未出现过的(非重复)字符进行交换,比list.contains效率高
if (!charSet.contains(chars[j])) {
charSet.add(chars[j]);
swap(chars, index, j);
//对剩下的子字符串进行递归
permutationTwoHelper(chars, index + 1, result);
//需要把字符串复原再进行下一次字符交换
swap(chars, j, index);
}
}
}
}
/**
*方法二:字典序全排列
*/
public ArrayList<String> permutationThree(String str){
ArrayList<String> result = new ArrayList<>();
if(str == null || str.length() == 0){
return result;
}
char[] chars = str.toCharArray();
int length = str.length();
//Arrays.sort能得到第一个字符串,然后以此为基础往下找
Arrays.sort(chars);
String next = new String(chars);
result.add(next);
while(true){
next = findNextString(next,length);
//如果已经完成了就退出循环
if(next.equals("done")){break;}
result.add(next);
}
return result;
}
/**
*返回下一个排列的String
*/
private String findNextString(String str,int length){
char[] chars = str.toCharArray();
//从右往左找左邻比本身大的元素,i为该左邻元素的索引值
int i = length - 2;
for(;i>=0 && chars[i]>=chars[i+1] ;i--){}
//找到最前面也没找到,说明当前值为最大的,可以结束了
if(i == -1){
return "done";
}
//从右往左找第一个比chars[i]大的元素
int j = length - 1;
for(;chars[j] <= chars[i];j--){}
//交换
swap(chars,i,j);
//逆序i后面的序列
for(int left = i+1,right = length - 1;left < right;left++,right--){
swap(chars,left,right);
}
return String.valueOf(chars);
}
public static void main(String[] args) {
Permutation permutation = new Permutation();
permutation.permutationOne("");
permutation.permutationTwo("");
permutation.permutationOne("a");
permutation.permutationTwo("a");
permutation.permutationOne("aa");
permutation.permutationTwo("aa");
permutation.permutationOne("abac");
permutation.permutationTwo("abac");
long s = Clock.systemDefaultZone().millis();
permutation.permutationOne("futgehatfgw");
System.out.println("permutationOne耗时: " + (Clock.systemDefaultZone().millis() - s) + " ms");
s = Clock.systemDefaultZone().millis();
permutation.permutationTwo("futgehatfgw");
System.out.println("permutationTwo耗时: " + (Clock.systemDefaultZone().millis() - s) + " ms");
s = Clock.systemDefaultZone().millis();
permutation.permutationThree("futgehatfgw");
System.out.println("permutationThree耗时: " + (Clock.systemDefaultZone().millis() - s) + " ms");
/* permutationOne耗时: 4312 ms
其中Collections.sort耗时2500ms左右
permutationTwo耗时: 3553 ms
permutationThree耗时: 883 ms*/
}
}