蓝桥杯——Java中的全排列
全排列的概念
排列
从n个数中选取m(m<=n)个数按照一定的顺序进行排成一个列,叫作从n个元素中取m个元素的一个排列。不同的顺序是一个不同的排列。从n个元素中取m个元素的所有排列的个数,称为排列数。
全排列
从n个元素取出n个元素的一个排列,称为一个全排列。全排列的排列数公式为[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gjUFMrJx-1614071860880)(https://math.jianshu.com/math?formula=n!)]
时间复杂度
n个数的全排列有n!种,每一个排列都有n个数据,所以输出的时间复杂度为O(n*n!),呈指数级,无法处理大型数据。
全排列(循环)
import java.util.ArrayList;
public class 全排列_非递归 {
public static ArrayList<String> getPermutation(String A) {
int n = A.length();
ArrayList<String> res = new ArrayList<>();
res.add(A.charAt(0) + ""); // 初始化,包含第一个字符
for (int i = 1; i < n; i++) {
ArrayList<String> new_res = new ArrayList<>();
char c = A.charAt(i); // 新字符
for (String str : res) { // 访问上一趟集合中的每一个字符串
// 插入到每个字符串的每个位置,形成新的字符串
String newStr = c + str; // 插在前面
new_res.add(newStr);
newStr = str + c;// 插在后面
new_res.add(newStr);
for (int j = 1; j < str.length(); j++) {// 插在中间
// substring(0, j) 返回下标为 0 到 j-1 的字串
// substring(j) 返回下标 从 j 开始到结束的字串
newStr = str.substring(0, j) + c + str.substring(j);
new_res.add(newStr);
}
}
res = new_res; // 更新res
}
return res;
}
public static void main(String[] args) {
String string="abc";
ArrayList<String> res=getPermutation(string);
System.out.println(res);
}
}
理解简单,三重for循环,复杂度极高
全排列(交换回溯)
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
public class 全排列_回溯 {
static ArrayList<String> res = new ArrayList<>();
public static ArrayList<String> getPermutation(char[] A) { // 函数入口
char[] arr = A; // 将字符串转换为字符数组
Arrays.sort(arr); // 对字符数组排序
getPumutationCore(arr, 0);
return res;
}
public static void getPumutationCore(char[] arr, int k) { // 构造排列串
if (k == arr.length) { // 多路递归终止条件,即排列出一种情况
res.add(new String(arr));
}
for (int i = k; i < arr.length; i++) {// 分别确定一种字符串的第i到n位
swap(arr, k, i); // 将第i位置的字符(最初字符串中)确定放置在第k位排列串中
getPumutationCore(arr, k + 1); // 进行下一个位置字符的排列
swap(arr, i, k);// 回溯
}
}
public static void swap(char[] arr, int i, int j) { // 交换函数
char temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
String string = "12345";
res = getPermutation(string.toCharArray());
Collections.sort(res);
System.out.println(res);
}
}
优点:简洁、重复
缺点:没有字典序 。可以对排列后的结果进行排序处理,从而解决没有字典序的问题
常用于求排列结果或者种数问题.
全排列(前缀法)
import java.util.ArrayList;
import java.util.Arrays;
public class 全排列_前缀法 {
static int count = 0;
final static int k = 3; // 查找第k个排列
static ArrayList<String> ans = new ArrayList<>();
public static void permutation(String prefix, char[] arr) {// 入口,传递的arr为字符数组,prefix为空串
if (prefix.length() == arr.length) { // 前缀长度==字符集的长度,一个排列就完成了
count++;
ans.add(prefix);
if (count == k) { // 按照顺序找到第k个排列
System.out.println("------" + prefix);
System.exit(0);// 退出系统
}
}
// 每次从头扫面(保证为升序),只要该字符可用,就附加到前缀后面,前缀变长了
for (int i = 0; i < arr.length; i++) {
char ch = arr[i]; // 获取第i个字符
// 这个字符在前缀字符中出现的次数小于字符数组中出现的次时才可选
if (count(prefix.toCharArray(), ch) < count(arr, ch)) {
permutation(prefix + ch, arr);
}
}
}
public static int count(char[] arr, char ch) {// 计算一个字符在字符数组中出现的次数
int cnt = 0;
for (char c : arr) {
if (c == ch) {
cnt++;
}
}
return cnt;
}
public static void main(String[] args) {
char[] string = "34510".toCharArray();
Arrays.sort(string);
permutation("", string);
System.out.println(ans);
System.out.println(count);
}
}
**优点:维持字典序 **
缺点:代码量大、复杂 。
常用于求第K个排列问题
练习(入门)
洛谷 P1706 全排列问题
题目描述
输出自然数 1 到 n 所有不重复的排列,即 n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
输入格式
一个整数 n。
输出格式
由 1 ∼n 组成的所有不重复的数字序列,每行一个序列。
每个数字保留 5 个场宽。
思路
全排列,这里选择使用递归回溯的方法,选择使用Vector容器,方便最后的排序
代码
import java.util.Arrays;
import java.util.Collections;
import java.util.Scanner;
import java.util.Vector;
public class 全排列问题 {
private static Scanner scanner = new Scanner(System.in);
private static Vector<String> res = new Vector<>(); // 自带排序函数Sort
private static int n;
public static void main(String[] args) {
n = scanner.nextInt();
char[] s = new char[n];
for (int i = 0; i < n; i++) {
Integer t = i + 1;
s[i] = t.toString().charAt(0);
}
Arrays.sort(s);
getPumutation(s, 0);
Collections.sort(res);
for (String string : res) {
for (int i = 0; i < string.length(); i++) {
System.out.print(" " + string.charAt(i));
}
System.out.println();
}
}
public static void swap(char[] arr, int i, int j) { // 构造交换字符函数
char temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void getPumutation(char[] arr, int k) { // 构造排列函数
if (arr.length == k)
res.add(new String(arr));
else {
for (int i = k; i < arr.length; i++) {
swap(arr, i, k);
getPumutation(arr, k + 1);
swap(arr, k, i);
}
}
}
}
练习(进阶)
洛谷 P4163 [SCOI2007] 排列
题目描述
给一个数字串 s和正整数 d 统计 s 有多少种不同的排列能被 d 整除(可以有前导 00)。例如 123434有 90 种排列能被 2整除,其中末位为 2 的有 30种,末位为 4 的有 60 种。
输入格式
输入第一行是一个整数 T,表示测试数据的个数,以下每行一组 s和 d*,中间用空格隔开。s 保证只包含数字 0,1,2,3,4,5,6,7,8,9。
输出格式
每个数据仅一行,表示能被 d整除的排列的个数。
**思路:**每次数据进行全排列。再对排列结果进行筛选
import java.util.Arrays;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class P4163_排列 {
private static Scanner scanner = new Scanner(System.in);
private static int N;
static Set<String> conSet = new HashSet<>();
static class SUM {
public int count;
public SUM() {
count = 0;
}
}
private static int count(char[] arr, char a) { // 计算字符出现次数
int count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i] == a) {
count++;
}
}
return count;
}
private static boolean isMod(String s, int d) { // 判断是否能整除
return Long.parseLong(s) % d == 0 ? true : false;
}
private static boolean isRepeat(String s) { // 判断排列串是否出现过
if(conSet.contains(s))
return true;
else {
conSet.add(s);
return false;
}
}
private static void getPumutation(char[] arr, String profix, SUM sum, int d) { // 排列函数
if (profix.length() == arr.length) { // 新的排列串构成
if (isMod(profix, d)) {
if (!isRepeat(profix))
sum.count++;
}
}
for (int i = 0; i < arr.length; i++) {
char temp = arr[i];
if (count(arr, temp) > count(profix.toCharArray(), temp)) {
getPumutation(arr, profix + temp, sum, d);
}
}
}
public static void main(String[] args) {
N = scanner.nextInt();
while ((N--) != 0) {
String string = scanner.next();
char[] a = string.toCharArray();
Arrays.sort(a);
int d = scanner.nextInt();
Integer countInteger = 0;
SUM sum = new SUM();
getPumutation(a, "", sum, d);
System.out.println(sum.count);
conSet.clear();//清空记录字串的容器
}
}
}
**结果:**超时严重,只对了2个测试用例。java没有TSL中的next_permutation函数,做起来相对麻烦。网上关于java的解析暂没找到。
学习总结
Set 容器的使用**
- HashSet Set的子接口 (散列存储) 查找速度快,易实现
- TreeSet Set的子接口 (有序存储)—排序的类集框架 默认字母升序排列
注:TreeSet 排序是依靠Comparable接口实现的,所以保存自定义类对象时要覆写compareTo方法
class Book1 implements Comparable<Book1>{
private String title;
private double price;
@Override
public int compareTo(Book1 o) {
//按照价格排序
if(this.price> o.price){
return 1;
}else if(this.price< o.price){
return -1;
}else {
return this.title.compareTo(o.title);
}
}
public static void main(String[] args) {
//按照价格排序
Set<Book1> set = new TreeSet<>();
set.add(new Book1("Java开发",990.9));
System.out.println(set);
}
//其它set操作
set.clear();//清空容器
set.contains(s);//判断是否含有s
值传递与引用传递
Java中数据类型分为基本类型的引用类型两大类
基本类型: byte、short、int、long、float、double、boolean、char
引用类型: 类、接口、数组
基本类型的变量在声明时就会分配数据空间
而引用类型在声明时只是给变量分配了引用空间,并不分配数据空间
1、基本数据类型传值,对形参的修改不会影响实参
2、引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象
3、String, Integer, Double等不可变的类型,可以理解为传值,最后的操作不会修改实参对象
**将基本类型实现引用传递的方法:**将基本数据类型构造成一个类的公有属性
字符串与数字的相互转换方法
//1、字符串转化为整型数字
Integer.parseInt(String s);
Integer.valueOf(String s);
//2、字符串转化为浮点型数字
Float.parseFloat(String s);
Double.parseDouble(String s);
//整形、浮点类型转化为字符串
String s = i + ""; // 方法一
String s = String.valueOf(i); // 方法二
String s = Integer.toString(i); // 方法三
//JDK中判断字符串是否为数字函数
public static boolean isNumeric(String str);
学习参考
https://blog.csdn.net/J_Jie_/article/details/109695386 对Set的使用
https://www.cnblogs.com/lmj612/p/10874052.html java 中的 值传递 与 引用传递
https://zhidao.baidu.com/question/175584476965443564.html java中如何对基本数据类型进行引用传递
https://zhidao.baidu.com/question/144544532.html java中字符串与数字的相互转换方法
https://www.jianshu.com/p/50a27d7d2972 全排列算法的理解与实现
https://www.bilibili.com/video/BV1e7411T7FV?p=114 全排列学习网课(算法很美)