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

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

本文主要内容:

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

ACM 模式下的输入输出详看:站内文章【刷题日记】Java 快速热身——ACM 模式下的输入输出

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

Java 语法基础

代码风格

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

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 语法格式
}
}

讨论类的定义和静态上下文的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 一个文件可以有多个类,但只能有一个与文件名同名的public类
public class Solution{

class ClassA{} // 非静态内部类
static class ClassB{}
public static void main(String[] args){
// 如果在静态方法中直接引用非静态成员,Java 编译器无法确定应该访问哪个实例的成员,因此会报错。
// ClassA a = new ClassA(); // cannot be referenced from a static context
ClassB b = new ClassB();
ClassC c = new ClassC();
}

public void func(){
ClassA a = new ClassA();
}

}

class ClassC{} // 包级私有类

操作符

一些注意点:

操作 说明 备注
右移操作 算数右移:>>
逻辑右移:>>>
记得和别的语言进行区分
取模运算 由于 Java 取模的特殊性,当被除数为负数时取模结果为负数,需要纠正:

int modulus = (sum % k + k) % k;
相关题目:🟨 974. 和可被 K 整除的子数组 - 力扣(LeetCode)
优先级 恒等运算符优先级比位运算符更大,因此正确写法为:
(num&mask)==0x01
这是语言之间普遍的优先级

值传递

有时候搞不清 Java 到底是引用传递还是值传递,根本原因是「值传递」没有理解透。

Java 中的函数传递本质是值传递的。

函数中如果传递的参数是对象 Object,那么传递的只是对象的引用。我们可以利用这个引用去修改对象,但如果在函数内重新 new Object(),函数返回时,主函数不会发生变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
String str = "Hello";
modifyString(str);
System.out.println(str); // 输出:Hello
}

public static void modifyString(String str) {
str = "World"; // str现在指向了一个新的字符串对象 "World"
System.out.println(str); // 输出:World
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Node {
int value;
Node(int value) {
this.value = value;
}
}

public class Test {
public static void main(String[] args) {
Node node = new Node(10);
modifyNode(node);
System.out.println(node.value); // 输出:20
}

public static void modifyNode(Node node) {
node.value = 20; // 修改 node 对象的 value 属性
}
}

此外需要注意的是 Java 在传递参数给方法前,都会计算参数的值。后文聊到 MapgetOrDefault 方法时会提到。

数组 Arrays

声明与初始化

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


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

排序 Arrays.sort

详看:站内文章【刷题日记】Java 快速热身——排序

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.copyOf() 的源码解读详见文章:站内文章搞清楚 Java 中 List 的 toArray 方法

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 子串的获取:

  • str.substring(int beginIndex)
  • str.substring(int beginIndex,int endIndex)。不含 endIndex

String 大小写转换:

  • str.toUpperCase()
  • str.toLowerCase()

String 查字符、字符串第一次出现的位置:

  • str.indexOf(c)

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

单步骤转换:

image.png

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

Character[] char[] 互转详见:站内文章【刷题日记】Java 快速热身——排序

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

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 保留小数精度的处理可看 站内文章【刷题日记】Java 快速热身——ACM 模式下的输入输出

基础函数

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

// 数学与常量
Math.max(num1,num2);
Math.min(num1,num2);
Math.abs(num);
Math.pow(base, exp);
Math.log(num); // 以 e 为底。对于非常规底数可使用换底公式 log_x(y)=ln(y)/ln(x);
Integer.MAX_VALUE;
Integer.MIN_VALUE;

// 随机数
Random rand = new Random(); // java.util.Random
rand.nextInt(num); // int [0,num)
Math.random(); // double [0.0,1.0)

// 【知识点】长度的获取
int len1 = nums.length; // 数组长度
int len2 = str.length(); // 字符串长度
int len3 = map.size(); // 集合类长度

集合 Collection/Map

image.png

集合增删改查速览:

img接口 Collection List Queue Deque
add(E e) add(int index,E element) 抛异常:
add(e)
返回特殊值:
offer(e)
抛异常:
addFirst(e)addLast(e)
返回特殊值:
offerFirst(e)offerLast(e)
remove(Object o) 移除指定对象
clear() 清空所有内容
抛异常:
remove()
返回特殊值:
poll()
抛异常:
removeFirst()removeLast()
返回特殊值:
pollFirst()pollLast()
set(int index, E element)
请避免对空列表调用该函数

get(int index) 抛异常:
element()
返回特殊值:
peek()
抛异常:
getFirst()getLast()
返回特殊值:
peekFirst()peekLast()
判断 contains(Object o)
isEmpty()

栈结构的使用:

  • 队列也可用作 LIFO(后进先出)堆栈。应优先使用此接口而不是遗留 Stack 类。
  • List 也可用于简单的栈。ls.add(e) 方法作为入栈,ls.remove(ls.size() - 1) 作为出栈。

变长数组可以使用 ArrayList 进行实现。

img接口 Map
put(key,value)
remove(key) 删除 key 对应的键值对
clear() 清空 map
put(key,newValue)
get(key)
getOrDefault(key,defaultValue)
判断 containsKey(key)
containsValue(value)
集合 entrySet()
keySet()
易错点: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 思否

Map 的遍历

1
2
3
4
5
6
7
8
9
10
// Map 遍历元组对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
int val = entry.getValue();
}

// Map 遍历 key
for (String key : map.keySet()) {
int val = map.get(key);
}

哈希算法

有时候我们需要在集合中存储一个元素的二维坐标,我们不需要自己定义二元组存储,我们只需要存储第几个格子即可:i*n+j。或者将其组合成独一无二的 String 来作为 Key。

我们可以自己实现简单字符串的哈希算法。

不同哈希算法的粗略分析

下面列举出的算法是在通过例题 🟨 3137. K 周期字符串需要的最少操作次数 - 力扣(LeetCode) 前提下,对计算时间和复杂性权衡后的结果。

Hash 算法 时间 (ms) 内存 (MB) 时间击败比率 (%)
简易位运算 Hash 23 45.2 100
简易乘法 Hash 28 88.37
Java Map hash() 43 44.85 11.63
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 乘法 Hash
public int genHash(char[] cc,int start,int length){
int hash=0,i;
for (hash=length, i=0; i<length; ++i)
// 推荐的乘数还有:31, 131, 1313, 13131, 131313等等。
hash = 33*hash+cc[start+i];
return hash;
}

// 位运算 Hash
public int genHash(char[] cc,int start,int length){
int hash,i;
for (hash=length, i=0; i<length; ++i)
hash = (hash<<5)^(hash>>27)^cc[i+start];
return hash;
}

ArrayList 之间的转换

两者之间的转换除了 for 循环外,还有以下方式。

Array2List:

1
2
3
4
String[] array = {"A","B","C"}; // 基本类型数组的话必须是包装类型数组
List<String> ls = new ArrayList<>(Arrays.asList(array));
// 注意Arrays.asList(array)得到的List是固定长度的,无法完成添加或删除元素
// 因此外面还需要ArrayList包装一下
阿里巴巴 Java 开发手册对集合使用的相关要求

【强制】使用工具类 Arrays.asList() 把数组转换成集合时, 不能使用其修改集合相关的方法, 它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常。
说明: asList 的返回对象是一个 Arrays 内部类, 并没有实现集合的修改方法。 Arrays.asList 体现的是适配器模式, 只是转换接口, 后台的数据仍是数组。

List2Array:

1
2
List<String> ls = new ArrayList<String>(){{add("aa");add("bb");add("cc");}};
String[] array = ls.toArray(new String[0]);

注意 String[] array = (String [])ls.toArray(); 是不对的。因为 ls.toArray() 返回的 Object[] 类不能向下转型为 String[]。有关 ListtoArray 方法详看:站内文章这篇文章

阿里巴巴 Java 开发手册对集合使用的相关要求

【强制】 使用集合转数组的方法, 必须使用集合的 toArray(T[] array), 传入的是类型完全一致、 长度为 0 的空数组。

List 的复制

1
2
3
4
// 底层源码涉及到了 Arrays.toArray()
// 注意,对于自定义类型的数组不生效
List<Integer> target = new ArrayList<Integer>(origin);
// target 将是一个全新的数组,与 origin 不相关

集合的排序

集合的排序详见:站内文章【刷题日记】Java 快速热身——排序

TreeSet TreeMap 的一些常用方法

以下函数在与「线段树」有关的题目的题解下见得多。可以稍微了解下,毕竟玩 API 玩得好能节省时间。

  • tset.floor(E e) 方法返回在这个集合中小于或者等于给定元素的最大元素,如果不存在这样的元素,返回 null
  • tset.ceiling(E e) 方法返回在这个集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回 null
  • tset.last()
  • tset.lower()
  • tmap.floorEntry(E e)
  • tmap.higherEntry(E e)
  • tmap.firstEntry()
  • tmap.subMap(E fromKey, E toKey): 返回一个视图,其键范围从 fromKey(包括)到 toKey(不包括)。注意,子 Map 中的更改会反映在原始 Map 中。

相关题目: 🟨 729. 我的日程安排表 I - 力扣(LeetCode)

优先队列 PriorityQueue

优先队列使用了堆数据结构,堆的形式为数组。

因为 PriorityQueue 中插入删除的数据是泛型类, 可以是基本类型对应的包装类, 也可以是自定义类, 在向上调整和向下调整的过程中,需要不断地比较,如果数据没有指定比较规则,或者为 null 时,程序就会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Queue<Integer> priorityQueue1 = new PriorityQueue<>(); // 默认数组容量 11
Queue<Integer> priorityQueue2 = new PriorityQueue<>(11); // 指定数组容量
// 设置堆插入时向上调整的比较规则
Queue<Integer> priorityQueue = new PriorityQueue<>(
new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
);
// 使用 Lambda 简写
Queue<Integer> priorityQueue = new PriorityQueue<>(
(o1,o2)->o2-o1
);

Java 集合中的 PriorityQueue 底层的堆默认是小根堆,如果要设置大根堆,就必须传入比较器, 并重写 compare 方法。

调试(标准化输出)

1
2
System.out.println(object); // 标准输出
// 原理:String.valueOf(object) -> object.toString()

ObjecttoString() 默认实现并不直观。

对数组进行「友好」的字符串转换:

1
Arrays.toString(arr);// 将数组转换为可视字符串

ArrayListHashSet 继承了 AbstractCollection,里面重写了 toString 。我们可以这样直观查看 ArrayList 中的内容:

1
2
3
4
5
System.out.println(c);
// [1,2,3,4,5]

// 或者炫技:
c.forEach(System.out::println);

AbstractCollectiontoString 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";

StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
// sb.append 调用了 String.valueOf()
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}

多线程编程

本章节主要就是背单词!

Semaphore 信号量

1
2
3
Semaphore s = new Semaphore(0);
s.acquire(); // s.acquire(2);
s.release(); // s.release(2);

语言无关的通用技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 循环增减
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种字符的情况。
arr1[str.charAt(i)-'a']++; // a 放到数组下标 0,z放到数组下标 25
arr2[str.charAt(i)-'0']++;
arr3[str.charAt(i)-'A']++;

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

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 的值来代替。

模板写法

单调栈/队列

原型题: 🟩 496. 下一个更大元素 I - 力扣(LeetCode)

1
2
3
4
5
6
// 假设数组A从左到右遍历
insert x // x 为数组中某一个元素
// 假设维护的是一个单调递增的栈(自栈顶向下单调递增)
while !stack.empty() && stack.top()<x
y = stack.pop() // 这也意味着y右侧第一个比y大的元素是x
stack.push(x)

有时候,单调栈或单调队列中不一定存的是实际数字的值,有可能存的是指向实际数字的指针或下标。比如这一题:🟥 239. 滑动窗口最大值 - 力扣(LeetCode)

滑动窗口

样例题:🟨 3. 无重复字符的最长子串 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int lengthOfLongestSubstring(String s) {
Set<Character> set = new HashSet<>(); // 滑动窗口中的内容

int res = 0;
int len = s.length();
int l = 0; // 左指针
for(int i=0;i<len;i++){ // 右指针
while(set.contains(s.charAt(i))){
set.remove(s.charAt(l)); // 弹出滑动窗口内容
l++;
}
set.add(s.charAt(i));
res= Math.max(set.size(),res); // 更新答案
}
return res;
}

方向移动

如果需要物体在规定的棋盘下(比如 n*n)进行方向移动,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义方向
static int[][] directions = new int[][]{
{-1,0},{1,0},{0,-1},{0,1} // 上下左右
};

// 遍历方向
// 传入当前位置i,j,以及棋盘
public void recursion(int i,int j, int[] board){
/* 省略此处递归边界判断 */

for(int k=0;k<directions.length;k++){
int newI = i + directions[k][0];
int newJ = j + directions[k][1];
if(newI>=0&&newI<n&&newJ>=0&&newJ<=n){ // 边界判断
recursion(newI,newJ,board); // 走下一步
}
}
}

相关题目:688. 骑士在棋盘上的概率 - 力扣(LeetCode)

本文参考