3.17
输入输出
(0条未读通知) 牛客竞赛ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛牛客竞赛OJ (nowcoder.com)
一系列数的和
import java.util.Scanner; /* 输入描述: 输入的第一行包括一个正整数t(1 <= t <= 100), 表示数据组数。 接下来t行, 每行一组数据。 每行的第一个整数为整数的个数n(1 <= n <= 100)。 接下来n个正整数, 即需要求和的每个正整数。 输出描述: 每组数据输出求和的结果 */ public class Main { public static void main(String[] args){ Scanner in = new Scanner(System.in); int num = in.nextInt(); for(int i = 0; i < num; i++){ // 进来每行 int sum = 0; int n = in.nextInt(); // 每行第一个数之后 for(int j = 0;j < n;j++){ // 累加 sum += in.nextInt(); } System.out.println(sum); } } }
import java.util.Scanner; /* 输入描述: 输入数据有多组, 每行表示一组输入数据。 每行的第一个整数为整数的个数n(1 <= n <= 100)。 接下来n个正整数, 即需要求和的每个正整数。 输出描述: 每组数据输出求和的结果 */ public class Main { public static void main(String[] args){ Scanner in = new Scanner(System.in); while (in.hasNext()){ int n = in.nextInt(); int sum = 0; for(int i = 0; i < n; i++){ int cur = in.nextInt(); if(cur > 0){ sum += cur; } } System.out.println(sum); } } }
import java.util.Scanner; /* 输入描述: 输入数据有多组, 每行表示一组输入数据。 每行不定有n个整数,空格隔开。(1 <= n <= 100)。 输出描述: 每组数据输出求和的结果 */ public class Main { public static void main(String[] args) { Scanner input = new Scanner(System.in); while (input.hasNextLine()){ String[] lines = input.nextLine().split(" "); int count = 0; // 也可以写成 for(String s: lines){ for(int i = 0; i < lines.length; i++){ count += Integer.parseInt(lines[i]); } System.out.println(count); } } }
字符串排序
sort
Arrays.sort(lines);
import java.util.Scanner; import java.util.Arrays; public class Main{ public static void main(String[] args){ Scanner in = new Scanner(System.in); while(in.hasNextLine()){ String[] lines = in.nextLine().split(" "); Arrays.sort(lines); String sortedString = String.join(" ", lines); System.out.println(sortedString); } } }
快速排序
快速排序过程解释:
选择基准(Pivot):快速排序首先从数组中选择一个元素作为基准值。在你的代码中,基准是每次递归调用的最后一个元素
arr[high]
。分区(Partitioning):数组被重新排列,所有比基准值小的元素都移动到基准的左边,所有比基准值大的元素都移动到基准的右边。这个操作结束时,基准值位于其最终位置。这个过程称为分区。
在分区过程中,我们从数组的一端开始,用一个索引
i
来跟踪比基准小的正确区域的边界。对于每个元素,如果它小于基准,我们就把它和i
索引处的元素交换,然后增加i
。这样确保了i
左边的所有元素都不大于基准。递归排序:分区操作结束后,基准位于其最终位置。然后,递归地对基准左边和右边的子数组进行快速排序。
import java.util.Arrays; import java.util.Scanner; /* 输入描述: 输入有两行,第一行n 第二行是n个字符串,字符串之间用空格隔开 输出描述: 输出一行排序后的字符串,空格隔开,无结尾空格 */ public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = Integer.parseInt(in.nextLine()); // 将输入的字符串分割成字符串数组 String[] lines = in.nextLine().split(" "); if(lines.length != n){ System.out.println("缺少输入"); return; } // 排序 // Arrays.sort(lines); // 或者手写快速排序 fastSort(lines, 0, n-1); String sorted = String.join(" ", lines); System.out.println(sorted); } private static void fastSort(String[] lines, int left, int right){ if(left < right){ int pivot = compareIndex(lines, left, right); fastSort(lines, 0, pivot -1); fastSort(lines, pivot+1, right); } } private static int compareIndex(String[] lines, int left, int right){ int i = left - 1; for(int j = left; j < right; j ++){ if(lines[j].compareTo(lines[right]) < 0){ // 如果当前数比最右侧的小,那么就向后移动 i++; String temp = lines[i]; lines[i] = lines[j]; lines[j] = temp; } } // 分区之后,应该把其正确位置找出来,这样每次都找到基准该在的地方 String temp = lines[right]; lines[right] = lines[i+1]; lines[i+1] = temp; return i + 1; } }
注意细节(快速排序)
-
分区,使得小于基准的放左侧,然后把基准移动到“左侧”的终点,这就是基准实际该在的位置,使得基准移动后的“右侧”都大于基准
这里分区的时候,
i
依次移动保证i
以及i
左边的数都是小于pivot的;j
用来遍历所有pivot
前的元素
-
if (arr[j].compareTo(pivot) < 0) { i++; // swap arr[i] and arr[j] String temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
-
String pivot = arr[high];
: 这行代码将数组最后一个元素设为基准值pivot
。 -
int i = (low - 1);
: 初始化变量i
为low-1
,这个变量会用来追踪比pivot
小的元素的最右边界。 -
对于每个
j
从low
到high-1
的遍历:if (arr[j].compareTo(pivot) < 0)
: 这个条件检查当前元素arr[j]
是否小于基准值pivot
-
如果是,我们首先通过
i++
将i
向右移动一位。这是因为,我们要找到一个位置为arr[i]
,使得所有在arr[i]
左边的元素都小于pivot
。每当我们找到一个小于pivot
的元素时,我们就需要将其与arr[i]
所在位置的下一个位置交换,因此需要先将i
加1。 -
然后,执行交换操作,将
arr[j]
(当前小于pivot
的元素)与arr[i]
交换。这样做确保了所有小于pivot
的元素都被移动到了数组的左边。
-
-
在遍历完成后,
i+1
的位置是基准值pivot
应该所在的正确位置(因为i
的位置是最后一个小于pivot
的元素的位置),因此将pivot
与arr[i+1]
交换,以确保pivot
位于其最终位置,这样pivot
左边的所有元素都比它小,右边的所有元素都不小于它。
-
这里分区的时候, i
依次移动保证i
以及i
左边的数都是小于pivot的; j
用来遍历所有 pivot
前的元素
华为机试【入门】
HJ7 取近似值
-
注意
public static void main(String[] args) {
而不是“Main” -
注意float是32位,double是64位
import java.util.Scanner; /* 写出一个程序,接受一个正浮点数值,输出该数值的近似整数值。如果小数点后数值大于等于 0.5 ,向上取整;小于 0.5 ,则向下取整。 数据范围:保证输入的数字在 32 位浮点数范围内 输入描述: 输入一个正浮点数值 输出描述: 输出该数值的近似整数值 */ public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); float n = in.nextFloat(); int integerPart = (int)n; // 取整数部分 float decimalPart = n - integerPart; // 取小数部分 if(decimalPart >= 0.5) integerPart += 1; System.out.println(integerPart); } }
32 位浮点数范围内
float
小数点后的数值
Scanner in = new Scanner(System.in); double n = in.nextDouble(); int integerPart = (int)n; // 取整数部分 double decimalPart = n - integerPart; // 取小数部分
double myDouble = 3.1415926; // 双精度浮点数,64 float myFloat = 3.14f; // 单精度浮点数,后面加f或F
float
是单精度浮点数类型,能提供大约6-7位有效数字;double
是双精度浮点数类型,能提供大约15位有效数字。double
因为精度更高,是默认的浮点数类型。如果你需要表示一个具体的浮点数,你直接写出这个数,Java会根据上下文推断其类型,通常为double
类型。单精度浮点数(
float
)
存储大小:32位(4字节)
组成
1位符号位(S):表示正负。
8位指数位(E):用于表示数值的幂次。
23位尾数位(M)或小数位:表示实际的数字精度。
精度:大约7位十进制有效数字。
范围:大约±3.4E±38(即±3.4乘以10的38次方到3.4乘以10的-38次方)。
用途:在需要节省内存和处理速度快于精度的情境中使用。
双精度浮点数(
double
)
存储大小:64位(8字节)
组成
1位符号位(S)
11位指数位(E)
52位尾数位(M)或小数位
精度:大约15位十进制有效数字。
范围:大约±1.7E±308(即±1.7乘以10的308次方到1.7乘以10的-308次方)。
用途:在需要高精度计算的应用中使用,如科学计算和工程计算。
double value = 3.1415926;
System.out.println(String.format("%.2f", value)); // 输出: 3.14
由于浮点数的表示使用的是二进制系统,某些小数在二进制中无法精确表示(比如0.1),这可能导致精度问题。如果需要进行精确的数学计算,特别是涉及到金融计算时,推荐使用BigDecimal
类。
import java.math.BigDecimal;
BigDecimal bd = new BigDecimal("3.1415926");
System.out.println(bd.setScale(2, BigDecimal.ROUND_HALF_UP)); // 输出: 3.14
HJ9 提取不重复的整数
-
直接将
Integer.toString(num)
转换成字符串后,你不能直接在for
循环中用String
类型迭代它-
int n = Character.getNumericValue(nums.charAt(i)); // 将字符转换为对应的数字
-
-
整数
%10
留下最后一位;/10
去掉最后一位
方法1:转成字符串
整数 字符串 转换
// 将整数转换为字符串 String numberStr = Integer.toString(number);
int n = Character.getNumericValue(nums.charAt(i)); // 将字符转换为对应的数字
import java.util.Scanner; import java.util.HashSet; /* 输入一个 int 型整数,按照从右向左的阅读顺序,返回一个不含重复数字的新的整数。 保证输入的整数最后一位不是 0 。 输入描述: 输入一个int型整数 输出描述: 按照从右向左的阅读顺序,返回一个不含重复数字的新的整数 */ public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int num = in.nextInt(); // int num = 1351652; num = read(num); System.out.println(num); } private static int read(int num) { HashSet<Integer> set = new HashSet<>(); String nums = Integer.toString(num); StringBuilder res = new StringBuilder(); for (int i = nums.length() - 1; i >= 0; i--) { int n = Character.getNumericValue(nums.charAt(i)); // 将字符转换为对应的数字 if (!set.contains(n)) { set.add(n); res.append(n); } } return Integer.parseInt(res.toString()); } }
方法2:整数除余
int temp = target % 10;
这行代码的作用是取得整数target
最右边一位的数字。在这个表达式中,%
是取余运算符,它会返回两个数相除的余数。对于任何整数来说,与10
进行取余操作的结果就是该整数的最低位(即最右边的一位)。
target / 10
则是执行整数除法运算,整数去掉最后一位
import java.util.*; public class Main{ public static void main(String[] args) { Scanner sc = new Scanner(System.in); while(sc.hasNext()){ // 使用HashSet来判断是否是不重复的 HashSet<Integer> hs = new HashSet<>(); int target = sc.nextInt();// 获取代求解的值 while(target != 0){ // 求解每位上面的整数 int temp = target % 10; if(hs.add(temp)) // 如果能加入,就是说明没有重复 System.out.print(temp); target /= 10;// 除10能去掉最右边的数字 } System.out.println(); } } }
HJ46 截取字符串
逐个添加
package hw; import java.util.Scanner; /** 输入一个字符串和一个整数 k ,截取字符串的前k个字符并输出 */ public class HJ46_截取字符串 { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNext()) { String str = in.nextLine(); int k = in.nextInt(); // 确保k不会超出字符串长度 k = Math.min(k, str.length()); StringBuilder res = new StringBuilder(); for (int i = 0; i < k; i++) { // 直接添加字符,无需转换为数字 res.append(str.charAt(i)); } System.out.println(res.toString()); // 清理输入缓冲区,准备读取下一行(如果存在) if (in.hasNextLine()) { in.nextLine(); } } } }
清理缓冲区
while (in.hasNext()) {
...
// 清理输入缓冲区,准备读取下一行(如果存在)
if (in.hasNextLine()) {
in.nextLine();
}
}
在Java中,使用
Scanner
类读取输入时,输入是通过输入流(如标准输入流System.in
)缓冲处理的。这意味着输入数据首先被存储在一个内部缓冲区中,Scanner
类的方法则从这个缓冲区中解析和提取数据。这个处理过程中,清理缓冲区的概念经常出现,尤其是在使用不同的Scanner
方法读取多种数据类型时。缓冲区的工作原理
当你输入数据并按下回车键时,输入的数据(包括回车或换行符)被操作系统接收并放置在程序的输入缓冲区中。
Scanner
类根据你调用的方法(如nextInt()
,nextLine()
等)从这个缓冲区中按需读取数据。清理缓冲区的需要
nextInt()
,next()
,nextDouble()
等方法:这些方法读取与其匹配的数据类型(整数、单词、浮点数等),但不消耗任何后续的换行符或回车符。这意味着,如果后面直接调用nextLine()
方法,nextLine()
会读取并返回直到下一个换行符的所有内容,这通常只包含换行符本身,因而看起来像是“跳过了用户的下一次输入”。
nextLine()
方法:这个方法读取当前位置直到下一个换行符的所有内容,并返回这部分内容的字符串表示(不包括换行符本身)。这使得nextLine()
非常适合用于清理缓冲区,因为它能够消耗并移除缓冲区中剩余的换行符。如何清理缓冲区
在读取了整数、浮点数、单词等后,你通常需要“清理”缓冲区中的换行符,以便下一次读取操作能够正常工作。最常见的做法是,在读取数值或单词后调用一次
nextLine()
,即使你不打算使用这次nextLine()
的返回值。这样做能够消耗并移除缓冲区中的换行符,避免影响到后续的读取操作。示例
Scanner scanner = new Scanner(System.in); int number = scanner.nextInt(); // 读取一个整数 scanner.nextLine(); // 清理缓冲区中的换行符 String line = scanner.nextLine(); // 正常读取下一行字符串这个简单的流程展示了如何在读取一个整数后清理缓冲区,确保
nextLine()
能够按预期工作,读取并返回下一次用户输入的完整行。这种模式对于混合使用Scanner
的多个读取方法时尤其重要,帮助避免逻辑上的错误和输入上的混淆。
按行读取
清理输入缓冲区的原理,特别是在使用
Scanner
类读取不同类型的输入时,是为了避免由于不匹配的读取操作导致的输入被意外忽略或“吞掉”问题。这种情况通常发生在读取数值(如int
,double
等)和读取行(nextLine()
)混合使用时。原理
当使用如
nextInt()
,nextDouble()
等方法读取数值时,Scanner
会读取并转换直到遇到的第一个非有效字符(通常是空格、制表符或换行符)的序列。但是,这些方法不会消耗任何后续的空白字符,包括换行符。
nextLine()
方法读取当前位置直到下一个换行符之间的所有字符,然后返回这段字符(不包括换行符)。如果紧接在数值读取方法之后调用nextLine()
,由于数值读取方法留下的换行符还在缓冲区中,nextLine()
会认为它已经到达了行的末尾,并返回一个空字符串。导致输入行被“吞掉”的问题
如果不适当地处理上述情况,确实会导致用户的某个输入行被“吞掉”。这通常发生在读取了一个数值后立即尝试读取下一行文本时。
nextLine()
会读取数值后的换行符之后的内容,如果数值输入后直接跟着换行符,那么nextLine()
就只会捕获到空字符串。解决方案
读取完数值后显式调用
nextLine()
:在每次使用nextInt()
,nextDouble()
等方法后,立即调用一次nextLine()
来消费掉留在缓冲区的换行符,从而避免它影响后续的nextLine()
调用。int number = scanner.nextInt(); // 读取一个整数 scanner.nextLine(); // 消费掉整数后的换行符 String line = scanner.nextLine(); // 现在可以正确读取下一行文本
统一使用
nextLine()
并手动转换:另一种策略是对所有输入都使用nextLine()
读取为字符串,然后根据需要将字符串转换为其他类型。这种方式避免了直接使用nextInt()
,nextDouble()
等方法可能引入的问题。String line = scanner.nextLine(); // 读取一行文本 int number = Integer.parseInt(line); // 将文本转换为整数选择哪种方式取决于具体的应用场景和个人偏好。第一种方法需要较少的转换操作,可能在某些情况下更直接、更方便;第二种方法通过统一输入处理逻辑,提高了代码的一致性和可预测性。
substring(0,k)
import java.util.Scanner; public class HJ46_截取字符串 { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextLine()) { String str = in.nextLine(); int k = Integer.parseInt(in.nextLine()); // 读取整数k,并清理输入缓冲区 String res = str.substring(0, k); // 直接使用substring方法截取前k个字符 System.out.println(res); } } }
HJ58 输入n个整数,输出其中最小的k个
-
输入拿来数组
-
排序
-
取前k个
-
组成字符串输出
import java.util.Scanner; import java.util.Arrays; /** 输入n个整数,找出其中最小的k个整数并按升序输出 */ public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextLine()) { // 注意 while 处理多个 case int n = in.nextInt(); int k = in.nextInt(); int nums[] = new int[n]; for(int i = 0; i < n; i++){ nums[i] = in.nextInt(); } Arrays.sort(nums); StringBuilder res = new StringBuilder(); for(int i = 0; i < k; i++){ res.append(nums[i]); if(i < k - 1){ res.append(" "); } } System.out.println(res.toString()); if(in.hasNextLine()){ in.nextLine(); } } } }
HJ101 数组排序
import java.util.Scanner; import java.util.Arrays; /** 输入整型数组和排序标识,对其元素按照升序或降序进行排序 第一行输入数组元素个数 第二行输入待排序的数组,每个数用空格隔开 第三行输入一个整数0或1。0代表升序排序,1代表降序排序 */ public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextInt()) { // 注意 while 处理多个 case int n = in.nextInt(); int nums[] = new int[n]; for(int i = 0; i < n; i++){ nums[i] = in.nextInt(); } int flag = in.nextInt(); if(flag == 0) Arrays.sort(nums); else if(flag == 1){ Arrays.sort(nums); // 麻烦点,写个交换顺序 int left = 0; int right = nums.length - 1; while (left < right){ int temp = nums[left]; nums[left] = nums[right]; nums[right] = temp; left++; right--; } } String res = Arrays.toString(nums).replace("[","").replace("]","").replace(",",""); System.out.println(res); } } }
调用函数
一、ArrayList 的升序与降序
升序:
Collections.sort(arr)
降序:
Collections.sort(arr,Collections.reverseOrder())
二、数组升序
使用 java.util.Arrays 类中的 sort() 方法对数组进行升序
Arrays.sort(array)
对数组进行排序,排序规则是从小到大,即升序。三、数组降序
在 Java 语言中使用 sort 实现降序有两种方法,简单了解即可。
方法一
1)利用 Collections.reverseOrder() 方法(Collections 是一个包装类)
方法二
2)实现 Comparator 接口的复写 compare() 方法
直接打印数字
import java.util.*; public class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); while(sc.hasNext()){ int n = sc.nextInt();//接收数组长度 int[] arr = new int[n];//创建数组 for (int i = 0; i < n; i++) {//数组填入 arr[i] = sc.nextInt(); } int flag = sc.nextInt();//接收排序标识 Arrays.sort(arr);//数组排序 if (flag == 0) {//正序输出 for(int i =0; i < arr.length; i++){ System.out.print(arr[i] + " "); } } else {//逆序输出 for(int i = arr.length - 1; i >= 0; i--){ System.out.print(arr[i] + " "); } } } } }
也可以
import java.util.Scanner; import java.util.Arrays; /** 输入整型数组和排序标识,对其元素按照升序或降序进行排序 第一行输入数组元素个数 第二行输入待排序的数组,每个数用空格隔开 第三行输入一个整数0或1。0代表升序排序,1代表降序排序 */ public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextInt()) { // 注意 while 处理多个 case int n = in.nextInt(); int nums[] = new int[n]; for(int i = 0; i < n; i++){ nums[i] = in.nextInt(); } int flag = in.nextInt(); StringBuilder out = new StringBuilder(); if(flag == 0){ Arrays.sort(nums); } else if(flag == 1){ Arrays.sort(nums); // 麻烦点,写个交换顺序 int left = 0; int right = nums.length - 1; while (left < right){ int temp = nums[left]; nums[left] = nums[right]; nums[right] = temp; left++; right--; } } for(int i = 0; i < n; i++){ out.append(nums[i]); if(i < n - 1){ out.append(" "); } } System.out.println(out.toString()); } } }
回溯算法
回溯搜索法:回溯是递归的副产品,只要有递归就会有回溯。
回溯函数也就是递归函数,指的都是一个函数。
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。
回溯法,一般可以解决如下几种问题:
-
组合问题:N个数里面按一定规则找出k个数的集合
-
切割问题:一个字符串按一定规则有几种切割方式
-
子集问题:一个N个数的集合里有多少符合条件的子集
-
排列问题:N个数按一定规则全排列,有几种排列方式
-
棋盘问题:N皇后,解数独等等
组合是不强调元素顺序的,排列是强调元素顺序。
回溯法解决的问题都可以抽象为树形结构。因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。
回溯算法中函数返回值一般为void。因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
组合
题目:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
组合问题是回溯法解决的经典问题,如果n为100,k为50的话,直接想法就需要50层for循环。
从而引出了回溯法就是解决这种k层for循环嵌套的问题。
然后进一步把回溯法的搜索过程抽象为树形结构,可以直观的看出搜索的过程。
接着用回溯法三部曲,函数参数、终止条件和单层搜索。
递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了。
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度。
图中每次搜索到了叶子节点,我们就找到了一个结果。相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。startIndex 就是防止出现重复的组合。在集合[1,2,3,4]取1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是startIndex。
所以需要startIndex来记录下一层递归,搜索的起始位置。
回溯函数终止条件
什么时候到达所谓的叶子节点了呢?
path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。
如图红色部分:
此时用result二维数组,把path保存起来,并终止本层递归。
单层搜索的过程:
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。
如此我们才遍历完图中的这棵树。
for循环每次从startIndex开始遍历,然后用path保存取到的节点i。
class Solution { // 用于存储所有可能组合的结果集 List<List<Integer>> result = new ArrayList<>(); // 用于存储每一次尝试构建的组合路径 LinkedList<Integer> path = new LinkedList<>(); // 公共调用方法,输入n和k,返回所有可能的k个数的组合 public List<List<Integer>> combine(int n, int k) { // 开始递归回溯,从数字1开始 backtracking(n, k, 1); // 返回最终的组合结果集 return result; } // 回溯方法,n是可选数字的最大值,k是组合中数字的目标数量,startIndex是本次递归应当开始选择数字的位置 public void backtracking(int n, int k, int startIndex) { // 如果路径长度等于k,说明构建了一个完整的组合 if (path.size() == k) { // 将这个组合添加到结果集中,注意要使用new ArrayList<>(path)来复制一份当前路径 result.add(new ArrayList<>(path)); // 结束本次递归调用 return; } // 从startIndex开始,尝试所有可能的下一个元素 for (int i = startIndex; i <= n; i++) { // 将当前数字添加到路径中 path.add(i); // 基于当前路径,递归调用backtracking尝试下一个数字,注意下次递归的起始位置是i+1 backtracking(n, k, i + 1); // 回溯步骤,移除路径的最后一个数字,尝试下一个可能的数字 path.removeLast(); } } }
剪枝优化
举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。
这么说有点抽象,如图所示:
图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。
所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
优化过程如下:
-
已经选择的元素个数:path.size();
-
所需需要的元素个数为: k - path.size();
-
列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
-
在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
class Solution { // result 用于存储最终的组合结果 List<List<Integer>> result = new ArrayList<>(); // path 用于存储当前路径(一个可能的组合) LinkedList<Integer> path = new LinkedList<>(); // combine 方法启动组合生成过程 public List<List<Integer>> combine(int n, int k) { // 从数字1开始生成组合 combineHelper(n, k, 1); return result; // 返回最终的组合列表 } /** * 辅助方法:递归地生成所有可能的组合 * @param n 组合中允许的最大数字 * @param k 组合的目标长度 * @param startIndex 下一个添加到组合中的数字的起始索引 */ private void combineHelper(int n, int k, int startIndex) { // 如果当前路径的长度等于k,意味着我们找到了一个有效的组合 if (path.size() == k) { // 将当前路径的一个副本添加到结果中 result.add(new ArrayList<>(path)); // 结束当前路径的递归,返回到上一步 return; } // 优化:只遍历到 “n - (k - path.size()) + 1” // 因为超出这个范围后就不可能构造出长度为k的组合 for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 将当前数字 i 添加到路径中 path.add(i); // 递归调用 combineHelper,起始索引为当前索引的下一个数字 combineHelper(n, k, i + 1); // 回溯步骤:移除路径中的最后一个数字,探索下一个可能的数字 path.removeLast(); } } }
注意细节
-
// result 用于存储最终的组合结果 List<List<Integer>> result = new ArrayList<>(); // path 用于存储当前路径(一个可能的组合) LinkedList<Integer> path = new LinkedList<>();
-
剪枝
优化过程如下:
-
已经选择的元素个数:path.size();
-
所需需要的元素个数为: k - path.size();
-
列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
-
在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
// 优化:只遍历到 “n - (k - path.size()) + 1” // 因为超出这个范围后就不可能构造出长度为k的组合 for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 将当前数字 i 添加到路径中 path.add(i); // 递归调用 combineHelper,起始索引为当前索引的下一个数字 combineHelper(n, k, i + 1); // 回溯步骤:移除路径中的最后一个数字,探索下一个可能的数字 path.removeLast(); }
-