赞
踩
题目:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。
LinkedList<Integer>
是 Java 标准库中的一个集合类,它实现了List
接口。LinkedList
使用双向链表的数据结构来存储元素,因此,与ArrayList
相比,它在列表中插入或删除元素时通常能提供更好的性能,特别是在列表的开头或中间进行操作时。在回溯算法中,使用
LinkedList
作为路径(path
)存储的数据结构是一个常见的选择,因为它经常需要在列表的末尾添加元素或删除最后的元素,而这些操作LinkedList
都能高效地完成。以下是
LinkedList<Integer>
的一些重要特点和常用方法:特点
动态大小:
LinkedList
可以根据需要自动扩展和缩减其大小。元素类型:
LinkedList<Integer>
中只能存储Integer
类型的对象,不能存储基本数据类型int
。在存取基本类型int
时,自动装箱和拆箱会自动发生。双向遍历:由于是双向链表,可以从两个方向遍历元素。
非同步:
LinkedList
是非同步的,所以如果在多线程环境中使用,需要外部同步。常用方法
add(E e)
: 将指定元素添加到列表的末尾。
add(int index, E element)
: 在列表的指定位置插入元素。
addFirst(E e)
和addLast(E e)
: 分别在列表开头和末尾添加元素。
get(int index)
: 返回列表中指定位置的元素。
remove(int index)
: 删除列表中指定位置的元素。
removeFirst()
和removeLast()
: 删除并返回列表的第一个或最后一个元素。
size()
: 返回列表中的元素数。
isEmpty()
: 如果列表不包含元素,返回true
。在回溯算法中的使用
在回溯算法中,
LinkedList<Integer>
作为path
的数据结构使用,具体来说,有以下几点优势:
快速的插入和删除操作:
path.addLast(i)
和path.removeLast()
能够快速地在路径的末尾添加或移除元素,这对于回溯算法中尝试和回退是非常有用的。不需要动态数组的随机访问功能:在大多数回溯算法实现中,我们并不需要随机访问路径中的元素,所以
LinkedList
的缺点(比如相对较慢的随机访问性能)在这里并不突出。自动装箱和拆箱:由于
LinkedList
存储的是Integer
对象,所以当我们添加一个基本类型int
时,它会自动被装箱为Integer
对象,同样地,在获取时会自动拆箱回int
类型。使用
LinkedList
的一个小缺点是每个元素都需要额外的内存空间来存储前驱和后继指针,但在回溯算法的上下文中,这通常不是一个大问题,因为算法的空间复杂度主要由递归深度决定。
class Solution { // 存储所有可能的组合结果 List<List<Integer>> res = new ArrayList<>(); // 用于记录当前的组合路径 LinkedList<Integer> path = new LinkedList<>(); // 公共方法,开始组合总和的搜索 public List<List<Integer>> combinationSum3(int k, int n) { // 从数字1开始,尝试所有可能的组合 backTracking(n, k, 1, 0); // 返回所有有效的组合 return res; } /** * 使用回溯法搜索所有可能的组合 * * @param n 目标数字,组合中数字的总和 * @param k 组合中所需的数字个数 * @param startIndex 开始的索引,避免重复的组合 * @param sum 当前路径中所有数字的总和 */ private void backTracking(int n, int k, int startIndex, int sum){ // 如果当前路径的总和已经大于目标数字n,则没有必要继续搜索 if(sum > n) return; // 检查当前路径的长度是否等于k if(path.size() == k){ // 如果当前路径的数字总和等于n,那么这是一个有效的组合 if(sum == n) res.add(new ArrayList<>(path)); // 返回上一层递归 return; } // 遍历所有可能的选择 // 减少循环的次数,只考虑那些有可能成为解的数字 for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++){ // 将当前数字添加到路径中 path.add(i); // 增加当前数字到总和中 sum += i; // 基于当前选择的数字,继续探索下一个数字 backTracking(n, k, i+1, sum); // 回溯,撤销前一步的选择 path.removeLast(); // 从总和中减去撤销的数字 sum -= i; } } }
// 上面剪枝 i <= 9 - (k - path.size()) + 1; 如果还是不清楚 // 也可以改为 if (path.size() > k) return; 执行效率上是一样的 class Solution { LinkedList<Integer> path = new LinkedList<>(); List<List<Integer>> ans = new ArrayList<>(); public List<List<Integer>> combinationSum3(int k, int n) { build(k, n, 1, 0); return ans; } private void build(int k, int n, int startIndex, int sum) { if (sum > n) return; if (path.size() > k) return; if (sum == n && path.size() == k) { ans.add(new ArrayList<>(path)); return; } for(int i = startIndex; i <= 9; i++) { path.add(i); sum += i; build(k, n, i + 1, sum); sum -= i; path.removeLast(); } } }
题目:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下。注意 1 不对应任何字母。
"23",抽象为树形结构:
String str = numString[digits.charAt(num) - '0'];
这行代码的作用是:
digits.charAt(num)
: 获取digits
字符串中位置为num
的字符(数字)。假设num
为0,那么digits.charAt(num)
将会是字符'2'。
- '0'
: 将得到的字符(例如'2')转换为对应的整数(例如2)。在ASCII码表中,数字字符'0'到'9'是连续排列的,'0'的ASCII码是48,'2'的ASCII码是50。因此,'2' - '0'
的结果是2,这是因为50 - 48 = 2
。
numString[digits.charAt(num) - '0']
: 根据步骤2得到的索引从numString
数组中取出对应的字符串。如果得到的索引是2,那么numString[2]
就是"abc"。
class Solution { // 使用list来存储所有可能的字母组合 List<String> list = new ArrayList<>(); public List<String> letterCombinations(String digits) { // 数字到字母的映射,index 0 和 1 是空字符串,因为它们在电话键盘上没有对应的字母 String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; // 如果输入的字符串为空,返回空列表 if (digits == null || digits.length() == 0) return list; // 开始回溯搜索 backtrack(digits, numString, 0); return list; } // 用于构建当前探索路径的字母组合 StringBuilder temp = new StringBuilder(); private void backtrack(String digits, String[] numString, int num) { // 如果当前路径的长度与输入字符串长度相同,说明找到了一个完整的字母组合 if (num == digits.length()) { list.add(temp.toString()); return; } // 获取当前数字对应的所有可能的字母 String str = numString[digits.charAt(num) - '0']; // 遍历这些字母 for (int i = 0; i < str.length(); i++) { // 将当前字母加入到路径中 temp.append(str.charAt(i)); // 继续探索下一个数字 backtrack(digits, numString, num + 1); // 回溯,移除路径中的最后一个字母,尝试下一个可能的字母 temp.deleteCharAt(temp.length() - 1); } } }
核心思路
初始化: 创建一个全局列表
list
来存储最终的所有字母组合,和一个StringBuilder
temp
来构建每一种可能的字母组合。检查输入: 在
letterCombinations
方法中,首先检查输入的字符串digits
是否为空,如果是,则直接返回空列表。开始回溯: 使用
backtrack
方法进行深度优先搜索,生成所有可能的字母组合。回溯逻辑:
如果当前的路径长度(即
temp
的长度)等于输入数字字符串的长度,说明已经形成了一个完整的字母组合,将其添加到list
中。从数字到字母的映射中获取当前数字对应的所有可能字母,遍历这些字母,对每个字母执行以下操作:
将其添加到
temp
中。递归地调用
backtrack
,以当前字母为起点,探索下一个数字对应的所有可能字母。在递归返回后,执行回溯操作,即从
temp
中移除最后添加的字母,以尝试其他可能的字母。这种方式通过递归和回溯结合,能够高效地搜索出所有可能的字母组合。
backtrack(digits, numString, 0);
String str = numString[digits.charAt(num) - '0'];
temp.append(str.charAt(i));
// 将当前字母加入到路径中 apped backtrack(digits, numString, num + 1); // 继续探索下一个数字
temp.deleteCharAt(temp.length() - 1);
// 回溯,移除路径中的最后一个字母,尝试下一个可能的字母 deleteCharAt
(0条未读通知) 牛客竞赛ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛牛客竞赛OJ (nowcoder.com)
import java.util.Scanner; public class Main { public static void main(String[] args){ Scanner in = new Scanner(System.in); while (in.hasNextLine()){ String[] lines = in.nextLine().split(","); int left = 0; int right = lines.length - 1; fastsort(lines , left , right); String outstr = String.join(",",lines); System.out.println(outstr); } } private static void fastsort(String[] lines, int left, int right){ if(left<right){ int pivot = part(lines, left, right); fastsort(lines, left, pivot-1); fastsort(lines, pivot+1 , right); } } private static int part(String[] lines, int left, int right){ int i =left -1; for(int j = 0; j < right; j++){ if(lines[j].compareTo(lines[right]) < 0){ i++; String temp = lines[j]; lines[j] = lines[i]; lines[i] = temp; } } int pivot = i + 1; String temp = lines[pivot]; lines[pivot] = lines[right]; lines[right] = temp; return pivot; } }
题目描述中提到的数据范围是0 < a, b < 2×10^10
,这个范围超出了Java中int
类型的最大值2,147,483,647
。因此,你应该使用long
类型来存储输入的整数和它们的和,以避免数值溢出。
使用long
类型,其范围大约在-9.22E18
到9.22E18
之间,足以容纳题目描述中的数据范围0 < a, b < 2×10^10
。
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); while(in.hasNextLong()){ // 使用hasNextLong检查下一个输入项是否可以作为long类型读取 long a = in.nextLong(); // 读取一个长整型数字 long b = in.nextLong(); // 读取另一个长整型数字 System.out.println(a + b); // 输出两个长整型数字的和 } } }
if(str.startsWith("0x") || str.startsWith("0X")) str = str.substring(2);
去掉16进制开头的 0x
0X
//将字母转换为数值 if(tc>='0' && tc<='9') t = tc - '0'; //字母'A'/'a'~'F''f'对应数字10~15 else if(tc>='A' && tc<='F') t = tc - 'A' + 10; else if(tc>='a' && tc<='f') t = tc - 'a' +10;
// 获取当前字符 char c = str.charAt(i); // 使用Character.digit方法将十六进制字符转换为对应的十进制数值 // 第二个参数16表示我们正在处理的是十六进制数 int n = Character.digit(c , 16);
import java.util.Scanner; import java.lang.Math; public class Main{ public static void main(String[] args){ Scanner in = new Scanner(System.in); while(in.hasNextLine()){ String s = in.nextLine(); //读入数字 int count = 0; //记录转换后的数字 for(int i=0; i < s.length()-2; i++){ //由于前面两位是'0x',故从第三位开始 char tc = s.charAt(i+2); int t = 0; //记录字母转换成的数值 //将字母转换为数值 if(tc>='0' && tc<='9') t = tc - '0'; //字母'A'/'a'~'F''f'对应数字10~15 else if(tc>='A' && tc<='F') t = tc - 'A' + 10; else if(tc>='a' && tc<='f') t = tc - 'a' +10; //计算加和 count += t * Math.pow(16, s.length()-i-3); } System.out.println(count); } } }
package hw; /** * 描述 * 写出一个程序,接受一个十六进制的数,输出该数值的十进制表示。 * 输入描述: * 输入一个十六进制的数值字符串。 * 输出描述: * 输出该数值的十进制字符串。不同组的测试用例用\n隔开。 */ import java.util.Scanner; public class HJ5_进制转换 { public static void main(String[] args) { // 创建一个Scanner对象来读取标准输入 Scanner in = new Scanner(System.in); // 使用一个循环来不断读取输入,直到没有新的输入 while(in.hasNextLine()){ // 读取一行输入 String str = in.nextLine(); // 如果输入的十六进制数以"0x"或"0X"开头,去掉这个前缀 // 因为这个前缀是十六进制的标准表示法,但对于转换来说是不必要的 if(str.startsWith("0x") || str.startsWith("0X")) str = str.substring(2); // 获取去掉前缀后的字符串长度 int length = str.length(); // 初始化一个计数器,代表当前位的数值应该乘以16的多少次方 int count = 1; // 初始化和为0 int sum = 0; // 从字符串的最后一个字符开始遍历,即从十六进制数的最低位开始 for(int i = length - 1; i >= 0 ; i--){ // 获取当前字符 char c = str.charAt(i); // 使用Character.digit方法将十六进制字符转换为对应的十进制数值 // 第二个参数16表示我们正在处理的是十六进制数 int n = Character.digit(c , 16); /** 或者写成 int n = 0; if(c>='0' && c<='9') n = c - '0'; else if(c>='A' && c<='F') n = c - 'A' + 10; else if(c>='a' && c<='f') n = c - 'a' + 10; */ // 将当前位的十进制数值乘以对应的16的幂次方,然后加到总和上 sum += n * count; // 更新16的幂次方,为处理下一位做准备 count *= 16; } // 最后打印出总和,即输入的十六进制数对应的十进制数 System.out.println(sum); } } }
题目:给定一个包含 n
个整数的数组 nums
,判断 nums
中是否存在三个元素 a
,b
,c
,使得 a + b + c = 0
?请找出所有和为 0
且 不重复 的三元组。题解 | #三数之和#_牛客网 (nowcoder.com)
知识点1:哈希表
哈希表是一种根据关键码(key)直接访问值(value)的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)O(1)O(1)时间内得到value,因此哈希表常用来统计频率、快速检验某个元素是否出现过等。
知识点2:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
具体做法:
step 1:排除边界特殊情况。
step 2:既然三元组内部要求非降序排列,那我们先得把这个无序的数组搞有序了,使用sort函数优先对其排序。
step 3:得到有序数组后,遍历该数组,对于每个遍历到的元素假设它是三元组中最小的一个,那么另外两个一定在后面。
step 4:需要三个数相加为0,则另外两个数相加应该为上述第一个数的相反数,我们可以利用双指针在剩余的子数组中找有没有这样的数对。双指针指向剩余子数组的首尾,如果二者相加为目标值,那么可以记录,而且二者中间的数字相加可能还会有。
step 5:如果二者相加大于目标值,说明右指针太大了,那就将其左移缩小,相反如果二者相加小于目标值,说明左指针太小了,将其右移扩大,直到两指针相遇,剩余子数组找完了。
注:对于三个数字都要判断是否相邻有重复的情况,要去重。
- class Solution {
- public List<List<Integer>> threeSum(int[] nums) {
- List<List<Integer>> res = new ArrayList<>(); // 初始化结果列表
- int length = nums.length;
- // 如果数组长度小于3,直接返回空列表,因为不可能有三元组
- if(length < 3) {
- return res;
- }
- // 对数组进行排序
- Arrays.sort(nums);
- // 遍历数组
- for(int i = 0; i < length; i++) {
- // 跳过重复的数字,以避免重复的三元组
- if(i > 0 && nums[i] == nums[i-1]) continue;
- // 初始化左右指针
- int left = i + 1;
- int right = length - 1;
- // 计算当前目标值为当前元素的相反数
- int target = -nums[i];
-
- // 当左指针小于右指针时,执行循环
- while(left < right) {
- // 如果找到了符合条件的三元组
- if(nums[left] + nums[right] == target) {
- // 将其添加到结果列表中
- res.add(Arrays.asList(nums[i], nums[left], nums[right]));
- // 移动左指针跳过重复元素
- while(left < right && nums[left] == nums[left + 1]) left++;
- // 移动右指针跳过重复元素
- while(left < right && nums[right] == nums[right - 1]) right--;
- // 继续探索其他可能的组合
- left++;
- right--;
- } else if(nums[left] + nums[right] > target) {
- // 如果两数之和大于目标值,移动右指针
- right--;
- } else {
- // 如果两数之和小于目标值,移动左指针
- left++;
- }
- }
- }
- // 返回所有找到的三元组
- return res;
- }
- }
List<Integer> list = new ArrayList<Integer>(); list.add(nums[first]); list.add(nums[second]); list.add(nums[third]); ans.add(list);
Set<Integer> set = new TreeSet<>(); // 直接使用TreeSet既去重又排序
HashSet<Integer> set = new HashSet<>(); // 创建一个HashSet用于去除重复的整数
package hw; import java.util.Arrays; import java.util.HashSet; import java.util.Scanner; /** 描述 N个1到500之间的随机整数,删去其中重复的数字,然后再把这些数从小到大排序,按照排好的顺序输出。 输入描述: 第一行先输入随机整数的个数 N 。 接下来的 N 行每行输入一个整数,代表明明生成的随机数。 具体格式可以参考下面的"示例"。 输出描述: 输出多行,表示输入数据处理后的结果 */ public class HJ3_明明的随机数 { public static void main(String[] args) { Scanner in = new Scanner(System.in); // 使用循环来处理多个测试用例 while (in.hasNextInt()) { int n = in.nextInt(); // 首先读取整数的个数N int[] nums = new int[n]; // 创建一个数组来存储这N个整数 // 循环读取N个整数并存储到nums数组中 for(int i = 0; i < n; i++) { nums[i] = in.nextInt(); } HashSet<Integer> set = new HashSet<>(); // 创建一个HashSet用于去除重复的整数 int j = 0; // j用于记录去重后的数组长度 // 遍历nums数组,去除重复的整数 for(int i = 0; i < n; i++) { // 如果set中尚未包含当前数字,则将其添加到set中,并更新nums数组 if(!set.contains(nums[i])) { nums[j] = nums[i]; // 重新利用nums数组存储去重后的整数 set.add(nums[i]); j++; // 增加去重后数组的计数器 } } // 创建一个新的数组res,用于存放去重并且要排序的整数 int[] res = new int[j]; for(int i = 0; i < j; i++) { res[i] = nums[i]; // 将去重后的整数复制到res数组中 } Arrays.sort(res); // 对res数组进行排序 // 遍历并输出排序后的res数组 for(int i: res) { System.out.println(i); } } } }
import java.util.Scanner; import java.util.Set; import java.util.TreeSet; public class HJ3_明明的随机数 { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextInt()) { int n = in.nextInt(); Set<Integer> set = new TreeSet<>(); // 直接使用TreeSet既去重又排序 for(int i = 0; i < n; i++) { set.add(in.nextInt()); } // TreeSet保证了元素的唯一性和排序,直接输出即可 for(Integer num : set) { System.out.println(num); } } } }
import java.util.Scanner; import java.util.HashSet; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextLine()){ String str = in.nextLine(); HashSet<Character> set = new HashSet<>(); for(char c : str.toCharArray()){ if(c>=0 && c<=127){ set.add(c); } } int count = set.size(); System.out.println(count); } } }
【经典算法题】【牛客网NC68】跳台阶(递归)_nc68.跳台阶 牛客网-CSDN博客
先更新前前状态,再更新前状态
current = prev + prev_prev; // 当前状态是前两个状态之和
prev_prev = prev; // 更新前前状态为前一个状态\
prev = current; // 更新前一个状态为当前状态
package hw; /** * 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。 * 数据在1到40之间;要求时间复杂度On,空间复杂度O1 */ import java.util.Scanner; public class NC68_跳台阶 { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); int res = getStep(n); System.out.println(res); } private static int getStep(int n){ // 处理特殊情况 if(n <= 0) return 0; // 如果n<=0, 没有跳法 if(n == 1) return 1; // 如果n==1, 只有1种跳法 if(n == 2) return 2; // 如果n==2, 有2种跳法 // 用于存储前两个状态的变量 int prev = 2; // 前一个状态,即dp[i-1],初始为n=2的情况 int prev_prev = 1; // 前前一个状态,即dp[i-2],初始为n=1的情况 int current = 0; // 当前状态 for(int i = 3; i <= n; i++){ // 从3开始计算到n current = prev + prev_prev; // 当前状态是前两个状态之和 prev = current; // 更新前一个状态为当前状态 prev_prev = prev; // 更新前前状态为前一个状态 } return current; // 返回n级台阶的跳法数量 } }
第i行有i+1个元素
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
class Solution { public List<List<Integer>> generate(int numRows) { List<List<Integer>> triangle = new ArrayList<>(); // 如果输入的行数不大于0,则直接返回空的杨辉三角 if (numRows <= 0) { return triangle; } // 第一行始终是[1] triangle.add(new ArrayList<>()); triangle.get(0).add(1); // 从第二行开始构建杨辉三角 for (int rowNum = 1; rowNum < numRows; rowNum++) { List<Integer> row = new ArrayList<>(); List<Integer> prevRow = triangle.get(rowNum - 1); // 每行的第一个元素始终是1 row.add(1); // 计算当前行中间的元素,每个元素是上一行的两个相邻元素之和 for (int j = 1; j < rowNum; j++) { row.add(prevRow.get(j - 1) + prevRow.get(j)); } // 每行的最后一个元素始终是1 row.add(1); // 将当前行添加到杨辉三角中 triangle.add(row); } return triangle; } }
class Solution { public List<List<Integer>> generate(int numRows) { List<List<Integer>> ret = new ArrayList<List<Integer>>(); for (int i = 0; i < numRows; ++i) { List<Integer> row = new ArrayList<Integer>(); for (int j = 0; j <= i; ++j) { if (j == 0 || j == i) { row.add(1); } else { row.add(ret.get(i - 1).get(j - 1) + ret.get(i - 1).get(j)); } } ret.add(row); } return ret; } }
- public class Solution {
- public List<List<Integer>> generate(int numRows) {
- // 初始化动态规划数组
- Integer[][] dp = new Integer[numRows][];
- // 遍历每一行
- for (int i = 0; i < numRows; i++) {
- // 初始化当前行;第i行有i+1个元素
- dp[i] = new Integer[i + 1];
- // 每一行的第一个和最后一个元素总是 1
- dp[i][0] = dp[i][i] = 1;
- // 计算中间元素
- for (int j = 1; j < i; j++) {
- // 中间元素等于上一行的相邻两个元素之和
- dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
- }
- }
-
- // 将动态规划数组转换为结果列表
- List<List<Integer>> result = new ArrayList<>();
- for (Integer[] row : dp) {
- result.add(Arrays.asList(row));
- }
- // 返回结果列表
- return result;
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。