由于各种原因,我在平时写代码或刷题时,每隔一定时期的就要进行语言切换(C、C++、Python、JavaScript、Java),导致一些常用的语法或写题技巧老是记混,每次卡壳都要去搜一搜。

本文将随着刷题进度持续记录一些使用 Java 语言刷题时必须记住的要素,快速解决在语法方面的障碍。如果你打算开始使用 Java 语言进行刷题时,可以通篇浏览实现快速热身。

本文主要内容:

  • 必背写法与重要函数用法
  • 集合增删改查与字符串操作速查
  • ACM 模式
  • 语言无关的通用技巧

本文题目难度标识:🟩简单,🟨中等,🟥困难。

基础语法要点

看看爪哇代码长啥样先,试图唤起尘封的记忆 😅😅😅

1
2
3
4
5
6
7
8
9
10
11
public class Solution{                             // 一个文件一个类
public static void main(String[] args){ // main 函数的写法
// 变量的声明方式
int n=1,m; // 声明变量+初始化
Map<Integer,String> map = new HashMap<>(); // 声明对象

// 迭代方式
for(int i=0;i<3;i++){}; // 基础for循环
for(int e:nums){}; // foreach 语法格式
}
}

一些注意点,容易和别的语言混淆:

右移操作 取模运算
算数右移:>>
逻辑右移:>>>
由于 Java 取模的特殊性,当被除数为负数时取模结果为负数,需要纠正:

int modulus = (sum % k + k) % k;

相关题目:🟨 974. 和可被 K 整除的子数组 - 力扣(LeetCode)

数组 Arrays

声明与初始化

1
2
3
4
5
6
7
8
9
10
11
// 一维数组
int[] nums1 = new int[n]; // 动态初始化 初始值 0、null(对于对象数组)
int[] nums2 = new int[]{1,2,3,4,5}; // 静态初始化
int[] nums3 = new int[]{v,printInfo()}; // 静态初始化接受变量和返回值


// 二维数组
int a[][] = {{1,2,3},{4,5,6},{7,8,9,10}};
int b[][] = {{},{2},{},{3,5}};
int [][]m = new int[3][]; // 每列指向一个 null 值
int [][]n = new int[3][2]; // 每格初始化为 0

排序 Arrays.sort

1
2
3
4
5
6
7
int[] nums = new int[]{4,5,2,7,3,1,0,6,8,9};  
Arrays.sort(nums); // 默认将nums正序排序

Integer[] nums = new Integer[]{4,5,2,7,3,1,0,6,8,9};
Arrays.sort(nums, Collections.reverseOrder()); // 倒序排序。数组必须是包装类型数组。

Arrays.sort(nums, (a,b)-> b-a); // 使用 Lambda 表达式实现倒序排序。

Arrays.stream

Arrays.stream() 可以将数组转换为流,以使用更多的操作。

1
2
3
4
5
6
int[] arr1 = {1, 2, 3, 4, 5};  
int sum = Arrays.stream(arr1).sum();
int max = Arrays.stream(arr1).max() // OptionalInt
.getAsInt(); // int
int min = Arrays.stream(arr1).min().getAsInt();
double avg = Arrays.stream(arr1).average().getAsDouble();

其他

使用 Arrays.equals(array1, array2); 比较两个数组的元素是否相等。

1
2
3
4
5
// 比较两个数组是否相等
int[] array1 = new int[]{1,2,3};
int[] array2 = new int[]{1,2,3};
int[] array3 = array1;
Arrays.equals(array1, array2); // 判断两个数组是否相等。以上3个数组使用Arrays.equals比较均为true

Arrays.copyOf() 方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。Arrays.copyOf() 的第二个参数指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。

1
2
3
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOf(arr1, 3); // arr2 = [1,2,3]
int[] arr3 = Arrays.copyOf(arr1, 10); // arr3 = [1,2,3,4,5,0,0,0,0,0]

Arrays.fill() 将指定的 int 值分配给指定 int 型数组的每个元素。

1
2
int[] arr1 = {1, 2, 3, 4, 5};  
Arrays.fill(arr1, 9); // arr = [9,9,9,9,9]

字符串 String

1
2
3
4
5
// 字符串相等判断
String str1 = "hello"; // 定义字符串常量
String str2 = new String("hello");
boolean isEquals1 = str1.equals(str2); // 字符串的比较 true
boolean isEquals2 = str1 == str2 // false
操作 String StringBuffer/StringBuilder
+ 操作符。注意从左到右结合性 sb.append(str);
sb.insert(int offset, str);
不可变类 sb.delete(start, end)
sb.deleteCharAt()
改(替换字符) replace(char oldChar, char newChar):返回新的字符串,原字符串不改变。
replaceAll(String regex, String replacement) :返回新的字符串,原字符串不改变。
replaceFirst(String regex, String replacement):返回新的字符串,原字符串不改变。
sb.replace(start,end, str);:将起始位置为 start,结束位置为 end-1 的子串替换为 str。不生成新的 StringBuilder 对象,在原来的 StringBuilder 对象上修改

sb.setCharAt(index,c)
str.charAt(index); 相同
长度 str.length(); 相同

StringBuilder 还提供以下函数:

  • sb.reverse():反转字符串
  • sb.toString():返回字符串
  • sb.capacity():返回当前容量。注意,和返回有效长度 sb.length() 不一样!

字符、字符串与数字之间的转换

String 进制型 String Number 类 char char[]
String - 静态方法:
Double.parseDouble(s);
Double.valueOf(s).doubleValue();
实例方法:
new Double(s).doubleValue();
s.toCharArray();
进制型 String - Integer.parseUnsignedInt(str,2);
Number 类 ""+d;
Double.toString(d);
new Double(d).toString();
String.valueOf(d);

Integer.toBinaryString(int i);
Integer.toOctalString(int i);
Integer.toHexString(int i);
Longintl.intValue();
char String.valueOf(c); -
char[] new String(charArr); -

一招静态方法 valueOf 可以解决大部分转换难题。被转的放里面。

我们在处理输入输出字符串题目时的连招:

1
2
3
4
5
6
7
8
// 把字符串转换成字符数组处理,更丝滑。不然只能疯狂 s.charAt(i) 咯
public String mySolution(String s){
char[] cc = s.toCharArray(); // 将题目输入字符串 `s` 转换为临时数组 `cc`
/*
对数组 `cc` 进行处理
*/
return new String(cc);
}

StringBufferStringBuilder 中的构造方法和其他方法几乎是完全一样的。StringBuilder 非线程安全,但速度更快。一般情况下(比如刷题)推荐使用 StringBuilder

String 保留小数精度的处理可看后续的「ACM 模式下的输入输出」章节。

基础函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 输出
System.out.println(); // 输出并换行
System.out.print(); // 输出不换行

// 数学与常量
Math.max(num1,num2);
Math.min(num1,num2);
Math.abs(num);
Integer.MAX_VALUE;
Integer.MIN_VALUE;

// 长度
int len1 = nums.length; // 数组长度
int len2 = str.length(); // 字符串长度
int len3 = map.size(); // 集合类长度

集合 Collection/Map

image.png

集合增删改查速览:

img接口 Collection List Deque
add(E e) add(int index,E element) addFirst()
addLast()
remove(Object o) 移除指定对象
clear() 清空所有内容
对空链表操作抛异常:
removeFirst()
removeLast()
set(int index, E element)

get(int index) getFirst()
getLast()
判断 contains(Object o)
isEmpty()
img接口 Map
put(key,value)
remove(key)
put(key,newValue)
get(key)
getOrDefault(key,defaultValue)
判断 containsKey(key)
containsValue(value)
集合 entrySet()
keySet()
valueSet()
易错点:getOrDefault 的使用

有时候我们会编写这样的代码:map.getOrDefault(key,func());,我们会误以为只有 map 不存在 key 时才会调用 func()。事实上,Java 在传递参数给方法前,都会计算参数的值。因此即使 key 存在,func() 还是会执行一次。

如果我们需要 map 不存在时才执行 func(),可以使用 computeIfAbsent 函数:map.computeIfAbsent(key,e->func());

或者使用三元运算符:int result = map.containsKey(key)? map.get(key) : func();

参考:java.util.Map 的 getOrDefault() 是如何工作的? - SegmentFault 思否

ACM 模式下的输入输出

相信我们已经非常熟悉 LeetCode 那种「核心代码」的写题模式,在这种模式下,我们可以更专注于代码逻辑本身。然而一些公司或学校的机考仍然使用 ACM 文件输入输出模式,在这种模式下需要对题目输入输出的逻辑进行额外的处理。

最最基本的输出写法。

1
System.out.println("Hello Nowcoder!"); // 换行输出 IDEA 下快捷输入:sout

java.util.Scanner 是 Java5 的新特征,我们可以通过 Scanner 类来获取用户的输入。

1
2
// 创建 Scanner 对象的基本语法
Scanner in = new Scanner(System.in);

输入时用到类 Scanner 类,里面有几个常用方法:

  • hasNext():用于判断是否还有输入的数据,常放在 while 循环中判断输入是否结束。
  • next():读到空格就停止读取。适合读取单个字符或字符串。返回类型 String
  • nextLine():读取一整行数据,碰到换行则停止。返回类型 String

next()nextLine() 区别:

函数 next() nextLine()
是否阻塞 一定要读取到有效字符后才可以结束输入
结束符 对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。 以 Enter 为结束符,该返回的是输入回车之前的所有字符。
带空格字符串 🟥 🟩

如果要输入 intfloat 类型的数据,在 Scanner 类中也有支持,但是在输入之前最好先使用 hasNextXxx() 方法进行验证,再使用 nextXxx() 来读取。

  • hasNextInt() / nextInt():遇到空格时会停止读取,返回的结果为空格前读取到的部分。返回类型 int
  • hasNextDouble() / nextDouble()
next()nextLine() 连用时的注意事项

next()nextInt() 读取数据后指针还在当前行,如果紧跟 nextLine(),读取数据会出错。处理方法是:增多一个 nextLine()「吃掉」换行符即可。具体案例详看下文案例「单组字符串输入输出」。

如果全程只使用 nextInt() 不会有上述问题。

形式 A:单组/多组 + EOF/零尾模式

输入结束模式:

  • EOF 结束模式:输入有多组,文件末尾即输入结束。
  • 零尾模式:输入有多组,输入参数均为 0 时即输入结束。

给定两个整数 a 和 b ,请你求出 a+b 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
************************* EOF模式 ***************************
输入:
1 2
114 514
2024 727

输出:
3
628
2751

************************* 零尾模式 ***************************
输入:
1 2
114 514
2024 727
0 0

输出:
3
628
2751
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNextInt()) { // 注意 while 处理多个 case
int a = in.nextInt();
int b = in.nextInt();

/*
零尾模式只需在此处增加以下语句即可
if(a==0 && b==0)break;
*/

System.out.println(a + b);
}
}
}

形式 B:T 组

给定两个整数 a 和 b ,请你求出 a+b 的值。

首先会给出一个数字 T,然后给出 T 组数字。

1
2
3
4
5
6
7
8
9
10
输入:
3
1 2
114 514
2024 727

输出:
3
628
2751
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
int T = in.nextInt();

while (T--!=0) { // 注意 while 处理多个 case
int a = in.nextInt();
int b = in.nextInt();
System.out.println(a + b);
}
}

形式 C:单组字符串的输出与输出

这里将探讨 nextInt()nextLine() 混用的问题。

给定一个长度为 n 的字符串 s ,请你将其倒置,然后输出。

1
2
3
4
5
6
输入:
5
abcde

输出:
edcba
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
Scanner in = new Scanner(System.in);

int len = Integer.valueOf(in.nextLine());
String str = in.nextLine();

char[] res = new char[len];
for(int i=0;i<len;i++){
res[len-1-i]=str.charAt(i);
}

System.out.println(new String(res));
}

注意到,上面的参考答案都统一使用了 nextLine() 函数读取输入。那么我们能不能用 nextInt()nextLine() 混合读取呢?答案是可以的,但是容易出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Scanner in = new Scanner(System.in);

int len = in.nextInt();
/** 位置 A **/
String str = in.nextLine();

System.out.println("读取到的len:"+len);
System.out.println("读取到的str:"+str);
}
/*
输出:
读取到的len:5
读取到的str:
*/

我们发现 str 并没有读取成功,这是因为第一次在读取 nextInt() 时,指针还在当前行,第二个 nextLine() 只读取到一个换行符。解决方法是在读取 nextInt() 后,在上面代码中「位置 A」处使用 nextLine()「吃掉」第一行的换行符即可。

形式 D:字符串格式化

给定一个小数 n ,请你保留 3 位小数后输出。
如果原来的小数位数少于 3 ,需要补充 0 。
如果原来的小数位数多于 3 ,需要四舍五入到 3 位。

看到这题目时我还纳闷,以前在 LeetCode 刷题就好少这种返回指定精度数组并要求保留小数点后的 0 的,其实仔细想想以前刷题的核心代码模式要么返回 intdouble,不需要我们处理输出的位数或补充 0。而这 ACM 模式输出的是 String,对输出自然会有另外的格式化要求。

1
2
3
4
5
输入:
1.23

输出:
1.230
1
2
3
4
5
6
7
8
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNextDouble()) { // 注意 while 处理多个 case
double a = in.nextDouble();
System.out.println(String.format("%.3f", a).toString());
}
}

String.format 作为文本处理工具,为我们提供强大而丰富的字符串格式化功能。

对浮点数进行格式化:占位符格式为 %[index$][标识]*[最小宽度][.精度]转换符

可用标识 可用转换符
- -,在最小宽度内左对齐,不可以与 0 标识一起使用。
- 0,若内容长度不足最小宽度,则在左边用 0 来填充。
- #,对 8 进制和 16 进制,8 进制前添加一个 0,16 进制前添加 0x
- +,结果总包含一个 +- 号。
- 空格,正数前加空格,负数前加 - 号。
- ,,只用与十进制,每 3 位数字间用 , 分隔。
- (,若结果为负数,则用括号括住,且不显示符号。
- b,布尔类型,只要实参为非 false 的布尔类型,均格式化为字符串 true,否则为字符串 false
- n,平台独立的换行符, 也可通过 System.getProperty(“line.separator”) 获取。
- f,浮点数型(十进制)。显示 9 位有效数字,且会进行四舍五入。如 99.99
- a,浮点数型(十六进制)。
- e,指数类型。如 9.38e+5
- g,浮点数型(比 %f%a 长度短些,显示 6 位有效数字,且会进行四舍五入)

更多示例:

语言无关的通用技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 循环增减
idx=(idx+1) % SIZE;
idx=(idx + SIZE - 1) % SIZE;

// 布尔大小比较转为输出整数
(a>b)-(a<b) // 可以得到 a 和 b 的大小关系,小于是 -1,等于是 0,大于是 1

// 四舍五入
double x;
x = (int)((x * 1000) + 0.5) / 1000.0) // 保留3位小数就乘1000、除以1000.0

// double计算时提高精度
double y;
int iy;
iy = (int)((y) + 0.5) / 1.0) // 先四舍五入到合适精度
if(Math.abs(iy-y)<0.0001)y=iy; // 视情况设范围

有时候有些对输入顺序有要求的,我们可以稍微调整一下顺序。

1
2
3
4
5
6
7
8
9
10
11
// 函数参数交换
public static Object foo(int param1, int param2) {
if (...){
// 在指定条件下调换顺序,省去手动转移的麻烦
return foo(param2, param1);
}
/*
按既定参数顺序的函数逻辑
*/
return result;
}

写题时常见的初始化最大最小值:

  • 最大值:0x3f3f3f3f
  • 最小值:0xc0c0c0c0
为什么 ACM 中经常使用 0x3f3f3f3f 作为整型的最大值?

原因如下:

  1. 0x3f3f3f3f 的十进制是 1,061,109,567,是 10^9 级别的,而一般场合下的数据都是小于 10^9 的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形。而平时用的 Integer.MAX_VALUE=0x7fffffff 不能满足“无穷大加一个有穷的数依然是无穷大”这个条件,加上一个数后,根据补码规范它会变成了一个很小的负数。
  2. 0x3f3f3f3f 的 2 倍还是一个「无穷大」的数。
  3. 0x3f3f3f3f 的每 8 位(每个字节)都是相同的。我们在程序设计中经常需要使用 memset(a, val, sizeof(a)) 初始化一个数组 a,该语句把数值 val0x00~0xFF)填充到数组 a 的每个字节上,所以用 memset 只能赋值出“每 8 位都相同”的 int。当需要把一个数组中的数值初始化成正无穷时,为了避免加法算术上溢出或者繁琐的判断,我们经常用 memset(a, 0x3f, sizeof(a)) 给数组赋 0x3f3f3f3f 的值来代替。

本文参考