一、蓝桥练习题(基础篇)
1、冒泡排序
给定一个长度为n的数列,将这个数列按从小到大的顺序排列。1<=n<=200。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int times = scan.nextInt();
int[] arr = new int[times];
for (int i = 0; i < times; i++) {
arr[i] = scan.nextInt();
}
int cout = 0;
for (int i = 0; i < times-1; i++) {
for (int j = 0; j < arr.length-1-i; j++) {
if (arr[j] > arr[j+1]) {
int tem = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tem;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
scan.close();
}
}
2、高精度大数加法
import java.math.BigInteger;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println(scan.nextBigInteger()
.add(scan.nextBigInteger()));
scan.close();
}
}
3、十进制转化八进制
思路:
- 先将16进制转换成2进制,并严格格式化成1位16进制数对应4位二进制数的格式。
- 将2进制串转化为8进制串,严格按照3个二进制数对应一个8进制数。
- 8进制数忽略前导0
-
使用的API
- 使用Integer类
- 使用StringBuilder类
import java.util.*; public class Main { public static void main(String[] args) { Scanner scan = new Scanner(System.in); int n = scan.nextInt(); for(int i = 0 ; i < n; i++) { //读取有多少个二进制数 String strHex = scan.next(); //读取每一行的十六进制数 StringBuilder hexStr = new StringBuilder(strHex); //转换2进制,有可能这个字符串很长可能大于形参的长度 为了保证效率我使用了 StringBuilder可变序列 StringBuilder builder = new StringBuilder(); for(int j = 0;j <= hexStr.length() - 1;j++){ builder.append(toBinString(hexStr.substring(j,j+1))); } //格式化16进制转化得到的2进制保证1位16进制位对应4个2进制位的二进制串 StringBuilder binaryStr =new StringBuilder( //6进制转化的二进制串格式化为能转为八进制的二进制串 formatStringToOct( //格式化16进制转化的二进制串 formatStringHexToBin( builder.reverse().toString()))); System.out.println(binaryStr.toString()); //二进制转八 得到结果res String res = toOcString(binaryStr.toString()); System.out.println(res) import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scan = new Scanner(System.in); int n = scan.nextInt(); for (int i = 0; i < n; i++) { //获取一行数据 String line = scan.next(); StringBuilder builder = new StringBuilder(); for (int k = 0; k < line.length();k++){ //取每一位八进制数转换成二进制数 1对4的关系 builder.append(hexToBin(line.substring(k,k+1))); } //格式化二进制字符串 // System.out.println("16转2(格式化好字串):"+builder.toString()); StringBuilder bin = new StringBuilder(formatBinToOct(builder.toString())); // System.out.println(" 2转8(格式化好字串):"+bin.toString()); //用于存储二进制转换八进制 builder = new StringBuilder(); for (int j = 0; j < bin.length(); j += 3) { builder.append(binToOct(bin.substring(j,j+3))); } String res = builder.toString(); System.out.println(check0(res)); } scan.close(); } /** * 十六转化二进制 * @param hex * @return */ private static String hexToBin(String hex){ //可能有的16转2进制没有四位 余姚格式化补零 return formatHexToBin(Integer.toBinaryString(Integer.valueOf(hex,16))); } /** * 二转八 * @param bin * @return */ private static String binToOct(String bin){ //每次传入的bin是3个位的二进制串对应一个八进制数 不用格式化 return Integer.toOctalString(Integer.valueOf(bin,2)); } /** * 格式化为1个十六进制对应4个二进制位的二进制串 * @param bin * @return */ private static String formatHexToBin(String bin){ long len = bin.length(); if (len % 4 == 3) return "0" + bin; if (len % 4 == 2) return "00" + bin; if (len % 4 == 1) return "000" + bin; return bin; } /** * 格式化为八进制对应的二进制串 * @param bin * @return */ private static String formatBinToOct(String bin){ long len = bin.length(); if (len % 3 == 2) return "0" + bin; if (len % 3 == 1) return "00" + bin; return bin; } /** * 检查前缀0 * @param str */ private static String check0(String str){ for (int i = 0; i < str.length(); i++) { if (str.charAt(i) != '0'){ return str.substring(i); } } return str; } } } scan.close(); } /** * 十六转化2 * @return */ private static String toBinString(String hex) { return Integer.toBinaryString(Integer.valueOf(hex,16)); } /** * 2转八 * @param bin * @return */ private static String toOcString(String bin) { return Integer.toOctalString(Integer.valueOf(bin,2)); } /** * 补全前导0 * @param str * @return */ private static String formatStringToOct(String str) { if (str.length() % 3 == 2) { return "0" + str; } if (str.length() % 3 == 1) { return "00" + str; } return str; } private static String formatStringHexToBin(String str) { if (str.length() % 4 == 3) { return "0" + str; } if (str.length() % 4 == 2) { return "00" + str; } if (str.length() % 4 == 1) { return "000" + str; } return str; } }
4、十六进制转十进制
注意数据范围,此处int类型有一个数据测试不通过,使用long类型。
思路:使用纯字符的码表相对位置计算。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String str = scan.next();
System.out.println(HexToTen(str));
scan.close();
}
private static long HexToTen(String hex){
int len = hex.length();
long sum = 0;
for (int i = 0; i < len; i++) {
if (hex.charAt(i)>='A' && hex.charAt(i) <= 'F'){
//(hex.charAt(i) - 'A' ) 表示A~F中的字符与A的相对位置 +上10 表示10进制数
sum = sum*16 + hex.charAt(i) - 'A' + 10;
}else {
//(hex.charAt(i) - '0' ) 表示0~9再码表中的字符与‘0’的相对位置
sum = sum * 16 + hex.charAt(i) - '0';
}
}
return sum;
}
}
5、十进制转化为十六进制
- 十进制转化为n进制,则是将数不断求余数,余数倒序输出,使用 栈结构最好,数据先进后出。
思路:拿到数据后循环取余数放入栈中,数据缩小n倍。直到数据的余数为0.
import java.util.Scanner;
import java.util.Stack;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
long number = scan.nextLong();
//排除特殊情况零
if (number == 0) {
System.out.println(0);
return;
}
//使用栈结构
Stack<Object> stack = new Stack<>();
while (number != 0){
long tem = number % 16;
//处理字符部分
if (tem >= 10 && tem <= 15){
switch ((int) tem){
case 10 :
stack.push('A');
break;
case 11:
stack.push('B');
break;
case 12 :
stack.push('C');
break;
case 13 :
stack.push('D');
break;
case 14 :
stack.push('E');
break;
case 15 :
stack.push('F');
break;
}
}else
//否则添加余数
stack.push(tem);
//原来的数据缩小16倍 直到等于0为止
number = number / 16;
}
//遍历输出数据
while (!stack.isEmpty()){
System.out.print(stack.pop());
}
scan.close();
}
}
6、特殊回文数
考点:给定数字取各个位数上的数字,这里数据少不用循环去取,用数组存了。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
opertion(n);
scan.close();
}
private static void opertion(int n){
//五位数字
for (int i = 10000; i <= 99999 ; i++) {
//取每一位上的数字
int a = i / 1 % 10;
int b = i / 10 % 10;
int c = i / 100 % 10;
int d = i / 1000 % 10;
int e = i / 10000 % 10;
//以上方式等同于
/* while (i != 0){
//取余
a = i % 10;
//除10 下一次循环取下一位
i /= 10;
}*/
if (a==e && b == d && a + b + c + d + e == n){
System.out.println(i);
}
}
//六位数字
for (int i = 100000; i <= 999999 ; i++) {
//取每一位上的数字
int a = i / 1 % 10;
int b = i / 10 % 10;
int c = i / 100 % 10;
int d = i / 1000 % 10;
int e = i / 10000 % 10;
int f = i / 100000 % 10;
if (a==f && b == e && a + b + c + d + e + f == n && c == d){
System.out.println(i);
}
}
}
}
- 使用三层循环暴力匹配
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
opertion(n);
scan.close();
}
private static void opertion(int target){
for (int i = 1; i < 10 ; i++) {
for (int j = 0; j < 10; j++) {
for (int k = 0; k < 10; k++) {
if (i+j+k+j+i == target) {
System.out.println(""+i+j+k+j+i);
}
//按理说在这里判断也是可以的 也是满足回文且相等 但是就是不通过
// 题目要求:按从小到大的顺序输出满足条件的整数,每个整数占一行。
/*if (i+j+k+k+j+i == target) {
System.out.println(""+i+j+k+k+j+i);
}*/
}
}
}
//六位数
for (int i = 1; i < 10 ; i++) {
for (int j = 0; j < 10; j++) {
for (int k = 0; k < 10; k++) {
if (i+j+k+k+j+i == target) {
System.out.println(""+i+j+k+k+j+i);
}
}
}
}
}
}
7、回文数
- 问题描述:
1221是一个非常特殊的数,它从左边读和从右边读是一样的,编程求所有这样的四位十进制数。
- 要求:
按从小到大的顺序输出满足条件的四位十进制数。
- 思路:1221以2位数为对称点,故只需要两层for循环即可。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
opertion();
scan.close();
}
private static void opertion(){
for (int i = 1; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println(""+i+j+j+i);
}
}
}
}
- 由于位数比较少,可以把每一位取数出来
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
opertion();
scan.close();
}
private static void opertion(){
for (int i = 1000; i <= 9999 ; i++) {
int a = i / 1 % 10;
int b = i / 10 % 10;
int c = i / 100 % 10;
int d = i / 1000 % 10;
if (a == d && b == c){
System.out.println(""+a+b+c+d);
}
}
}
}
8、特殊的数字
- 题目描述:
153是一个非常特殊的数,它等于它的每位数字的立方和,即153=111+555+333。编程求所有满足这种条件的三位十进制数。
- 题目要求
按从小到大的顺序输出满足条件的三位十进制数,每个数占一行。
- 思路:
由于只是三位数数据比较小,一个for循环就可以解决,分别取出每一位上的数字做平方计算。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
opertion();
scan.close();
}
private static void opertion(){
for (int i = 100; i <= 999 ; i++) {
int tem = i;
int a = tem / 1 % 10;
int b = tem / 10 % 10;
int c = tem / 100 % 10;
if (i == a*a*a + b*b*b + c*c*c){
System.out.println(i);
}
}
}
}
9、杨辉三角形(力扣上出现)
- 题目描述:
杨辉三角形又称Pascal三角形,它的第i+1行是(a+b)i的展开式的系数。它的一个重要性质是:三角形中的每个数字等于它两肩上的数字相加。
给出n,输出它的前n行。
- 输入格式:
输入包含一个数n。 - 输出格式:
输出杨辉三角形的前n行。每一行从这一行的第一个数开始依次输出,中间使用一个空格分隔。请不要在前面输出多余的空格。
-
数据规模: 1 <= n <= 34。
-
思路:
将所有数据对齐后发现,对角线和每行最左边的第一个元素为1,其余的都是当前位置的上一行同一个位置的数,加上上一行最左斜方的数。
- 代码:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
opertion(n);
scan.close();
}
private static void opertion(int n){
//因为要访问上一行的元素 我们需要数组来存储数据
int[][] res = new int[n][n];
for (int i = 0; i <= n-1; i++) {
for (int j = 0; j <= i; j++) {
//判断是否是行头元素 和最后一个元素
//i == i == 0 是为了处理第一个开始的元素
if (i == 0 || j == n - 1 || j == 0) {
res[i][j] = 1;
}else {
//上一层的元素和
res[i][j] = res[i-1][j] + res[i-1][j-1];
}
}
}
//遍历输出
for (int i = 0; i < res.length; i++) {
for (int j = 0; j <= i; j++) {
System.out.print(res[i][j]+ " ");
}
System.out.println();
}
}
}
10、查找整数
- 题目描述
给出一个包含n个整数的数列,问整数a在数列中的第一次出现是第几个。
- 输出格式
如果a在数列中出现了,输出它第一次出现的位置(位置从1开始编号),否则输出-1。
- 数据规模
1 <= n <= 1000
- 思路:
将输入的数据作为可变字符串(节省空间,不用频繁的创建对象,而消耗内存),之后使用字符串API查询目标值。
- 数组暴力查询
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[] arr = new int[n];
for (int i = 0; i < arr.length; i++) {
arr[i] = scan.nextInt();
}
int tar = scan.nextInt();
//标志是否已经找到目标值
boolean isSearch = false;
for (int i = 0; i < arr.length; i++) {
if(arr[i] == tar) {
System.out.println(i+1);
isSearch = true;
break;
}
}
//执行到这里需要判断是否已经找到值
if (!isSearch) {
System.out.println(-1);
}
scan.close();
}
}
- 可以考虑二分法查找(首先得排序)。
11、数列特征
- 题目描述
给出n个数,找出这n个数的最大值,最小值,和。
- 输入格式
第一行为整数n,表示数的个数。
第二行有n个数,为给定的n个数,每个数的绝对值都小于10000。
- 输出格式
输出三行,每行一个整数。第一行表示这些数中的最大值,第二行表示这些数中的最小值,第三行表示这些数的和。
- 样例输入
5
1 3 -2 4 5
- 样例输出
5
1 3 -2 4 5
- 思路
用min,max ,sum 分别表示,一次循环
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = scan.nextInt();
}
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
int sum = 0;
for (int i = 0; i < arr.length; i++) {
min = arr[i] < min ? arr[i] : min;
max = arr[i] > max ? arr[i] : max;
sum += arr[i];
}
System.out.println(max);
System.out.println(min);
System.out.println(sum);
scan.close();
}
}
12、字母图形
- 题目描述
利用字母可以组成一些美丽的图形,下面给出了一个例子:
ABCDEFG
BABCDEF
CBABCDE
DCBABCD
EDCBABC
这是一个5行7列的图形,请找出这个图形的规律,并输出一个n行m列的图形。
- 输入格式
输入一行,包含两个整数n和m,分别表示你要输出的图形的行数的列数。
- 输出格式
输出n行,每个m个字符,为你的图形。
- 样例输入
5 7
- 样例输出
ABCDEFG
BABCDEF
CBABCDE
DCBABCD
EDCBABC
- 数据规模
1 <= n, m <= 26
- 思路
看着输出案例,当i=j时you(i,j)= A,然后往左右两边每一个位置相加一
所以每次都只需要在当前需要初始化的位置初始化A,在求当前位置(i,j)与(i,i)的相对位置,在A的基础上加上相对应的数,再转化为char类型存入。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
char[][] res = new char[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < res[i].length; j++) {
res[i][j] = 'A';
// 当前位置(i,j) 与(i,i)A的位置相对距离
for (int k = Math.abs(j - i); k > 0; k--) {
res[i][j] = (char) (res[i][j] + 1);
}
// if (i < j) {
// for (int k = j - i; k > 0; k--) {
// res[i][j] = (char) (res[i][j] + 1);
// }
//
// } else if (i > j) {
// for (int k = i - j; k > 0; k--) {
// res[i][j] = (char) (res[i][j] + 1);
// }
// }
}
}
// 遍历
for (int i = 0; i < res.length; i++) {
for (int j = 0; j < res[i].length; j++) {
System.out.print(res[i][j]);
}
System.out.println();
}
scan.close();
}
}
13、01字串
- 问题描述
对于长度为5位的一个01串,每一位都可能是0或1,一共有32种可能。它们的前几个是:
00000
00001
00010
00011
00100
请按从小到大的顺序输出这32种01串。
- 考点Integer类的方法使用
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//又题目可知 就是遍历0~32的二进制 不足5为补全前导0到5位
for (int i = 0; i < 32; i++) {
System.out.println(format(Integer.toBinaryString(i)));
}
scan.close();
}
/**
* 格式化字符串 不到5为补全前导0
*
* @param bin
* @return
*/
private static String format(String bin) {
int len = bin.length();
switch (len) {
case 1:
bin = "0000" + bin;
break;
case 2:
bin = "000" + bin;
break;
case 3:
bin = "00" + bin;
break;
case 4:
bin = "0" + bin;
break;
default:
break;
}
return bin;
}
}
14、闰年判断
- 题目描述
给定一个年份,判断这一年是不是闰年。
当以下情况之一满足时,这一年是闰年:
年份是4的倍数而不是100的倍数;
年份是400的倍数。
其他的年份都不是闰年。
- 数据规模与约定
1990 <= y <= 2050
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int year = scan.nextInt();
if (year % 400 == 0) {
System.out.println("yes");
}else if(year % 100 != 0 && year % 4 == 0){
System.out.println("yes");
}else {
System.out.println("no");
}
scan.close();
}
}
15、斐波那契大数求余数
- 问题描述
- 数据规模
1 <= n <= 1,000,000。
- 思考:首先想到的是先把Fn出,再去邱Fn%10007的余数,由于数据规模太大,这昂做数据会溢出。我们得换思路,使用动态规划
- 动态规划步骤:
- 确定dp数组:一维数组还是二维数组?含义是什么?
- 此题dp[i]:表示第i+1个斐波那契数列余10007的值
- 数值较大建议使用long类型,其实Java中Int也能通过。
- 代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
/**
* 使用动态规划 dp[i] 表示 第i+1个斐波那契数余10007的值
*/
int[] dp = new int[n];
// 处理特殊情况 否则 dp[1] = 1; 会越界
if (n == 1) {
System.out.println(1);
return;
}
//初始化
dp[0] = 1;
dp[1] = 1;
//赋值
for (int i = 2; i < n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) % 10007;
}
System.out.println(dp[n - 1]);
scan.close();
}
}
16、圆的面积
- 题目描述
- 思考
Java数学类的使用,浮点表示,格式化输出
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
double res = (double) Math.PI * n * n;
//格式化输出
System.out.printf("%.7f", res);
scan.close();
}
}
17、序列求和
- 题目描述
- 思考:由于数据规模较大,使用常规方法求和数据一定会溢出。使用大数List存储,在使用分治求和。
- 将大数序列拆分成若干个小数序列,每个小数序列的长度不超过一个阈值,这个阈值可以根据具体问题而定。
- 对于每个小数序列,使用合适的方法计算出它们的和。
- 将每个小数序列的和合并得到大数序列的和。
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Main {
private static final int THRESHOLD = 1000; // 阈值,每个小数序列的长度不超过THRESHOLD
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
// 初始化数据
List<BigInteger> nums = new ArrayList<BigInteger>();
for (int i = 1; i <= n; i++) {
nums.add(BigInteger.valueOf(i));
}
System.out.println(sum(nums));
scan.close();
}
/**
* 分治算法实质是递归 需要注意返回条件
*
* @param list
* @return
*/
private static BigInteger sum(List<BigInteger> list) {
// 如果不超过阈值 就属于小序列 计算求和结果
if (list.size() <= THRESHOLD) {
BigInteger sum = BigInteger.ZERO;
for (BigInteger bigInteger : list) {
sum = sum.add(bigInteger);
}
// 计算完毕返回
return sum;
}
// 如果不满足以上条件 就是序列过长需要进行 分治处理
int mid = list.size() / 2;
List<BigInteger> leftNums = list.subList(0, mid);
List<BigInteger> rightNums = list.subList(mid, list.size());
// 分开计算 区间值
BigInteger leftResult = sum(leftNums);
BigInteger rightResult = sum(rightNums);
// 合并返回
return leftResult.add(rightResult);
}
}
数据过大,只通过70%,其余超时。
-
换一种思路,这是一个等差数列,使用高斯求和。
-
前n项和公式为:Sn=na1+n(n-1)d/2或Sn=n(a1+an)/2
import java.util.Scanner;
public class Main {
private static final int THRESHOLD = 1000; // 阈值,每个小数序列的长度不超过THRESHOLD
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
long sum = 0;
long n = scan.nextLong();
// 简称 (上底 + 下底) * 高 / 2
// (n + 1) * n / 2
// sum = (1 + n) * n / 2;
sum = n + n * (n - 1) / 2;
System.out.println(sum);
scan.close();
}
}
悲哀啊!忘记数学公式了~~
18、大数阶乘
- 题目描述
- 数据规模表较小可以考虑Java大数暴力求解。
import java.math.BigInteger;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
long sum = 0;
long n = scan.nextLong();
BigInteger factorial = BigInteger.ONE;
for (int i = 1; i <= n; i++) {
factorial = factorial.multiply(new BigInteger("" + i));
}
System.out.println(factorial);
scan.close();
}
}
19、 Huffuman树求过程代价
- 题目描述
- 思路
由于题目中构造的方法原理的每次从数列中取出两个最小值相加得到代价。仔细观察题目初始的数据并不是有规律(按大小排序)、
取最小值的方法可以每次求得最代价之后存入两者较小数的第一位,记录下这一为代价。将另一个较小值复制为0.再一次排序。一次遍历完数组。
- 实现
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[] res = new int[n];
for (int i = 0; i < res.length; i++) {
res[i] = scan.nextInt();
}
//查看初始化数据是否正确
//System.out.println(Arrays.toString(res));
//记录代价值
int sum = 0;
//System.out.println("第一次排序后:" + Arrays.toString(res));
for (int i = 0; i < res.length - 1; i++) {
// 排序
Arrays.sort(res);
//计算代价值存入前者
res[i] = res[i] + res[i + 1];
// System.out.println("res[" + i + "]=" + res[i]);
//累计代价
sum += res[i];
res[i + 1] = 0;
// System.out.println("循环中每一次排序后:" + Arrays.toString(res));
}
System.out.println(sum);
scan.close();
}
}
20、2n皇后问题(使用回溯太难,首先得学N皇后问题)
21、报时助手
- 题目描述
- 思考
看到题目感觉,好复杂数据很多。但是仔细一想不就是一个数字对应一个英文,
0-20是对应的,30 40 50 也是对应的;其余的就是拼接,例如23 59 这种就是
twenty three fifty nine 这种格式;这就涉及到了数字位上的字符处理。考到了Map的使用。
其实这道题就是考耐心,初始化数据麻烦一点。
- 代码
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int hh = scan.nextInt();
int mm = scan.nextInt();
Map<Integer, String> englishTimeExpres = initMap();
// 判断是否是0点
if (mm == 0) {
System.out.println(NumericConversionCharacter(hh, englishTimeExpres) + " o'clock");
} else {
// 处理小时
String resHH = NumericConversionCharacter(hh, englishTimeExpres);
// 处理分钟
String resMM = NumericConversionCharacter(mm, englishTimeExpres);
System.out.println(resHH + " " + resMM);
}
scan.close();
}
/**
* 初始化数据
* @return
*/
private static Map<Integer, String> initMap() {
Map<Integer, String> englishTimeExpres = new HashMap<>();
englishTimeExpres.put(0, "zero");
englishTimeExpres.put(1, "one");
englishTimeExpres.put(2, "two");
englishTimeExpres.put(3, "three");
englishTimeExpres.put(4, "four");
englishTimeExpres.put(5, "five");
englishTimeExpres.put(6, "six");
englishTimeExpres.put(7, "seven");
englishTimeExpres.put(8, "eight");
englishTimeExpres.put(9, "nine");
englishTimeExpres.put(10, "ten");
englishTimeExpres.put(11, "eleven");
englishTimeExpres.put(12, "twelve");
englishTimeExpres.put(13, "thirteen");
englishTimeExpres.put(14, "fourteen");
englishTimeExpres.put(15, "fifteen");
englishTimeExpres.put(16, "sixteen");
englishTimeExpres.put(17, "seventeen");
englishTimeExpres.put(18, "eighteen");
englishTimeExpres.put(19, "nineteen");
englishTimeExpres.put(20, "twenty");
englishTimeExpres.put(30, "thirty");
englishTimeExpres.put(40, "forty");
englishTimeExpres.put(50, "fifty");
return englishTimeExpres;
}
/**
* 判断数据是否小于二十 小于直接返回num 否者返回负一
*
* @param num
* @return
*/
private static int dealNum(int num) {
if ((num >= 0 && num <= 20) || num == 30 || num == 40 || num == 50) {
return num;
} else {
return -1;
}
}
/**
* 从map中取出元素 拼接成对应的字符串 返回
*
* @param num
* @param map
* @return
*/
private static String NumericConversionCharacter(int num, Map<Integer, String> map) {
if (dealNum(num) != -1) {
return map.get(num);
} else {
// 此处的数字一定大于20 除掉30 40 50 题目没有要求 这样表示 four zero 这种表示
// 0 30 --> zero thirty
// 经过程序执行此处必不可能为0
int tem = 0;
int decade = 0;
// 求余数取个位
tem = num % 10;
// 取十位上的数据
decade = num / 10;
/*
* 23 59 twenty three fifty nine
*/
// 乘以10 为了恢复成20 30 等这种数据,比如23 经过上面的取余拿到3,再
// 除10并使用int就是为了去掉个位数,取到十位上的数字2 我们最后要恢复成20
return map.get(decade * 10) + " " + map.get(tem);
}
}
}
22、回形取数
22.1 方法一–模拟
- 题目描述
- 思考(同下题)
不同在于,方向不一样而已。
- 代码
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int m = scan.nextInt();
int n = scan.nextInt();
int[][] matrix = new int[m][n];
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
matrix[i][j] = scan.nextInt();
}
}
// 定义一个辅助矩阵
int rows = matrix.length, columns = matrix[0].length;
boolean[][] visited = new boolean[rows][columns];
int total = rows * columns;
int row = 0, column = 0;
// 定义方向数组 分别表示向下、向右、向上、向左
int[][] directions = { { 1, 0 }, { 0, 1 }, { -1, 0 }, { 0, -1 } };
// 定义方向开始位置
int directionIndex = 0;
// 计数
int count = 0;
while (count < total) {
System.out.print(matrix[row][column] + " ");
// 设置该位置为true 已经访问
visited[row][column] = true;
// 更新索引到下一个位置
int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1];
// 判断 列是否越过右边界 行是否越过最低行 行是否越过上界 列是否越过左边界
if (nextColumn >= columns || nextRow >= rows || nextRow < 0 || nextColumn < 0
|| visited[nextRow][nextColumn]) {
// 越界 或者被访问了 换方向
directionIndex = (directionIndex + 1) % 4;
}
// 由于上一个越界处理好后 我们继续移动
row += directions[directionIndex][0];
column += directions[directionIndex][1];
// 进入下一次
count++;
}
scan.close();
}
}
- 时间复杂度同下
22.1 方法二–按层模拟
- 思路:
23、螺旋矩阵(力扣中等)
- 题目描述:
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
采用一个方向数组,当前数组中的索引坐标不断加上方向数组即可完成旋转(前提是没有越界,超过原来的二维数组,超过、访问过就转向继续移动。
-
解题步骤
- 创建返回值对象
- 判断给定数组是否合法,不合法返回该空对象
- 定义一个与所求数组等大的Boolean数组visited,默认值为false
- 定义方向数组directions,分别表示分别表示向右、向下、向左、向上
- 循环遍历二维数组
-
代码
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<Integer>();
//判断集合数组是否合法
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return res;
}
//定义一个辅助矩阵
int rows = matrix.length, columns = matrix[0].length;
boolean[][] visited = new boolean[rows][columns];
int total = rows * columns;
int row = 0,column = 0;
//定义方向数组 分别表示向右、向下、向左、向上
int[][] directions = {{0,1},{1,0},{0,-1},{-1,0}};
//定义方向开始位置
int directionIndex = 0;
//计数
int count = 0;
while (count < total) {
//将访问过的元素添加到容器中
res.add(matrix[row][column]);
//设置该位置为true 已经访问
visited[row][column] = true;
//更新索引到下一个位置
int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1];
//判断 列是否越过右边界 行是否越过最低行
//行是否越过上界 列是否越过左边界
if (nextColumn >= columns
|| nextRow >= rows ||
nextRow < 0 || nextColumn < 0
|| visited[nextRow][nextColumn] ) {
//越界 或者被访问了 换方向
directionIndex = (directionIndex + 1) % 4;
}
//由于上一个越界处理好后 我们继续移动
row += directions[directionIndex][0];
column += directions[directionIndex][1];
//进入下一次
count++;
}
return res;
}
- 时间复杂度分析
本题时间复杂度为O(m * n), m ,n 分别是二维数组的行数和列数。由于二维数组中的每个元素都会被访问一次,数组的元素个数为n*m,因此,时间复杂度取决于矩阵的大小。
- 空间复杂度分析
本题额外使用了m*n的额外数组,记录当前元素是否被访问过。有m * n个元素,还需要一个一维列表 directions来存储访问矩阵的顺序,其长度也不会超过 m * n,因此额外的空间复杂度为 O(m * n),所以空间复杂度为O(m * n)。
24、龟兔赛跑预测
- 题目描述
- 输入输出格式
- 样例
- 思考
根据题目所说,兔子领先t米后会休息s秒,在这期间我们让乌龟继续跑,兔子的已经跑路程
不变,由于乌龟的路程一直在变,所以乌龟所跑一次就需要判断一次是否产生了胜利者。在兔子没有休息的时候兔子和乌龟的已走路程都在变化,也需要判断是否产生了胜利者。
- 代码实现
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// v1, v2, t, s,
// 兔子速度 乌龟速度 兔领先秒数 ——>兔子休息s秒
// l 赛道长度
int v1 = scan.nextInt();
int v2 = scan.nextInt();
int t = scan.nextInt();
int s = scan.nextInt();
int l = scan.nextInt();
int rabbitL = 0;
int tortoiseL = 0;
int seconds = 0;
while (true) {
// 比赛默认进行
Boolean isCompetition = true;
// 开始奔跑
rabbitL += v1;
tortoiseL += v2;
seconds++;
// 产生胜利者 比赛结束
if (isWhoWin(rabbitL, tortoiseL, seconds, l)) {
break;
}
// 兔子领先t秒 兔子休息乌龟不休息
if (rabbitL - tortoiseL >= t) {
for (int i = 0; i < s; i++) {
// 兔子不跑 只有乌龟
tortoiseL += v2;
seconds++;
// 乌龟没跑一下判断 是否到达
Boolean isWin = isWhoWin(rabbitL, tortoiseL, seconds, l);
if (isWin) {
// 产生胜利者 比赛结束
isCompetition = false;
break;
}
}
// 判断比赛是否结束
if (!isCompetition) {
break;
}
}
}
scan.close();
}
/**
* 返回true说明已经产生了胜利者 false:继续比赛
*
* @param rabbitL
* @param tortoiseL
* @param seconds
* @param l
* @return
*/
private static boolean isWhoWin(int rabbitL, int tortoiseL, int seconds, int l) {
// 判断是否到达
if (rabbitL >= l && tortoiseL < l) {
System.out.println("R");
System.out.println(seconds);
return true;
}
if (tortoiseL >= l && rabbitL < l) {
System.out.println("T");
System.out.println(seconds);
return true;
}
if (tortoiseL >= l && rabbitL >= l) {
System.out.println("D");
System.out.println(seconds);
return true;
}
return false;
}
}
25、芯片测试
- 题目描述
- 思考
基于题目所说,好芯片大于一半,好的芯片测出来的是1,而坏的芯片测出来的结果可能是1,0不确定。且对角线i==j永为1.
这是二维数组表示(i,j)表示,用i芯片去测试j芯片的结果,体重要求芯片编号从1开始,数组中索引为0开始,所以第一i行表示,用(i+1)号芯片测试j+1号芯片的结果。
我们得这样想,假如所有的芯片都是好的,那么所测出来的结果肯定都是1
1 | 1 | 1 | 1 |
---|---|---|---|
1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 |
这里会发现他们的测试结果(每一行)都是1.假如第二个芯片坏了。
1 | 0 | 1 | 1 |
---|---|---|---|
0 | 1 | 1 | 0 |
1 | 0 | 1 | 1 |
1 | 0 | 1 | 1 |
这里可以看出第二块芯片是坏的,因为测试数据与其他的都不一样l,是吧。
题目有告诉我们坏的芯片可能会测出0和1的结果,也就是号的和坏的是吧。那假如用它来测试其他芯片的结果恰好产生了正确的测试结果,那不就和其他的一样了吗。这里不会出现这样的情况 。因为对角线恒伟1.就算它残生了器他芯片是正确的也不会和其他正确的芯片所测说出来的结果一样。因为正确的芯片在测试坏的芯片时,它的值只能为0.接下来请看图。
假设第二块芯片还是坏的,也就是第二行.
1 | 0 | 1 | 1 |
---|---|---|---|
1 | 1 | 1 | 1 |
1 | 0 | 1 | 1 |
1 | 0 | 1 | 1 |
所以看出规律了吗,我们只要在二维数组中找到相同一维数组的数量大于一半n/2,就默认在着相同的一维数组中(其中一个)的非零值的索引加一就是芯片的编号。这里用j+1表示更容易。
- 代码
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[][] res = new int[n][n];
for (int i = 0; i < res.length; i++) {
for (int j = 0; j < res[0].length; j++) {
res[i][j] = scan.nextInt();
}
}
for (int i = 0; i < n; i++) {
int x = 0;
int[] b = new int[n];
for (int j = 0; j < n; j++) {
int y = 0;
for (int t = 0; t < n; t++) {
if (res[i][t] == res[j][t]) {
y++;
} else {
break;
}
}
if (y == n) {
b[x++] = j + 1;
}
}
if (x >= n / 2) {
for (int j = 0; j < x; j++) {
System.out.print(b[j] + " ");
}
break;
}
}
scan.close();
}
}
- 这种是正常的解法,还有一种就是,因为对角线的值很为1,我们发现在测试数据中第一块和最后一块永远是正确的。也是基于上面的想法,只要两块芯片都是好的,他们的互相测试的结果都是一样的,且都是1,简而言之,只要是沿着做斜对角线对称且值为1的就是好芯片,所以我们课在左斜上三角遍历对称比较就行,遍历整个矩阵就会重复。这样就会出现第一个个芯片和最后一个芯片是特殊情况。没有人和他们对称,他们与自己对称,它们两个永远是好的。这个方法可能有局限性,基于这道题目的册十数据来说是能够通过的。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int[][] res = new int[n][n];
for (int i = 0; i < res.length; i++) {
for (int j = 0; j < res[0].length; j++) {
res[i][j] = scan.nextInt();
}
}
int index = 0;
for (int i = 0; ;) {
for (int j = 0; j < res[i].length - index; j++) {
//
if (i == 0 && j == 0) {
System.out.print(j + 1 + " ");
}
if (i != j && res[i][j] != 0 && res[j][i] != 0 && res[i][j] == res[j][i]) {
System.out.print(j + 1 + " ");
}
if (i == n - 1 && j == n - 1) {
System.out.print(j + 1 + " ");
break;
}
}
index++;
break;
}
scan.close();
}
}
26、 FJ的字符串(简单动态规划)
- 题目描述
- 思考
仔细观察会发现,后者与前者有关; Ai = Ai-1 + 字母表中的第i个字母 + Ai-1(i大于1),所以此题用动态规划比较适合。
A1=“A"
A2 = “ABA” = A1 + B + A1
……
Ai = Ai-1 + 字母表中的第i个字母 + Ai-1 (i大于1)
A在ASCII码表中的位置是 65,这样我们就可以用循环拼接字符串了。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
String[] dp = new String[n];
//初始化
dp[0] = "A";
for (int i = 1; i < n; i++) {
dp[i] = dp[i - 1] + (char) ('A' + i) + dp[i - 1];
}
System.out.println(dp[n - 1]);
scan.close();
}
}
这样做显然是可以通过但是呢,由于String是不可变字符,每一次操作都会产生对象。开辟了一个n的字符串数组,导致空间复杂度为O(n),时间复杂度为O(n),其实这里是可以优化一下空间复杂度的。只需要O(1)的空间复杂度即可完成。
那就是使用两个循环变量来迭代循环。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
a = "A";
String b = a;
for (int i = 1; i < n; i++) {
b = a + (char) ('A' + i) + a;
//将其复制给a,方便下一次循环调用前者来拼接
a = b;
}
System.out.println(b);
scan.close();
}
}
27、Sine之舞
- 题目描述
- 思考
开始看这题目挺难,多读几遍发现是找规律。解释代码中有
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt(); // 需要计算的项数
printSnExpression(n);
scan.close();
}
// Sn=(...(A1+n)A2+n-1)A3+...+2)An+1
// 求Sn
// 输入3的案例:((sin(1)+3)sin(1–sin(2))+2)sin(1–sin(2+sin(3)))+1
public static void printSnExpression(int n) {
String str = "";
// 拼接前n个括号 分析可得Sn前会有n-1个符号
for (int i = 1; i < n; i++) {
str += "(";
}
//添加前导括号完成后 找出Sn中有这样的规律,每次都会在后面追加 A1+n)
//但是最后一个略有不同,如果不处理An+1)这里的括号就会与案例中的输出不一致
for (int i = 1; i <= n; i++) {
if (i != n) {
//不是最后一个
str = str + printAnExpression(i) + "+" + (n - i + 1) + ")";
} else {
//是最后一个
str = str + printAnExpression(i) + "+" + (n - i + 1);
}
}
//输出结果
System.out.println(str);
}
// An=sin(1–sin(2+sin(3–sin(4+...sin(n))...)
// 求An的表达式:仔细观察就会发现 每次都在字符串后面追加 sin(i + 符号
//再仔细观察发现 i为奇数时就是- ,i为偶数时就是+ ,最后一个没有符号
public static String printAnExpression(int n) {
String str = "";
for (int i = 1; i <= n; i++) {
//判断是不是奇数
if (i % 2 != 0) {
// 是不是最后一个 最后一个就没有符号
if (i == n) {
//最后一个
str = str + "sin(" + i;
} else {
//非最后一个
str = str + "sin(" + i + "-";
}
} else if (i != n) {
// 不是最后一个就有加号
str = str + "sin(" + i + "+";
} else {
str = str + "sin(" + i;
}
}
// 追加尾部n个括号 由于左边一直没有出现右括号 所以在最后一定会有n个括号
for (int i = 1; i <= n; i++) {
str = str + ")";
}
return str;
}
}
- 代码优化分析:
由于String是不可变字符串,所以每一次拼接,操作都会产生一个新的对象,极大花费内存。我们可以在这里改变为StringBuilder可变字符串,每次操作都是在原来的基础上修改,节省了很多的内存开销。
- 优化后
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt(); // 需要计算的项数
printSnExpression(n);
scan.close();
}
// Sn=(...(A1+n)A2+n-1)A3+...+2)An+1
// 求Sn
// 输入3的案例:((sin(1)+3)sin(1–sin(2))+2)sin(1–sin(2+sin(3)))+1
public static void printSnExpression(int n) {
StringBuilder str = new StringBuilder();
// 拼接前n个括号 分析可得Sn前会有n-1个符号
for (int i = 1; i < n; i++) {
str.append("(");
}
// 添加前导括号完成后 找出Sn中有这样的规律,每次都会在后面追加 A1+n)
// 但是最后一个略有不同,如果不处理An+1)这里的括号就会与案例中的输出不一致
for (int i = 1; i <= n; i++) {
if (i != n) {
// 不是最后一个
str.append(printAnExpression(i)).append("+").append(n - i + 1).append(")");
} else {
// 是最后一个
str.append(printAnExpression(i)).append("+").append(n - i + 1);
}
}
// 输出结果
System.out.println(str.toString());
}
// An=sin(1–sin(2+sin(3–sin(4+...sin(n))...)
// 求An的表达式:仔细观察就会发现 每次都在字符串后面追加 sin(i + 符号
// 再仔细观察发现 i为奇数时就是- ,i为偶数时就是+ ,最后一个没有符号
public static String printAnExpression(int n) {
StringBuilder str = new StringBuilder();
for (int i = 1; i <= n; i++) {
// 判断是不是奇数
if (i % 2 != 0) {
// 是不是最后一个 最后一个就没有符号
if (i == n) {
// 最后一个
str.append("sin(").append(i);
} else {
// 非最后一个
str.append("sin(").append(i).append("-");
}
} else if (i != n) {
// 不是最后一个就有加号
str.append("sin(").append(i).append("+");
} else {
str.append("sin(").append(i);
}
}
// 追加尾部n个括号 由于左边一直没有出现右括号 所以在最后一定会有n个括号
for (int i = 1; i <= n; i++) {
str.append(")");
}
return str.toString();
}
}
28、字符串对比
- 题目描述:
- 思考:
这题就是简单的考了字符串的简单处理和分支语句判断。和一些API的使用。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String s1 = scan.next();
String s2 = scan.next();
if (s1.length() != s2.length()) {
System.out.println(1);
}else if (s1.equals(s2)) {
System.out.println(2);
}else if (s1.equalsIgnoreCase(s2)) {
System.out.println(3);
}else {
System.out.println(4);
}
scan.close();
}
}
29、分解质因数
29.1 埃拉托斯特尼筛法
埃拉托斯特尼筛法(Sieve of Eratosthenes)是一种简单而高效的素数筛法,可以用来找出小于等于n的所有素数。
- 算法思想
从2开始,将每一个素数的倍数标记成合数,然后找到下一个未标记的数,将其作为素数,继续进行标记,直到筛选范围内的所有数都被标记过。
- 题目描述
- 思考
判断一个数是否是质数,且要求的数能否被该质素整除,能就凭借在字符串中。每拼接一次要求的数除以当前质数为1时结束循环。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int a = scan.nextInt();
int b = scan.nextInt();
for (int i = a; i <= b; i++) {
resolveZhiShu(i);
}
scan.close();
}
/**
* 题意:在ab区间的任意数等于con二开始的所有质数积
*
* @param n
*/
public static void resolveZhiShu(int n) {
boolean isFirst = true;
int minZhishu = 2;
StringBuilder builder = new StringBuilder(n + "=");
// 4 = 2 * 2 * 1 到1 就可以节数循环 不用拼接了
while (n > 1) {
if (n % minZhishu == 0) {
if (isFirst) {
isFirst = false;
builder.append(minZhishu);
} else {
builder.append("*" + minZhishu);
}
n /= minZhishu;
} else {
while (!isZhiShu(++minZhishu)) {
}
}
}
System.out.println(builder.toString());
}
/**
* 判断是否是素数(质素)
*
* @param n
* @return
*/
public static boolean isZhiShu(int n) {
for (int i = 2; i * i <= n; i++) {
if (n % i == 0)
return false;
}
return true;
}
}
二、回溯算法
1、回溯介绍
- 什么叫回溯法?
回溯法也叫回溯搜索法,它是一种搜索的方式。回溯是递归的衍生产品,只要是递归就一定会有回溯。也就是说,回溯函数也就是递归函数,指的是一个函数。
- 回溯算法的效率
会所算法很抽象,但其实并不是什么很高深的算法。其本质上就是暴力穷举,穷举所有的可能,然后选出我们的答案,如果想提高回溯算法的效率,可以加一些剪枝的操作,但也是改变不了回溯的本质。
- 回溯法解决的问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按照一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少个符合条件的子集
- 排列问题:N个数按照一定规则的去全排列,有几种排列的方式
- 棋盘问题:N皇后,解数独等问题
- 何为组合?何为排列?
组合是不强调元素的顺序,排列是强调元素的排列顺序的。
例如:{1,2}和{2,1}在组合上,就是一个集合,因为不强调顺序,二排列的话就是要强调顺序,{1,2}和{2,1}是不同的集合。
组合无序,排列有序。
-
回溯法模板
- 回溯函数模板返回值及参数,在回溯算法中,习惯函数起名为backtracking,也可以为别的。但返回值一般为void 。
void backtracking(参数)
- 回溯函数终止条件
if (终止条件) { 存放结果; return; }
- 回溯算法模板框架
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
-
如何理解回溯法
回溯法解决的问题度介意抽象成树形结构。
回溯法解决的都是在集合中递归查找子集,集合的大小就构成数的宽度,递归的深度,就构成了树的深度。
递归一定是有种植条件的,所以必然是一棵高度有限度的树(N叉树)。
参数在一开始不确定,后期需要什么就传入什么。
从树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
2、回溯题目分类
2.1 第77题:组合
-
力扣题目链接:https://leetcode.cn/problems/combinations/
-
题目描述
- 思路
直接的解法当然是使用for循环,例如示例中k=2,就很容易想到for循环实现,也可以达到题目要求的结果:
int n = 4;
for (int i = 1 ; i <= n ; i++) {
for (int j = i+1; j <= n; j++) {
System.out.println(i+""+j);
}
}
n = 100, k = 3 那么就三层for循环,代码如下:
int n = 100;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
for (int u = j + 1; u <= n; n++) {
System.out.println(i+""+j+u);
}
}
}
这还可以接受,可是如果n为100,k为50呢?要用50个循环??n为1000,k为200呢?
所以显然这样的方式是行不通的。
So?
就用到了回溯法,虽然回溯也是暴力搜索,但是至少能解出来,而不是for循环让人绝望。
要解决 n为100,k为50的情况,暴力写法需要嵌套50层for循环,那么回溯法就用递归来解决嵌套层数的问题
递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了。
分析结构
- 回溯实现
import java.util.ArrayList;
import java.util.List;
public class sulation {
public List<List<Integer>> combine(int n, int k) {
List<Integer> arr = new ArrayList<Integer>();
List<List<Integer>> res = new ArrayList<>();
backstring(n,k,1,res,arr);
return res;
}
private void backstring(int n,int k,int statindex,List<List<Integer>> res,List<Integer> arr){
//终止条件
if(arr.size() == k){
res.add(new ArrayList<>(arr));
return;
}
//递归
for (int i = statindex; i <= n; i++) {
arr.add(i);
backstring(n,k,i+1,res,arr);
arr.remove(arr.size()-1);
}
}
}
- 部分解释
backstring
方法的作用是递归求解所有符合要求的组合,并将它们添加到res
列表中。
backstring
方法首先判断当前arr
列表中是否已经有k个数了。如果是,则将arr
列表添加到res
列表中,然后返回。
如果arr
列表中的元素数量还不足k个,那么就从当前位置statindex
开始,循环遍历1~n范围内的所有数。
对于每个遍历到的数i,将它添加到arr
列表中,然后递归调用backstring
方法,传递参数n、k和i+1。这里传递i+1是为了保证组合中的数字是递增的,避免重复的组合出现。
在递归调用backstring
方法之后,需要将最后添加的元素从arr
列表中删除,以便进行下一次循环遍历。
最终,backstring
方法会递归求解所有符合要求的组合,并将它们添加到res
列表中。最后,combine
方法返回res
列表,即为所有符合要求的组合。
3、组合问题剪枝操作
- 题目描述:同上。
- 思路
举个梨子,
我们假设n=4,k=4,我们画图来看,发现哪一些步骤是多余的。
第一层for循环的时候,从元素2开始遍历都没有意义了,从2开始后面不论怎么取都拿不到要求的k个数的组合。所以我们把它剪掉不需要遍历。
画×的分支是永远不可能达到题目所要求的k个数的集合的,所以直接去掉,不进行遍历。
图中的么一个节点(矩形),就代表一个for循环,就会发现每一层从第二个数开始遍历都是无效遍历!
所以,可以剪枝的地方就在每一层递归中的for循环中的起始位置。如果for循环的起始位置之后的元素个数加上已经取到的元素不足k个,则就没必要遍历了。
注意代码中i,就是for循环里选择的起始位置。
for (int i = startIndex; i <= n; i++) {
- 优化过程
- 当前已经选择的元素个数:path.size()
- 所需要的元素个数为:k - path.size()
- 列表中所剩余的元素(n-i) >= 所需要的元素个数(k - path.size())
- 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
- 对于找边界的方法理解
path.size():已经找到的个数。
k-path.size():还需要找的个数
在[x,n]的区间上的数组长度起码是在k - path.size()才有继续搜索的可能,那么就有
n - x + 1 = k - path.size();此处的 n - x + 1 表示:在该区间上总的元素个数
解上列方程得:
x = n + 1 -(k - path.size())
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。
所以优化之后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
- 代码实现
import java.util.ArrayList;
import java.util.List;
public class sulation {
public List<List<Integer>> combine(int n, int k) {
// 初始化容器
// 所访问的路径 元素
List<Integer> path = new ArrayList<Integer>();
// 返回的元素结果集
List<List<Integer>> result = new ArrayList<>();
backstring(n, k, 1, path, result);
return result;
}
private void backstring(int n, int k, int start, List<Integer> path, List<List<Integer>> result) {
// 终止条件
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
i为本次搜索的起始位置
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
// 处理节点
path.add(i);
backstring(n, k, i, path, result);
回溯,撤销处理的节点
path.remove(path.size() - 1);
}
}
}
- 题目描述:同上。
- 思路
举个梨子,
我们假设n=4,k=4,我们画图来看,发现哪一些步骤是多余的。
第一层for循环的时候,从元素2开始遍历都没有意义了,从2开始后面不论怎么取都拿不到要求的k个数的组合。所以我们把它剪掉不需要遍历。
[外链图片转存中…(img-9EDP1lLj-1680651749739)]
画×的分支是永远不可能达到题目所要求的k个数的集合的,所以直接去掉,不进行遍历。
图中的么一个节点(矩形),就代表一个for循环,就会发现每一层从第二个数开始遍历都是无效遍历!
所以,可以剪枝的地方就在每一层递归中的for循环中的起始位置。如果for循环的起始位置之后的元素个数加上已经取到的元素不足k个,则就没必要遍历了。
注意代码中i,就是for循环里选择的起始位置。
for (int i = startIndex; i <= n; i++) {
- 优化过程
- 当前已经选择的元素个数:path.size()
- 所需要的元素个数为:k - path.size()
- 列表中所剩余的元素(n-i) >= 所需要的元素个数(k - path.size())
- 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
- 对于找边界的方法理解
path.size():已经找到的个数。
k-path.size():还需要找的个数
在[x,n]的区间上的数组长度起码是在k - path.size()才有继续搜索的可能,那么就有
n - x + 1 = k - path.size();此处的 n - x + 1 表示:在该区间上总的元素个数
解上列方程得:
x = n + 1 -(k - path.size())
[外链图片转存中…(img-gtsf6otl-1680651749740)]
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。
所以优化之后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
- 代码实现
import java.util.ArrayList;
import java.util.List;
public class sulation {
public List<List<Integer>> combine(int n, int k) {
// 初始化容器
// 所访问的路径 元素
List<Integer> path = new ArrayList<Integer>();
// 返回的元素结果集
List<List<Integer>> result = new ArrayList<>();
backstring(n, k, 1, path, result);
return result;
}
private void backstring(int n, int k, int start, List<Integer> path, List<List<Integer>> result) {
// 终止条件
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
i为本次搜索的起始位置
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
// 处理节点
path.add(i);
backstring(n, k, i, path, result);
回溯,撤销处理的节点
path.remove(path.size() - 1);
}
}
}