3.14
螺旋矩阵
参考螺旋矩阵II:
要设置循环(圈数);循环还要考虑奇数的情况
一圈顺时针保持 【左开右闭】
开始的位置要设置start;这个start每个循环还要累加
for循环(i;i<n;i++)这个格式不能变:for (初始化语句; 循环条件检查; 步进语句);也可以int i = 0;
for (; i < n; i++)
loop++ < n/2
:这个条件确保了算法只处理到矩阵的一半深度,因为螺旋填充是对称的。当n
是奇数时,中心点会单独处理。
start++
:每完成一层的填充后,start
递增,意味着下一轮填充的起始点向内移动一格。
-
要找一个m*n大小的来接数
-
m行n列,取其中小的数值edge来计算循环loop
-
loop++<edge/2
-
如果edge%2 == 1,loop再加1
-
在Java中,不能直接返回一个数组。您需要将数组转换为列表类型,然后返回该列表
不能用左闭右开
方法 1 循环
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
if (matrix.length == 0 || matrix[0].length == 0) return res;
int m = matrix.length, n = matrix[0].length;
int loop = (Math.min(m, n) + 1) / 2; // 计算循环次数
for (int i = 0; i < loop; i++) {
// 从左到右遍历
for (int j = i; j < n - i; j++) {
res.add(matrix[i][j]);
}
// 从上到下遍历
for (int j = i + 1; j < m - i; j++) {
res.add(matrix[j][n - 1 - i]);
}
// 如果存在多余的行或列,再从右到左和从下到上遍历
if (m - 1 - i > i) { // 防止重复遍历同一行
for (int j = n - 1 - i - 1; j >= i; j--) {
res.add(matrix[m - 1 - i][j]);
}
}
if (n - 1 - i > i) { // 防止重复遍历同一列
for (int j = m - 1 - i - 1; j > i; j--) {
res.add(matrix[j][i]);
}
}
}
return res;
}
}
方法2 边界
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
int m = matrix.length;
int n = matrix[0].length;
if( m == 0 || n == 0 ) return res;
int left = 0, right = n-1, top = 0, bottom = m-1;
while(left<=right && top<= bottom){
for(int col = left;col <= right;col++){
res.add(matrix[top][col]);
}
for(int row = top + 1;row <= bottom;row++){
res.add(matrix[row][right]);
}
// 对于当前范围内非仅一行或一列
if(left<right && top<bottom){
for(int col = right -1; col>=left;col--){
res.add(matrix[bottom][col]);
}
for(int row = bottom -1;row>top;row--){
res.add(matrix[row][left]);
}
}
left++;
right--;
top++;
bottom--;
}
return res;
}
}
注意细节
-
int loop = (Math.min(m, n) + 1) / 2; // 计算循环次数
-
左开右闭容易漏,所以最好使用边界法
-
边界使用时,注意
int left = 0, right = n-1, top = 0, bottom = m-1; while(left<=right && top<= bottom){ for(int col = left;col <= right;col++){ for(int row = top + 1;row <= bottom;row++){ // 对于当前范围内非仅一行或一列 if(left<right && top<bottom){ for(int col = right -1; col>=left;col--){ for(int row = bottom -1;row>top;row--){ left++; right--; top++; bottom--;
-
不能返回一个数组,可以返回一个列表
二叉树
二叉树的迭代遍历
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
前序遍历(迭代法)
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
-
添加根节点,
-
保证不为空(遍历过程中一边弹一边压所以不会空直到遍历结束)
-
弹出根,同时,压入根的右和左
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
res.add(node.val);
if(node.right != null) stack.push(node.right);
if(node.left != null) stack.push(node.left);
}
return res;
}
}
栈不为空:循环的条件是栈不为空(
!stack.isEmpty()
)。这意味着,只要栈中还有节点,就会继续进行遍历。弹出栈顶元素:在每次循环中,首先执行的操作是弹出栈顶元素(
TreeNode node = stack.pop();
)。这个弹出操作是无条件的,因为进入循环的前提已经确保了栈不为空。处理节点:弹出栈顶元素后,先将其值添加到结果列表中(这实现了“中”的访问顺序)。然后检查弹出节点的右子节点和左子节点,如果它们非空,则按右子节点、左子节点的顺序压入栈中。这个顺序保证了下一次弹出的是左子节点(如果有的话),这样就遵循了前序遍历的中-左-右顺序。
中序遍历(迭代法)
为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
-
处理:将元素放进result数组中
-
访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
-
想要出来的是 左-中-右,那就想办法从根向下压栈push,同时找到最左侧节点
-
最左侧意味着cur.left为空,此时就弹出它,并指向它 的cur.right
-
如果cur.right为空,就弹出当前栈内的值,然后指向它的right
-
遍历条件是指针不为空同时栈内没有值
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(!stack.isEmpty() || cur != null){
if(cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
res.add(cur.val);
cur = cur.right;
}
}
return res;
}
}
初始化
栈(
Stack<TreeNode>
)用于存储将要访问的节点。当前节点(
TreeNode cur
)开始时指向根节点(root
)。遍历过程
向左深入:从根节点开始,尽可能地向左深入,将沿途经过的所有节点压入栈中。这一过程持续进行直到当前节点为空,即无法再向左深入为止。
入栈操作:每次当当前节点
cur
非空时,将cur
压入栈中,然后将cur
更新为其左子节点(cur = cur.left
)。访问节点:当无法再深入左子节点时(
cur
为空),从栈中弹出一个节点(这个节点没有左子节点或其左子节点已经被访问过)。访问该节点(将节点值添加到结果列表中),然后转向访问其右子树。
弹出操作:从栈中弹出栈顶元素,将
cur
更新为弹出的节点。访问操作:将弹出节点的值添加到结果列表中。
向右转:将
cur
更新为其右子节点,以便在下一个循环中访问该右子节点的左子树。重复以上步骤:重复执行向左深入和访问节点的操作,直到当前节点为空且栈也为空为止。这意味着整棵树已经被完全遍历。
结束条件
当当前节点为空且栈也为空时,遍历结束。这意味着所有的节点都已经按照中序遍历的顺序被访问过。
算法的核心思想
先左后中再右:确保每个节点都是在访问其左子树之后、访问其右子树之前被访问。
利用栈管理访问顺序:栈用于保持访问路径,确保在访问完当前节点及其左子树后,能够回到该节点并转向其右子树。
后序遍历(迭代法)
再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
-
添加根节点,
-
保证不为空(遍历过程中一边弹一边压所以不会空直到遍历结束)
-
弹出根,同时,压入根的左和右
-
后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
res.add(node.val);
if(node.left!=null) stack.push(node.left);
if(node.right!=null) stack.push(node.right);
}
Collections.reverse(res);
return res;
}
}
另一种后序遍历(迭代)
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
LinkedList<TreeNode> stack = new LinkedList<>(); // 使用栈来跟踪待访问的节点
TreeNode curr = root; // 当前节点开始于根节点
TreeNode pop = null; // 用来记录上一次弹出(访问)的节点
List<Integer> result = new ArrayList<>(); // 存储遍历结果
while (!stack.isEmpty() || curr != null) {
if (curr != null) {
stack.push(curr); // 将当前节点推入栈
curr = curr.left; // 移动到左子节点
} else {
TreeNode peek = stack.peek(); // 查看栈顶元素但不弹出
if (peek.right == null || peek.right == pop) {
// 如果栈顶节点的右子节点为空或已经访问过,则可以访问栈顶节点
pop = stack.pop(); // 弹出栈顶节点
result.add(pop.val); // 将其值添加到结果列表中
} else {
// 如果栈顶节点的右子节点还没有被访问,则移动到右子节点
curr = peek.right;
}
}
}
return result;
}
}
-
stack.peek()
是用于查看栈顶元素的方法。在栈数据结构中,栈顶指的是最近添加到栈中的元素,也就是最后一个入栈的元素。因此,stack.peek()
返回的是栈顶元素,而不是栈底元素。栈是一种后进先出(LIFO,Last In, First Out)的数据结构,所以栈顶元素是最后一个入栈的元素,先出栈。 -
核心思想与步骤
-
向左深入:从根节点开始,尽可能深入到左子节点,同时将遍历过的节点推入栈中。
-
判断右子节点:
-
如果当前栈顶节点的右子节点为空,或其右子节点是最近一次访问(弹出)的节点,说明左子树和右子树都已经访问完毕,可以安全地访问(弹出并处理)这个栈顶节点。
-
如果右子节点存在且未被访问,将当前节点更新为栈顶节点的右子节点,然后重复步骤1,尝试向左深入。
-
-
访问节点:当节点的左子树和右子树都被访问后,将节点从栈中弹出,并将其值添加到结果列表中。
关键点
-
栈:用于跟踪待访问的节点,同时保持遍历的状态。
-
当前节点(
curr
)和上一次访问的节点(pop
):curr
用于控制当前遍历的位置,pop
用于记录最近一次访问的节点,以判断是否可以回溯到父节点。 -
循环条件:当栈不为空或
curr
不为空时,继续遍历。这确保了所有节点都能被正确访问。
通过这种方法,后序遍历的非递归实现能够在不使用递归的情况下,有效地遍历整棵树。
-
注意细节
-
前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
-
前序遍历:
-
添加根节点,
-
保证不为空(遍历过程中一边弹一边压所以不会空直到遍历结束)
-
弹出根,同时,压入根的右和左
-
-
中序遍历:
-
想要出来的是 左-中-右,那就想办法从根向下压栈push,同时找到最左侧节点
-
最左侧意味着cur.left为空,此时就弹出它,并指向它 的cur.right
-
如果cur.right为空,就弹出当前栈内的值,然后指向它的right
-
遍历条件是指针不为空同时栈内没有值
-
-
后序遍历:后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
-
添加根节点,
-
保证不为空(遍历过程中一边弹一边压所以不会空直到遍历结束)
-
弹出根,同时,压入根的左和右
-
后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
或者
核心思想与步骤
-
向左深入:从根节点开始,尽可能深入到左子节点,同时将遍历过的节点推入栈中。
-
判断右子节点:
-
如果当前栈顶节点的右子节点为空,或其右子节点是最近一次访问(弹出)的节点,说明左子树和右子树都已经访问完毕,可以安全地访问(弹出并处理)这个栈顶节点。
-
如果右子节点存在且未被访问,将当前节点更新为栈顶节点的右子节点,然后重复步骤1,尝试向左深入。
-
-
访问节点:当节点的左子树和右子树都被访问后,将节点从栈中弹出,并将其值添加到结果列表中。
-
-
res.add(node.val);
不用node.val()
-
Collections.reverse(res);
翻转
3.15
蒙特卡洛树
贪心算法
贪心的本质是选择每一阶段的局部最优,从而达到全局最优。
这么说有点抽象,来举一个例子:
例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿?
指定每次拿最大的,最终结果就是拿走最大数额的钱。
每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优。
再举一个例子如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。动态规划的问题在下一个系列会详细讲解。
唯一的难点就是如何通过局部最优,推出整体最优。靠自己手动模拟,如果模拟可行,就可以试一试贪心策略,如果不可行,可能需要动态规划。最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧。
贪心算法一般分为如下四步:
-
将问题分解为若干个子问题
-
找出适合的贪心策略
-
求解每一个子问题的最优解
-
将局部最优解堆叠成全局最优解
只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。贪心没有套路,说白了就是常识性推导加上举反例。
分发饼干
题目:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例 1:
输入: g = [1,2,3], s = [1,1]
输出: 1 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以你应该输出 1。
方法1 大饼干满足大胃口
大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。
这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。
可以尝试使用贪心策略,先将饼干数组和小孩数组排序。
从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。
这个例子可以看出饼干 9 只有喂给胃口为 7 的小孩,这样才是整体最优解,并想不出反例
先遍历的胃口,在遍历的饼干,那么可不可以 先遍历 饼干,在遍历胃口呢?
其实是不可以的。
外面的 for 是里的下标 i 是固定移动的,而 if 里面的下标 index 是符合条件才移动的。
如果 for 控制的是饼干, if 控制胃口,就是出现如下情况 :
所以 一定要 for 控制 胃口,里面的 if 控制饼干。
大饼干满足大胃口
// 应该将start_s>=0的条件判断放到if语句之前,确保不会出现数组越界的情况。
-
排序
-
遍历孩子胃口
-
如果饼干满足大胃口,饼干往前移动
class Solution{
public int findContentChildren(int[] g, int[] s){
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
// 饼干开始的地方从大开始
int start_s = s.length - 1;
// 遍历孩子
for(int i = g.length - 1; i >= 0; i--){
// 应该将start_s>=0的条件判断放到if语句之前,确保不会出现数组越界的情况。
if(start_s >= 0 && g[i] <= s[start_s]){
start_s--;
count++;
}
}
return count;
}
}
方法2 小饼干满足小胃口
注意,正序排列,遍历饼干,只有饼干满足胃口小的孩子后,才去找下一个孩子
-
排序
-
遍历饼干
-
如果饼干满足孩子胃口,孩子都向后移动
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g);
Arrays.sort(s);
int count = 0;
int start_g = 0;
for(int j = 0;j<s.length && start_g<g.length;j++){
if(g[start_g] <= s[j]){
count++;
start_g++;
}
}
return count;
}
}
注意细节
-
// 正序排序 Arrays.sort(g);
// 倒序排序 Arrays.sort(g, Collections.reverseOrder());
-
方法2 小饼干满足小胃口
注意,正序排列,遍历饼干,只有饼干满足胃口小的孩子后,才去找下一个孩子
-
排序
-
遍历饼干
-
如果饼干满足孩子胃口,孩子都向后移动
-
-
方法1 大饼干满足大胃口
// 应该将start_s>=0的条件判断放到if语句之前,确保不会出现数组越界的情况。
-
排序
-
遍历孩子胃口
-
如果饼干满足大胃口,饼干往前移动
-
3.16
输入输出
-
读整数输入
import java.util.Scanner;
用于简化文本扫描和解析的过程
Scanner in = new Scanner(System.in);
创建了一个新的Scanner
对象,命名为in
,并将其构造函数的参数设置为System.in
,即标准输入流(通常是键盘)
while (in.hasNextInt())
:这个循环会持续检查输入中是否还有下一个整数。如果有,循环继续;否则,循环结束。这允许程序处理不定量的输入数据对。
-
int a = in.nextInt();
:这行代码读取输入中的下一个整数并将其赋值给变量a
。 -
int b = in.nextInt();
:同样,这行代码读取随后的下一个整数并将其赋值给变量b
。 -
System.out.println(a + b);
:这行代码计算两个整数a
和b
的和,并输出结果。
1. Scanner 类
的
Scanner
类是最常用的一种方式,因为它简单易用,能够解析不同类型的数据,包括整数、浮点数、字符串等。Scanner scanner = new Scanner(System.in); int number = scanner.nextInt(); String str = scanner.nextLine();优点:易于使用,可以直接解析多种类型的输入。 使用场景:适用于大多数情况,尤其是需要解析特定数据类型的输入时。
2. BufferedReader 和 InputStreamReader 类
BufferedReader
配合InputStreamReader
可以用来读取文本数据。这种方法能够读取一行文本,然后你可以使用如Integer.parseInt
等方法将文本转换为其他类型。BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String line = reader.readLine(); int number = Integer.parseInt(line);优点:读取效率高,适合读取大量文本数据。 使用场景:需要读取字符串或者一行完整数据,尤其是在处理大量数据时。
3. Console 类
Java的
Console
类提供了一种方法来读取控制台输入。它不仅可以读取字符串,还可以读取密码或其他敏感数据,而不会在控制台上显示输入的字符。Console console = System.console(); String input = console.readLine(); char[] password = console.readPassword();优点:能够读取密码等敏感信息而不显示在控制台上。 使用场景:适用于需要处理敏感信息的命令行程序。
4. DataInputStream 类
虽然
DataInputStream
主要用于处理数据输入流,但也可以用来从标准输入中读取数据。DataInputStream dis = new DataInputStream(System.in); int number = dis.readInt();优点:可以直接读取原始数据类型。 使用场景:适用于需要从标准输入读取二进制数据的情况。
5. System.in
直接使用
System.in
也是可能的,但这种方式相对不那么方便,因为System.in
是一个输入流,主要用于读取字节。int byteData = System.in.read(); // 读取一个字节的数据优点:直接使用,不需要额外的类。 使用场景:适用于低级操作,比如直接处理字节数据
输出和
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int t = 100;
for(int i = 0; i < t; i++){
int a = in.nextInt();
int b = in.nextInt();
if(a != 0 || b != 0){
System.out.println(a+b);
}else {
System.exit(0);
}
}
}
}
hasNext()
在Java中,
while(in.hasNext()){ ... }
循环结构是一种常用于处理输入流(如标凈输入、文件等)的方法。这个循环依赖于Scanner
对象in
的hasNext()
方法来决定是否继续执行循环体内的代码。下面详细解释这个结构的功能和作用:
Scanner
类的hasNext()
方法
功能:
hasNext()
方法检查输入流中是否还有下一个输入元素。这个方法返回一个布尔值:如果输入流中有更多的数据,则返回true
;如果输入流已经结束,则返回false
。需要注意的是,hasNext()
方法并不会移动扫描器的位置,也就是说,它不会实际读取或消耗任何数据,仅仅是进行检查。作用:在使用
while
循环配合hasNext()
时,它的主要作用是确保只要输入流中还有数据未被处理,循环就会继续执行。这使得处理动态或未知长度的输入数据变得简单高效。循环的工作原理
循环开始时的检查:在每次循环开始之前,
hasNext()
方法会被调用来检查输入流中是否还有数据。如果有,循环进入下一轮迭代;如果没有,循环结束。读取和处理数据:在循环体内,可以使用
Scanner
对象的next()
,nextInt()
,nextLine()
等方法来读取具体的数据,并进行相应的处理。这些读取方法会移动扫描器的位置,并实际消耗输入流中的数据。适用于各种类型的输入:通过使用
hasNextInt()
,hasNextDouble()
,hasNextLine()
等方法代替hasNext()
,可以更精确地控制期望的输入类型,使得循环仅在遇到特定类型的数据时继续。
System.exit(0);
System.exit(0)
是Java中用于终止当前运行的Java虚拟机(JVM)的方法。这个方法接受一个参数,该参数作为状态码返回给系统。状态码是一个整数,它向系统提供信息关于程序终止的原因或方式。在System.exit
方法被调用时,程序将立即停止执行。参数解释
状态码
0
:通常用来表示程序正常结束。它是告诉操作系统或调用程序,当前程序已经顺利完成任务,没有发生任何错误。非零状态码:用来表示程序异常终止。具体的非零值可以用来表示不同类型的错误或终止原因,这些具体的值和含义可以由程序员自定义或遵循特定的约定。
方法工作原理
当
System.exit(0)
被调用时,Java虚拟机将执行以下操作:
执行所有通过
Runtime.getRuntime().addShutdownHook(Thread hook)
注册的关闭钩子(shutdown hooks)。这些关闭钩子是初始化但未启动的线程,当虚拟机开始关闭过程时,这些线程会被启动并运行完成。它们可以用来清理资源,如关闭文件句柄或数据库连接。释放虚拟机占用的资源,如内存和打开的文件。
终止JVM,返回状态码给操作系统。
使用注意
立即停止:调用
System.exit(0)
会立即停止程序运行,不会继续执行任何后续代码。资源管理:因为
System.exit(0)
会导致程序立即停止,所以在调用它之前,需要确保已经适当管理了资源。例如,你可能需要确保已经关闭了打开的文件流或数据库连接。关闭钩子:可以使用关闭钩子来执行清理任务,但需要注意的是,关闭钩子的执行顺序并不保证,且在关闭钩子执行期间,其他部分的JVM已经在关闭过程中了。
输出和 II
import java.util.Scanner;
/*
输入数据包括多组。
每组数据一行,每行的第一个整数为整数的个数n(1 <= n <= 100), n为0的时候结束输入。
接下来n个正整数,即需要求和的每个正整数。
输出:每组数据输出求和的结果
*/
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int t = 100;
for(int i = 0; i < t ; i++){
int n = in.nextInt();
if(n == 0) System.exit(0);
int count = 0;
for(int j = 0; j < n; j++){
int num = in.nextInt();
count += num;
}
System.out.println(count);
}
}
}
数组
寻找数组的中心下标
题目:
给你一个整数数组
nums
,请计算数组的 中心下标 。数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。
如果中心下标位于数组最左端,那么左侧数之和视为
0
,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回
-1
。
-
计算数组总和
-
从前向后count
-
如果在某个count = 数组总和 减去此前每一个数值(count包括当前数 = 数组总和减去当前以前的数 = 当前数加上以后的数)
class Solution {
public int pivotIndex(int[] nums) {
if(nums.length == 1){
return 0;
}
int sum = 0;
for(int i = 0; i <nums.length;i++){
sum += nums[i];
}
int total = 0;
for(int j = 0; j <nums.length;j++){
total += nums[j];
if(sum == total){
return j;
}
sum -= nums[j];
}
return -1;
}
}
找素数
素数是一个大于1的自然数,它没有正除数(或因子)其他于1和它本身。换句话说,一个素数只能被1和它自身整除。比如,2、3、5、7、11都是素数,因为它们只能被1和它自身整除。值得注意的是,2是唯一一个偶数素数,因为除了2以外的所有偶数都至少能被2整除。
遍余平方根
在编写Java代码以找出0到100之间的所有素数之前,我们可以先确定一个数n是不是素数的方法:检查从2到sqrt(n)(即n的平方根)之间是否有任何数能整除n。如果没有,则n是素数。这个方法利用了一个事实:如果n不是素数,则它必须有一个因子f,使得f<=sqrt(n)。
public class 素数 {
public static void main(String[] args) {
// 定义起始和结束范围
int start = 0;
int end = 100;
// 遍历从start到end的每个数
for (int i = start; i <= end; i++) {
// 调用isPrime函数检查i是否为素数
if (isPrime(i)) {
System.out.println(i);
}
}
}
// 检查一个数是否为素数的函数
public static boolean isPrime(int number) {
// 如果数字小于2,则它不是素数
if (number < 2) {
return false;
}
// 检查从2到该数的平方根之间是否有因子
for (int i = 2; i <= Math.sqrt(number); i++) {
// 如果找到一个因子,则该数不是素数
if (number % i == 0) {
return false;
}
}
// 如果没有找到任何因子,则该数是素数
return true;
}
}
埃拉托斯特尼筛法
Sieve of Eratosthenes
目的是找出小于给定数值 n
的所有素数的数量。这个算法的核心思想是逐步排除那些为已知素数的倍数的数。
public static void main(String[] args) {
int n = 100;
boolean[] isPrime = new boolean[n]; // 如果 isPrime[i] 为 false,则表示数字 i 是素数;如果为 true,则表示不是素数;方便计算
int count = 0;
for(int i = 2; i < n; i++){
if(!isPrime[i]){
count++; // 从2开始,如果有是素数(boolen是false的)
System.out.println(i); // 当确认i是素数时打印i
for(int j = 2 * i;j < n; j+=i){
// 这个内层循环是筛选的核心。它从 i 的两倍开始(因为 i 的一倍是它自己,我们已经知道 i 是素数),以 i 为步长遍历数组,直到达到 n。这样,它可以遍历 i 的所有倍数。
// 比如i=2,第一次j=4,给索引4置true是合数不是素数
// 下一次,就是 j+=i = 4+2 = 6,给索引6置ture非素数
// 再下一次,j+=i = 6+2 = 8,给索引8置ture非素数
isPrime[j] = true;
}
//遍历一次后,跳出2的所有倍数为非素数true
// 整个循环后,下一次i = 3;j = 6;下一次 j+=i = 6+3 = 9;...
}
}
System.out.println("Total prime numbers up to " + n + ": " + count);
}
注意细节
-
要在
public static int calculatePrimeCount(int n) {
这种方法中才能return
-
public static void main(String[] args) {
有void
中是不能返回一个值的
动态规划
Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动规是由前一个状态推导出来的,而贪心是局部直接选最优的
1. 确定dp数组(dp table)以及下标的含义 2. 确定递推公式 3. dp数组如何初始化 4. 确定遍历顺序 5. 举例推导dp数组
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
这道题目我举例推导状态转移公式了么?
我打印dp数组的日志了么?
打印出来了dp数组和我想的一样么?
斐波那契数
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。
-
确定dp数组以及下标的含义
dp[i]的定义为:第i个数的斐波那契数值是dp[i]
-
确定递推公式
题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
-
dp数组如何初始化
题目中把如何初始化也直接给我们了,如下:
dp[0] = 0; dp[1] = 1;
-
确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
-
举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
class Solution {
public int fib(int n) {
// 基础情况处理: 当 n < 2 时,函数直接返回 n。这意味着对于 n = 0 和 n = 1 的情况,直接返回其值,分别对应斐波那契序列的第0项和第1项。
if(n<2){
return n;
}
int a = 0;
int b = 1;
int c = 0;
// 从第三个数开始计算
for(int i = 2;i <= n;i++){
//也可以是for(int i = 1;i < n;i++){
c = a + b;
a = b;
b = c;
}
return c;
}
}
-
for(int i = 2;i <= n;i++){
//也可以是for(int i = 1;i < n;i++){
-
//非压缩状态的版本
class Solution { public int fib(int n) { if (n <= 1) return n; int[] dp = new int[n + 1]; // 通过定义 int[] dp = new int[n + 1];,代码确保了数组有足够的空间来存储斐波那契数列中的所有需要的值,从而能够正确计算出第 n 个斐波那契数的值。 // 可以直接通过 dp[i] 访问斐波那契数列的第 i 个数,无需再进行计算。 dp[0] = 0; dp[1] = 1; for (int index = 2; index <= n; index++){ dp[index] = dp[index - 1] + dp[index - 2]; } return dp[n]; } }
贪心算法
跳跃游戏
题目:
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
-
这个只需要找一个覆盖范围,达到最后
-
cover不断更新,取max(cover,nums[i])
class Solution {
public boolean canJump(int[] nums) {
if(nums.length == 1) return true;
int n = nums.length -1;
int cover = 0;
for(int i = 0;i <= cover; i++){
cover = Math.max(cover, nums[i] + i);
if(cover >= n ) return true;
}
return false;
}
}
摆动序列
题目:
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
遍历
-
考虑只有一个数的情况
-
做差得Diff
-
只要刚开始的Diff不为0,则至少2个摆动数
-
当当前差值和前一个差值符号不同时,才算作一次摆动。注意后面的 这里是curDuff不为0,也就是后面不是平的
-
后再找curDiff
-
统计的次数
这个过程中,要注意,正常序列以 preDiff==0
开始的情况
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length < 2) return nums.length;
int count = 1; // 至少有一个数
int preDiff = nums[1] - nums[0];
// 如果第一个差值不为0,那么摆动序列至少有2个数
if (preDiff != 0) {
count = 2;
}
for (int i = 1; i < nums.length - 1; i++) {
int curDiff = nums[i + 1] - nums[i];
// 只有当当前差值和前一个差值符号不同时,才算作一次摆动
if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
// 注意这里是curDuff不为0,也就是后面不是平的
count++;
preDiff = curDiff;
}
// 如果preDiff为0,我们需要更新它以保持最新的非零差值
else if (preDiff == 0) {
preDiff = curDiff;
}
}
return count;
}
}
贪心
注意后面的 这里是curDuff不为0,也就是后面不是平的
class Solution {
public int wiggleMaxLength(int[] nums) {
if (nums.length <= 1) {
return nums.length;
}
//当前差值
int curDiff = 0;
//上一个差值
int preDiff = 0;
int count = 1;
for (int i = 1; i < nums.length; i++) {
//得到当前差值
curDiff = nums[i] - nums[i - 1];
//如果当前差值和上一个差值为一正一负
//等于0的情况表示初始时的preDiff
if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
// 注意这里是curDuff不为0,也就是后面不是平的
count++;
preDiff = curDiff;
}
}
return count;
}
}
动态规划
对于我们当前考虑的这个数,要么是作为山峰(即 nums[i] > nums[i-1]),要么是作为山谷(即 nums[i] < nums[i - 1])。
-
设 dp 状态
dp[i][0]
,表示考虑前 i 个数,第 i 个数作为山峰的摆动子序列的最长长度 -
设 dp 状态
dp[i][1]
,表示考虑前 i 个数,第 i 个数作为山谷的摆动子序列的最长长度
则转移方程为:
-
dp[i][0] = max(dp[i][0], dp[j][1] + 1)
,其中0 < j < i
且nums[j] < nums[i]
,表示将 nums[i]接到前面某个山谷后面,作为山峰。 -
dp[i][1] = max(dp[i][1], dp[j][0] + 1)
,其中0 < j < i
且nums[j] > nums[i]
,表示将 nums[i]接到前面某个山峰后面,作为山谷。
初始状态:
由于一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1
。
-
设置一个dp 状态
dp[i][0]
表示 第 i 个数作为 山峰 的摆动子序列的最长长度 -
转移方程,每次对于当前 山峰 ,统计,选大的(之前任意一个比他小的点的山谷 序列长度 +1) 和(该峰点默认最少的1)
class Solution {
public int wiggleMaxLength(int[] nums) {
int[][] dp = new int[nums.length][2];
// i 0 作为以i为波峰的最大序列长度;
// i 1 作为以i为波谷的最大序列长度;
dp[0][0] = dp[0][1] = 1;
for(int i = 0; i < nums.length; i++){
dp[i][0] = dp[i][1] = 1;
// 每个点自己至少之前一个波峰或波谷
for(int j = 0;j < i; j++){
if(nums[j]>nums[i]){
dp[i][1] = Math.max(dp[j][0] + 1 , dp[i][1]);
}else if(nums[j] < nums[i]){
dp[i][0] = Math.max(dp[j][1] + 1, dp[i][0]);
}
}
}
return Math.max(dp[nums.length-1][0],dp[nums.length -1][1]);
}
}
最大子序和
题目:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
贪心算法从整数开始count
-
count加下一个数如果count<0;则从下一个不为负的数开始再算
class Solution {
public int maxSubArray(int[] nums) {
/*
1. 从整数开始count
2. count加下一个数如果count<0;则从下一个不为负的数开始再算
*/
if(nums.length == 1) return nums[0];
int sum = Integer.MIN_VALUE;
int count = 0;
for(int i = 0; i < nums.length; i++){
count += nums[i];
sum = Math.max(sum, count);
if(count<0){
count = 0;
}
}
return sum;
}
}
-
注意,
int sum = Integer.MIN_VALUE;
要从最小值开始,因为后面要max,防止负的不计入 -
注意,先统计取sum,再处理局部计数数值,防止负的处理不上
DP算法(慢)
-
用dp[i]表示在i之前连续的最大和
-
dp[i] = max(dp[i-1]+nums[i],nums[i])
-
res = max[dp[i],res]
public int maxSubArray(int[] nums){
/*
动态规划的方法:
1. 用dp[i]表示在i之前连续的最大和
2. dp[i] = max(dp[i-1]+nums[i],nums[i])
3. sum = max[dp[i],sum]
注意第一个数的处理,要初始化sum和dp时添加,防止负数不计入
*/
int[] dp = new int[nums.length];
dp[0] = nums[0];
int sum = dp[0];
if(nums.length == 1) return nums[0];
for(int i = 1; i < nums.length; i++){
dp[i] = Math.max( dp[i-1] + nums[i], nums[i]);
sum = Math.max( dp[i] , sum);
}
return sum;
}
如果找到局部最优,然后推出整体最优,那么就是贪心
买卖股票的最佳时机 II
题目:
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
贪心算法
计算两天之间的差值,直接累加正的差值
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1) return 0;
int max = 0;
for(int i = 1; i < prices.length; i++){
if(prices[i]>prices[i-1]){
max += prices[i] - prices[i-1];
}
}
return max;
}
}
动态规划
-
用
dp[][]
表示第i天
是否持有股票,0不持有,1持有
,的时候 的利润
-
初始化
dp[0][0] = 0
dp[0][1] = -prices[0]
-
转移方程
dp[i][0] = max(dp[i-1][0] , prices[i] + dp[i-1][1])
dp[i][1] = max(dp[i-1][1] . - prices[i] + dp[i-1][0])
public int maxProfit(int[] prices) {
// 动态规划方法
// dp[i][0]表示在第i天结束时不持有股票的情况下的最大利润
// dp[i][1]表示在第i天结束时持有股票的情况下的最大利润
int dp[][] = new int[prices.length][2];
// 初始化
// 第0天结束时不持有股票,利润为0
// 第0天结束时持有股票,利润为负的第0天股票价格(因为我们买入了股票)
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; i++) {
// 对于每一天i,我们都有两种选择:持有股票或不持有股票
// 如果今天结束后我们不持有股票,那么:
// 1. 我们可能是从昨天不持有股票的状态继续过来的;
// 2. 或者是昨天持有股票,但今天卖出了股票,因此要加上今天的股票价格。
// 我们选择这两种情况下利润更大的一个。
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
// 如果今天结束后我们持有股票,那么:
// 1. 我们可能是从昨天持有股票的状态继续过来的;
// 2. 或者是昨天不持有股票,但今天买入了股票,因此要减去今天的股票价格。
// 我们选择这两种情况下利润更大的一个。
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
// 最终,我们的最大利润会在不持有股票的情况下实现,因为持有股票意味着还未实现利润。
return dp[prices.length-1][0];
}
机试真题
参考自b站:万诺coding
跳跃台阶
动态规划(暴力)
逻辑解析
-
初始化:创建一个名为
dp
的数组,用来记录到达每一级台阶所需的最少跳跃次数。因为你还没有开始跳跃,所以除了起点以外,其他所有台阶都被视为“暂时无法到达”,这就是为什么要将dp
数组的所有元素初始化为Integer.MAX_VALUE
(一个非常大的数,代表“无穷大”或“不可达”状态)的原因。 -
起点:将
dp[0]
设置为0,因为你已经站在第一个台阶上了,不需要任何跳跃就可以到达。 -
动态规划:遍历每一级台阶,对于当前台阶
i
,检查你能跳到的每个可能的下一个台阶i+j
(j
是从1到powers[i]
的值)。使用Math.min(dp[i+j], dp[i]+1)
来更新到达i+j
台阶的最小跳跃次数。这意味着,“到达当前台阶的最少跳跃次数+1”与“已知到达i+j
台阶的最少跳跃次数”之间较小的一个,会成为新的到达i+j
台阶的最少跳跃次数。// 更新到达i+j台阶的最少跨越次数;dp[i + j]是此前该台阶最小条跳跃数,dp[i] + 1是从次点开始跳跃过去的最小跳跃数
-
检查能否到达终点:最后,检查
dp[n-1]
(到达最后一个台阶的最少跳跃次数)是否小于或等于k
。如果是,说明你可以在跳跃次数限制内到达最顶端;否则,表示无法在k
次跳跃内到达,应返回-1。
package 贪心算法;
/*
华为机试第一题
输入:台阶长度n <= 100000
台阶魔力值(有多少魔力可以跨越多少个台阶)[M1,M2...M<= 100000]
最大跨越次数k <= 100000
输出:拿到魔法奥秘最少跨越次数;如果不能返回-1
*/
import java.util.Arrays;
import java.util.Scanner;
public class 跨越台阶 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 读取台阶总长度n
int n = Integer.parseInt(in.nextLine());
// 创建数组存储每个台阶的魔力值
int[] powers = new int[n];
// 分割输入的魔力值字符串,转换成魔力值数组
String[] lines = in.nextLine().split(" ");
for (int i = 0; i < n; i++) {
powers[i] = Integer.parseInt(lines[i]);
}
// 读取最大跨越次数k
int k = Integer.parseInt(in.nextLine());
// 调用findMinSteps方法计算并返回结果
int result = findMinSteps(n, powers, k);
// 打印结果
System.out.println(result);
}
// findMinSteps方法:计算达到最后一个台阶的最少跨越次数
private static int findMinSteps(int n, int[] powers, int k) {
// dp数组,dp[i]表示到达第i个台阶的最少跨越次数
int[] dp = new int[n];
// 初始化dp数组,设为最大值表示初始时除了起点外,所有台阶都无法到达
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0; // 起点台阶的跨越次数设为0,因为你已经在起点上了
// 遍历每个台阶,获取到达该台阶的最小跳跃数
for (int i = 0; i < n; i++) {
// 如果当前台阶可达
if (dp[i] != Integer.MAX_VALUE) {
// 尝试使用当前台阶的魔力值跳跃到后续的台阶
for (int j = 1; j <= powers[i] && i + j < n; j++) {
// 更新到达i+j台阶的最少跨越次数;dp[i + j]是此前该台阶最小条跳跃数,dp[i] + 1是从次点开始跳跃过去的最小跳跃数
dp[i + j] = Math.min(dp[i + j], dp[i] + 1);
}
}
// 不断更新迭代每个台阶的最小跳跃到达数
}
// 检查是否能在k步内到达最后一个台阶
// 如果dp[n-1]的值小于等于k,说明可以在k步内到达
if (dp[n - 1] <= k) {
return dp[n - 1]; // 返回到达最后一个台阶的最少跨越次数
} else {
return -1; // 如果不能在k步内到达,返回-1
}
}
}
贪心算法
package 贪心算法;
/*
华为第一题
输入:台阶长度n <= 100000
台阶魔力值(有多少魔力可以跨越多少个台阶)[M1,M2...M<= 100000]
最大跨越次数k <= 100000
输出:拿到魔法奥秘最少跨越次数;如果不能返回-1
*/
import java.util.Arrays;
import java.util.Scanner;
public class 跨越台阶 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = Integer.parseInt(in.nextLine()); // 台阶的总长度
int[] powers = new int[n]; // 存储每个台阶的魔力值的数组
String[] lines = in.nextLine().split(" "); // 读取台阶的魔力值,并用空格分割成数组
for (int i = 0; i < n; i++) {
powers[i] = Integer.parseInt(lines[i]); // 将字符串转换为整数存入powers数组
}
int k = Integer.parseInt(in.nextLine()); // 读取最大的跳跃次数限制
// 计算最少的跳跃次数
int result = findMinSteps(n, powers, k);
// 如果跳跃次数超过了最大限制,则输出-1,否则输出计算的跳跃次数
System.out.println(result > k ? -1 : result);
in.close(); // 关闭Scanner对象
}
// 使用贪心算法来找到到达最后一个台阶的最小跳跃次数
private static int findMinSteps(int n, int[] powers, int k) {
int result = dfs(0, powers); // 从第一个台阶开始递归搜索
return result > 100000 ? -1 : result; // 如果返回的结果超过了一个很大的数,说明失败了,返回-1
}
// 使用深度优先搜索(DFS)来递归地找到最小的跳跃次数
private static int dfs(int idx, int[] powers) {
if (idx >= powers.length - 1) return 0; // 如果当前已经在最后一个台阶上或者超过了,不需要再跳,返回0
if (powers[idx] == 0) return 100001; // 如果当前台阶的魔力值是0,表示无法前进,返回一个很大的数表示失败
int maxStep = 0, nextIdx = 0; // 初始化可以跳到的最远距离maxStep和下一个跳跃的台阶索引nextIdx
for (int i = 1; i <= powers[idx]; i++) { // 遍历当前台阶可以跳到的所有台阶
if (idx + i >= powers.length - 1) return 1; // 如果当前的跳跃可以直接到达最后一个台阶,直接返回1
// 如果通过跳到idx + i的台阶可以达到更远的距离,更新maxStep和nextIdx
if (powers[idx + i] + i + idx > maxStep) {
maxStep = powers[idx + i] + i + idx; // 新台阶“i + idx”;新台阶的魔力值能跳到的powers[i + idx]
nextIdx = idx + i;
}
}
// 从新的台阶索引nextIdx继续递归搜索,并且跳跃次数加1
return dfs(nextIdx, powers) + 1;
}
}
-
for
循环从 1 开始,遍历所有从当前台阶idx
出发,可能一跳能够达到的台阶。循环变量i
表示当前尝试的跳跃距离。 -
在每次迭代中,我们计算如果从当前台阶
idx
跳跃i
个台阶,然后从那个新的台阶idx + i
出发,能够再跳跃的最大距离。这个总距离是由两部分组成的:i
(当前跳跃的距离)和powers[idx + i]
(新台阶的魔力值,表示从新台阶再次跳跃能达到的距离)。 -
我们用
powers[idx + i] + i + idx
计算当前尝试跳跃能达到的最远总距离。powers[idx + i]
是从新台阶idx + i
出发,最多能跳跃的台阶数,i + idx
是我们已经到达的台阶位置。 -
如果这个总距离大于当前记录的
maxStep
(到目前为止找到的最远距离),我们就更新maxStep
为这个更远的距离,并将nextIdx
设置为这个新的台阶索引idx + i
。 -
通过这种方式,我们不断更新
maxStep
和nextIdx
,直到找到从当前台阶idx
出发能一跳到达的、并且结合下一跳能够达到最远距离的那个台阶。