算法笔记

一些好的逻辑思路

反问题

如果一个问题正问题很复杂,可以尝试想一下反问题,
比如求有恰好k个相邻数的数组个数,正问题很复杂,但是反问题只需要找到n-k-1个分割线,把数组分割成n-k块.
3405. 统计恰好有 K 个相等相邻元素的数组数目

子问题

如果一个大的问题可以分解成多个子问题,可以尝试先解决子问题,再解决大问题.这就可以用递归或动态规划的方法来解决.

邻域消除问题

对于临项消除问题,并且先后顺序无关,可以考虑使用栈来实现.
从左到右来执行.

ACM 格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.*;
import java.io.*;

public class Main{
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
PrintWriter pw = new PrintWriter(System.out);

int n = Integer.parseInt(br.readLine());
String[] times = br.readLine().split(" ");

long prev = parseTime(times[0]);
for (int i = 1; i < n; i++) {
long current = parseTime(times[i]);
long diff = current - prev;
if (diff < 0) {
diff += 24 * 60 * 60;
}
double circles = diff / 60.0;
pw.printf("%.2f\n", circles);
prev = current;
}
pw.flush();
}

private static long parseTime(String time) {
String[] parts = time.split(":");
int hour = Integer.parseInt(parts[0]);
int minute = Integer.parseInt(parts[1]);
int second = Integer.parseInt(parts[2]);
return hour * 3600L + minute * 60L + second;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.*;
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;

while ((line = br.readLine()) != null) { // 逐行读取
int n = Integer.parseInt(line.trim());
int[] nums = new int[n];

String[] parts = br.readLine().split(" ");
for (int i = 0; i < n; i++) {
nums[i] = Integer.parseInt(parts[i]);
}
// 处理逻辑
// 输出结果
System.out.println(result);
}
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
System.out.println(replaceNumber(s));
scanner.close();
}

第二种:

1
2
3
4
5
6
7
8
9
import java.util.*;
import java.io.*;

public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
System.out.println(replaceNumber(s));
scanner.close();
}

输入格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Main {
public static void main(String[] args) {
// 示例1: 数组输入
int[] nums = {1, 3, 5, 2, 4};
int target = 5;

// 示例2: 矩阵/二维数组
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

// 示例3: 链表(需要先定义ListNode类)
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);

// 示例4: 字符串
String s = "hello world";
String pattern = "hello";

// 调用你的算法函数
int result = twoSum(nums, target);
System.out.println("结果: " + result);
}

// 你的算法函数
public static int twoSum(int[] nums, int target) {
// 算法实现...
return 0;
}
}
//字符串数组
String[] words1 = {"hello", "world", "java", "algorithm"};

// 链表节点定义
class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}

杂项操作

绝对值:
Math.abs();
Int最大值:
Integer.MAX_VALUE
前导零:
Integer.numberOfLeadingZeros(k);

取随机值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class RandomDemo {
public static void main(String[] args) {
int n = 10;
//0~9
// 方法1: Math.random()
int random1 = (int) (Math.random() * n);
System.out.println("Math.random(): " + random1);

// 方法2: Random类
Random rand = new Random();
int random2 = rand.nextInt(n);
System.out.println("Random: " + random2);

// 方法3: ThreadLocalRandom(多线程安全)
int random3 = ThreadLocalRandom.current().nextInt(n);
System.out.println("ThreadLocalRandom: " + random3);
}
}

Array 常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ========== 数组声明与初始化 ==========
int[] arr = new int[5]; // 创建空数组 [0,0,0,0,0]
int[] nums = {3,1,4,5,2}; // 直接初始化
String[] strArr = {"A", "B", "C"}; // 字符串数组
int[][] matrix = {{1,2}, {3,4,5}}; // 二维数组
// ========== 基本操作 ==========
// 访问元素
int first = nums[0];
nums[2] = 9;
// 获取长度
int len = nums.length; // 一维数组长度 → 5
int cols = matrix[1].length; // 二维数组第二维长度 → 3

// ========== 遍历操作 ==========
// 普通for循环
for(int i=0; i<nums.length; i++) {
System.out.print(nums[i] + " "); // 3 1 9 5 2
}
// 增强for循环
for(int num : nums) {
System.out.print(num + " ");
}
// 二维数组遍历
for(int[] row : matrix) {
for(int n : row) {
System.out.print(n + " "); // 1 2 / 3 4 5
}
}

数组工具操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ========== 数组工具操作 ==========
// ========== 排序操作 ==========
//升序排序
Arrays.sort(array);
//降序排序
Arrays.sort(Collections.reverseOrder());
//自定义排序
Arrays.sort(array, (a, b) -> a - b); //升序
Arrays.sort(array, (a, b) -> b - a); //降序
// 复制
int[] copy1 = Arrays.copyOf(nums, 3); // 复制前3元素 → [1,2,3]
int[] copy2 = Arrays.copyOfRange(nums, 1, 4); // 复制1-3索引 → [2,3,5]
// 比较
boolean isEqual = Arrays.equals(nums, copy1); // false
// 填充
Arrays.fill(nums, 0); // 全部填充0 → [0,0,0,0,0]
Arrays.fill(nums, 1, 3, 5); // 索引1-2填充5 → [0,5,5,0,0]
// ========== 类型转换 ==========
// 数组转List(固定大小)
List<Integer> list = Arrays.asList(1,2,3);
// 数组转Stream
IntStream stream = Arrays.stream(nums);
// ========== 其他操作 ==========
// 二分查找(需先排序)
int index = Arrays.binarySearch(nums, 5); // 返回元素索引
// 多维数组操作
int[][] matrixCopy = Arrays.copyOf(matrix, matrix.length); // 浅拷贝
String deepStr = Arrays.deepToString(matrix); // "[[1, 2], [3, 4, 5]]"
// 反转数组
for(int i=0; i<nums.length/2; i++) {
int temp = nums[i];
nums[i] = nums[nums.length-1-i];
nums[nums.length-1-i] = temp;
}

数组带着索引排序
例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public int[] maxSubsequence(int[] nums, int k) {
int n=nums.length;
int ans[]=new int[k];
Integer idx[]=new Integer[n];
Arrays.setAll(idx, i->i);
Arrays.sort(idx,(a,b)->nums[a]-nums[b]);
int j=0;
for(int i=n-k;i<n;i++) {
ans[j++]=idx[i];
}
Arrays.sort(ans);
for(int i=0;i<k;i++) {
ans[i]=nums[ans[i]];
}
return ans;
}

和 int 数组相互转换:

1
2
3
4
5
6
7
Integer[] arr = {1,2,3};
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(arr));
ArrayList<Integer> list = new ArrayList<>();
Integer[] arr = list.toArray(new Integer[0]);
//但是如何转成int[]数组呢
//方法1
arr = list.stream().mapToInt(Integer::valueOf).toArray();

List 常用操作

初始化

1
2
3
4
// ====================== 1. 初始化 List ======================
List<String> arrayList = new ArrayList<>(); // 可修改列表
List<Integer> linkedList = new LinkedList<>();
List<String> immutableList = List.of("A", "B", "C"); // Java9+ 不可变列表

增删改查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ====================== 2. 添加元素 ======================
arrayList.add("Apple"); // 末尾添加
arrayList.add(0, "Banana"); // 索引0插入
arrayList.addAll(List.of("Orange", "Grape")); // 批量添加

// ====================== 3. 访问元素 ======================
String firstElement = arrayList.get(0); // "Banana"
boolean containsApple = arrayList.contains("Apple"); // true
int indexOfOrange = arrayList.indexOf("Orange"); // 2

// ====================== 4. 删除元素 ======================
arrayList.remove("Banana"); // 按值删除
arrayList.remove(0); // 按索引删除
arrayList.clear(); // 清空列表

// ====================== 5. 修改元素 ======================
arrayList.add("Mango");
arrayList.set(0, "Pineapple"); // 修改索引0的元素

// ====================== 6. 遍历 List ======================
Collections.sort(path); // 直接修改原List
Collections.sort(path, Collections.reverseOrder()); // 降序排序
// 方式1: 普通for循环
for (int i = 0; i < arrayList.size(); i++) {
System.out.println(arrayList.get(i));
}

// 方式2: 增强for循环
for (String fruit : arrayList) {
System.out.println(fruit);
}

// 方式3: 迭代器
Iterator<String> it = arrayList.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}

// 方式4: forEach + Lambda
arrayList.forEach(System.out::println);

其他操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// ====================== 7. 其他操作 ======================
int size = arrayList.size(); // 列表长度
boolean isEmpty = arrayList.isEmpty(); // 是否为空
List<String> subList = arrayList.subList(0, 2); // 截取子列表
Object[] array = arrayList.toArray(); // 转为数组

// ====================== 8. 注意事项 ======================
/*
1. ArrayList初始容量10,扩容1.5倍
2. LinkedList用节点链接实现
3. 线程不安全,多线程环境使用:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
4. 快速失败机制(fast-fail):遍历时修改会抛出ConcurrentModificationException
*/

二维List例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TwoDListExample {
public static void main(String[] args) {
// ======================
// 1. 创建二维List
// ======================
List<Integer>[] ls = new ArrayList[26];
Arrays.setAll(ls,i->new ArrayList<>());
// 方法1: 使用Arrays.asList()初始化
List<List<Integer>> matrix1 = new ArrayList<>();
matrix1.add(Arrays.asList(1, 2, 3));
matrix1.add(Arrays.asList(4, 5, 6));
matrix1.add(Arrays.asList(7, 8, 9));

// 方法2: 动态创建空二维List
List<List<String>> matrix2 = new ArrayList<>();

// 方法3: 使用嵌套循环初始化
List<List<Character>> matrix3 = new ArrayList<>();
for (int i = 0; i < 3; i++) {
List<Character> row = new ArrayList<>();
for (int j = 0; j < 4; j++) {
row.add((char) ('A' + i + j));
}
matrix3.add(row);
}
// ======================
// 2. 添加元素
// ======================
// 添加新行
matrix2.add(new ArrayList<>(Arrays.asList("Java", "Python")));
matrix2.add(new ArrayList<>(Arrays.asList("C++", "JavaScript")));

// 在指定行添加元素
matrix2.get(0).add("Ruby"); // 第一行添加元素
matrix2.get(1).add(0, "Go"); // 第二行开头插入元素

// 添加新行(空行)
matrix2.add(new ArrayList<>());
matrix2.get(2).add("Swift"); // 给新行添加元素

// ======================
// 3. 访问元素
// ======================

// 访问单个元素
int element = matrix1.get(1).get(2); // 获取第二行第三列元素 → 6
String lang = matrix2.get(0).get(1); // 获取第一行第二列元素 → "Python"

// 获取行数
int rows = matrix1.size();

// 获取列数(特定行)
int colsRow0 = matrix1.get(0).size();
int colsRow2 = matrix2.get(2).size();

// ======================
// 4. 修改元素
// ======================

matrix1.get(0).set(0, 100); // 修改第一行第一列: 1 → 100
matrix2.get(1).set(2, "TypeScript"); // 修改第二行第三列

// ======================
// 5. 删除元素
// ======================

// 删除指定位置的元素
matrix1.get(2).remove(1); // 删除第三行第二列元素(8)

// 删除整行
matrix2.remove(2); // 删除第三行

// ======================
// 6. 遍历二维List
// ======================

System.out.println("\n遍历matrix1:");
// 方法1: 索引遍历
for (int i = 0; i < matrix1.size(); i++) {
for (int j = 0; j < matrix1.get(i).size(); j++) {
System.out.print(matrix1.get(i).get(j) + " ");
}
System.out.println();
}

System.out.println("\n遍历matrix2:");
// 方法2: 增强for循环
for (List<String> row : matrix2) {
for (String item : row) {
System.out.print(item + " ");
}
System.out.println();
}

System.out.println("\n遍历matrix3:");
// 方法3: 使用forEach + lambda
matrix3.forEach(row -> {
row.forEach(item -> System.out.print(item + " "));
System.out.println();
});

// ======================
// 7. 其他常用操作
// ======================

// 检查是否为空
boolean isEmpty = matrix2.isEmpty();

// 检查是否包含元素
boolean containsPython = matrix2.get(0).contains("Python");

// 查找元素位置
int rowIndex = -1, colIndex = -1;
for (int i = 0; i < matrix1.size(); i++) {
int index = matrix1.get(i).indexOf(5);
if (index != -1) {
rowIndex = i;
colIndex = index;
break;
}
}

// 转换为二维数组
String[][] array2D = new String[matrix2.size()][];
for (int i = 0; i < matrix2.size(); i++) {
List<String> row = matrix2.get(i);
array2D[i] = row.toArray(new String[0]);
}

////翻转行
//方法1 使用工具类
List<List<Integer>> matrix = new ArrayList<>();
// 假设 matrix 已经填充数据
// 翻转行(首行变末行,次行变次末行,依此类推)
Collections.reverse(matrix);

//方法2
List<List<Integer>> matrix = new ArrayList<>();
// 假设 matrix 已经填充数据
int left = 0;
int right = matrix.size() - 1;
while (left < right) {
// 使用临时行 temp 交换左右两行
List<Integer> temp = matrix.get(left);
matrix.set(left, matrix.get(right));
matrix.set(right, temp);
left++;
right--;
}
//方法3 Stream
List<List<Integer>> matrix = new ArrayList<>();
// 假设 matrix 已经填充数据
// 翻转行顺序
List<List<Integer>> reversed = IntStream.range(0, matrix.size())
.mapToObj(i -> matrix.get(matrix.size() - 1 - i))
.collect(Collectors.toList());

matrix = reversed; // 如果需要原地翻转,需重新赋值

// 打印结果
System.out.println("\nmatrix1: " + matrix1);
System.out.println("matrix2: " + matrix2);
System.out.println("matrix3: " + matrix3);
System.out.println("找到数字5的位置: [" + rowIndex + "][" + colIndex + "]");
}
}

HashSet 常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// =================== HashSet 基础操作 ===================
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

// 创建对象
Set<String> set = new HashSet<>(); // 空集合(默认初始容量16,负载因子0.75)
Set<Integer> initSet = new HashSet<>(32); // 指定初始容量
Set<String> prefilled = new HashSet<>(Arrays.asList("A", "B", "C")); // 通过集合初始化

// 元素操作
boolean added = set.add("Apple"); // 添加元素 → true(首次添加)
boolean dupAdd = set.add("Apple"); // 添加重复元素 → false
boolean hasBanana = set.contains("Apple"); // 检查存在 → true
boolean removed = set.remove("Apple"); // 删除元素 → true(存在时)
set.clear(); // 清空集合

// 批量操作
Set<String> fruits = new HashSet<>(Arrays.asList("Orange", "Mango"));
boolean addedAll = set.addAll(fruits); // 合并集合 → true(集合改变时)
boolean retainAll = set.retainAll(Arrays.asList("Mango")); // 保留交集 → true(集合改变时)
boolean removeAll = set.removeAll(fruits); // 删除所有匹配元素 → true(集合改变时)

// 遍历操作
set.add("Apple");
set.add("Banana");
for (String item : set) { // 增强for循环(无序)
System.out.print(item + " "); // 输出顺序不确定(如 "Banana Apple")
}

Iterator<String> it = set.iterator(); // 迭代器遍历
while (it.hasNext()) {
System.out.print(it.next() + " ");
}

set.forEach(item -> System.out.print(item)); // Java8+ Lambda遍历

// 集合信息
int size = set.size(); // 元素数量(如 2)
boolean isEmpty = set.isEmpty(); // 是否空集合 → false
Object[] array = set.toArray(); // 转Object数组
String[] strArray = set.toArray(new String[0]); // 转指定类型数组

// 特殊操作
Set<String> cloneSet = (HashSet<String>) ((HashSet<String>) set).clone(); // 浅拷贝
Set<String> syncSet = Collections.synchronizedSet(set); // 线程安全包装

// 集合运算示例
Set<String> set1 = new HashSet<>(Arrays.asList("A", "B"));
Set<String> set2 = new HashSet<>(Arrays.asList("B", "C"));

Set<String> union = new HashSet<>(set1); // 并集 → [A, B, C]
union.addAll(set2);

Set<String> intersection = new HashSet<>(set1); // 交集 → [B]
intersection.retainAll(set2);

Set<String> difference = new HashSet<>(set1); // 差集 → [A]
difference.removeAll(set2);

/* 核心特性:
1. 唯一性:基于 hashCode() 和 equals() 判断重复
2. 无序性:遍历顺序不保证与插入顺序一致
3. 允许 null 元素(但只能有一个 null)
4. 基础操作时间复杂度:add/remove/contains → 平均 O(1)
5. 非线程安全:需通过 Collections.synchronizedSet 包装实现线程安全
*/

HashMap 常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// =================== HashMap 基础操作 ===================
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

// 创建对象
Map<String, Integer> map = new HashMap<>(); // 默认容量16,负载因子0.75
Map<String, String> initMap = new HashMap<>(32); // 指定初始容量
Map<String, Integer> prefilled = new HashMap<>(Map.of("A", 1, "B", 2)); // Java9+快速初始化

// 增删改操作
map.put("Apple", 10); // 添加键值对 → {Apple=10}
map.put("Banana", 20); // → {Apple=10, Banana=20}
map.putIfAbsent("Apple", 50); // 仅当键不存在时添加 → 原值10保持不变
map.replace("Apple", 15); // 替换已有键的值 → {Apple=15, Banana=20}
map.remove("Banana"); // 删除键 → {Apple=15}
map.replace("Apple", 15, 20); // 键值匹配时替换 → {Apple=20}
map.clear(); // 清空映射

// 查询操作
int count = map.get("Apple"); // 获取值(需确保键存在)
Integer countSafe = map.get("Orange"); // 键不存在时返回null
boolean existsKey = map.containsKey("Apple"); // 检查键是否存在 → true
boolean existsValue = map.containsValue(20); // 检查值是否存在 → true
int size = map.size(); // 键值对数量
boolean isEmpty = map.isEmpty(); // 是否为空映射

// 遍历操作(4种方式)
// 1. Entry遍历(推荐)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}

// 2. Key遍历
for (String key : map.keySet()) {
System.out.println(key + " -> " + map.get(key));
}

// 3. Value遍历
for (Integer value : map.values()) {
System.out.println("Value: " + value);
}

// 4. Java8+ Lambda遍历
map.forEach((k, v) -> System.out.println(k + "=" + v));

// 批量操作
Map<String, Integer> newItems = Map.of("Cherry", 5, "Durian", 8);
map.putAll(newItems); // 合并映射 → {Apple=20, Cherry=5, Durian=8}

// 特殊值处理
map.put(null, 0); // 允许null键 → {null=0}
map.put("Mango", null); // 允许null值 → {null=0, Mango=null}

// 高级操作(Java8+)
map.computeIfAbsent("Orange", k -> 3); // 不存在时计算 → 添加 Orange=3
map.computeIfPresent("Apple", (k, v) -> v + 5); // 存在时更新 → Apple=25
map.merge("Apple", 10, (oldVal, newVal) -> oldVal + newVal); // 合并值 → Apple=35

// 线程安全包装
Map<String, Integer> syncMap = Collections.synchronizedMap(map);

// 不可变映射(Java9+)
Map<String, Integer> immutableMap = Map.ofEntries(
Map.entry("A", 1),
Map.entry("B", 2)
);

/* 核心特性:
1. 键唯一性:基于 hashCode() 和 equals() 判断重复
2. 无序存储:迭代顺序不保证与插入顺序一致
3. 允许一个null键和多个null值
4. 基础操作时间复杂度:get/put → 平均 O(1)
5. 扩容机制:当元素数量超过(容量*负载因子)时自动翻倍扩容
6. 树化优化:当链表长度超过8时转红黑树(Java8+)
*/
1
2
3
4
5
6
7
HashMap<Character, Integer> hm_s = new HashMap<>();
//遍历字符串的所有字符(更清晰的逻辑)
for (char c : s.toCharArray()) {
hm_s.merge(c, 1, Integer::sum);
}
// 使用getOrDefault避免NullPointerException
int countS = hm_s.getOrDefault(c, 0);

一个 Key 只能对应一个值,所以如果要实现类似 Multimap 的结构需要如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//初始化
HashMap<String, List<Integer>> multiValueMap = new HashMap<>();

/////添加元素
if (!multiValueMap.containsKey("scores")) {
multiValueMap.put("scores", new ArrayList<>());
}
multiValueMap.get("scores").add(90);
// 若键不存在,自动创建空列表
multiValueMap.computeIfAbsent("scores", k -> new ArrayList<>()).add(90);
multiValueMap.computeIfAbsent("scores", k -> new ArrayList<>()).add(85);

////访问元素
//获取某个键的所有值
List<Integer> scores = multiValueMap.get("scores");
if (scores != null) {
for (int num : scores) {
System.out.println(num); //输出 90, 85
}
}
//避免 NullPointerException的情况
List<Integer> scores = multiValueMap.getOrDefault("scores", new ArrayList<>());
for (int num : scores) {
System.out.println(num);
}

////删除元素
//删除整个键值对
multiValueMap.remove("scores");
// 删除某个键的特定值
List<Integer> scores = multiValueMap.get("scores");
if (scores != null) {
scores.remove(Integer.valueOf(90)); // 删除值为 90 的元素
// 如果列表为空,可选删除键
if (scores.isEmpty()) {
multiValueMap.remove("scores");
}
}

////遍历哈希表
// 遍历所有键值对
for (Map.Entry<String, List<Integer>> entry : multiValueMap.entrySet()) {
String key = entry.getKey();
List<Integer> values = entry.getValue();
System.out.println(key + ": " + values);
}
//遍历所有键
for (String key : multiValueMap.keySet()) {
System.out.println("Key: " + key);
}
//遍历所有值
for (List<Integer> values : multiValueMap.values()) {
System.out.println("Values: " + values);
}

String的常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// =================== String 常用操作 ===================
// 创建对象
String str1 = "Hello"; // 直接量创建(字符串常量池)
String str2 = new String("World"); // 堆内存新对象
char[] chars = {'J','a','v','a'};
String str3 = new String(chars); // 通过字符数组创建 → "Java"

// 基本操作
int len = str1.length(); // 获取长度 → 5
char c = str1.charAt(1); // 获取索引1字符 → 'e'
String substr1 = str1.substring(2); // 从索引2截取 → "llo"
String substr2 = str1.substring(1,4); // 截取1-3索引 → "ell"
String concatStr = str1.concat(" World");// 拼接 → "Hello World"
String upper = str1.toUpperCase(); // 转大写 → "HELLO"
String lower = "HELLO".toLowerCase(); // 转小写 → "hello"
String trimStr = " text ".trim(); // 去首尾空格 → "text"
String replaced = str1.replace('l', 'L');// 替换字符 → "HeLLo"
String replacedAll = "a1b2c3".replaceAll("\\d", "#"); // 正则替换 → "a#b#c#"

// 转换与比较
char[] arr = str1.toCharArray(); // 转字符数组 → ['H','e','l','l','o']
byte[] bytes = str1.getBytes(); // 按默认编码转字节数组
boolean eq1 = str1.equals("Hello"); // 值比较 → true
boolean eq2 = str1.equalsIgnoreCase("hElLo"); // 忽略大小写 → true
int cmp = str1.compareTo("Hella"); // 字典序比较 → 正数('o' > 'a')

// 正则处理
boolean matches = "123".matches("\\d+"); // 正则匹配 → true
String[] parts = "a,b,c".split(","); // 分割
//Arrays.adList()
// 字符串 → ["a","b","c"]
String[] regexParts = "a1b2c3".split("\\d"); // 按数字分割 → ["a","b","c"]

// 格式化处理
String format1 = String.format("%s-%d", "ID", 100); // → "ID-100"
String format2 = String.join("|", "A", "B", "C"); // → "A|B|C"

// 特殊判断
boolean isEmpty = "".isEmpty(); // 空字符串 → true
boolean isBlank = " ".isBlank(); // 全空白字符(Java 11+) → true
boolean starts = str1.startsWith("He"); // 开头判断 → true
boolean ends = str1.endsWith("lo"); // 结尾判断 → true

/* 核心特性:
1. 不可变性:所有操作返回新字符串,原对象不变
2. 字符串常量池复用机制:直接量赋值优先使用常量池
3. 支持正则表达式操作(split/matches/replaceAll等)
4. 包含丰富的格式化方法(format/join等)
*/

int转String
String s = String.valueOf(i);
String s = Integer.toString(i);
String 转 int
String str = "123";
int num = Integer.parseInt(str);

StringBuffer 和 StringBuilder

StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// =================== StringBuilder ===================
// 创建对象
StringBuilder sb = new StringBuilder(); // 默认容量16
StringBuilder sb2 = new StringBuilder("Hello"); // 指定初始内容

// 基本操作
sb.append(" World"); // 追加内容 → "Hello World"
sb.insert(5, " Java"); // 指定位置插入 → "Hello Java World"
sb.delete(5, 10); // 删除5-9位置 → "Hello World"
sb.replace(6, 11, "Earth"); // 替换指定区间 → "Hello Earth"
sb.reverse(); // 反转 → "htraE olleH"
sb.setLength(5); // 截断保留前5字符 → "htraE"
char c = sb.charAt(2); // 获取索引2的字符 → 'r'
sb.setCharAt(0, 'H'); // 修改索引0字符 → "HtraE"
int len = sb.length(); // 获取当前长度 → 5
String s = sb.toString(); // 转换为String
res = new StringBuilder(p.s).repeat(res,p.k); //重复res k次 重复操作

// 正向遍历
for (int i = 0; i < sb.length(); i++) {
char c = sb.charAt(i);
System.out.println(c);
}
// 链式调用
StringBuilder sb3 = new StringBuilder()
.append(123) // 支持多类型
.append(3.14)
.append(true);

sb.setLength(prevLen); // 直接回退到添加前的长度

// =================== StringBuffer ===================
// 创建对象(操作方法与StringBuilder完全一致)
StringBuffer sbf = new StringBuffer();
sbf.append(100); // 追加数值
sbf.insert(3, new char[]{'A','B'}); // 插入字符数组
sbf.deleteCharAt(4); // 删除单个字符
sbf.setLength(0); // 清空缓冲区(复用对象)

// 线程安全示例
sbf.append("ThreadSafe"); // 所有方法都有synchronized修饰符

/* 共性特征:
1. 初始容量16,自动扩容(每次扩容2n+2)
2. append() 支持所有基础类型/Object类型
3. 修改后对象地址不变(与String的不可变性对比)
4. 主要方法:append/insert/delete/replace/reverse
*/
String s1 = buffer.toString(); // StringBuffer → String
String s2 = builder.toString(); // StringBuilder → String

PriorityQueue的常用操作

record的常用操作

相当于定义一个不可变的数据类.
由于record是Java 14引入的,所以需要使用Java 14以上的版本。
并且通常record是用在优先队列的比较器中,所以直接放在这里.
直接举一个例子:

1
2
3
4
5
6
7
8
record Node(double gain , int a , int b){}
PriorityQueue<Node> pq = new PriorityQueue<>((a, b) -> Double.compare(b.gain, a.gain));
pq.offer(new Node(1.0, 1, 2));
pq.offer(new Node(2.0, 2, 3));
pq.offer(new Node(3.0, 3, 4));
System.out.println(pq.poll().gain);
System.out.println(pq.poll().gain);
System.out.println(pq.poll().gain);

输出:

1
2
3
3.0
2.0
1.0

基本用法就是这样.

小顶堆(默认,前 K 大,升序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建小顶堆(默认)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// 添加元素
minHeap.offer(5);
minHeap.add(3); // offer和add功能相同
// 查看堆顶元素(不删除)
int min = minHeap.peek();
// 取出堆顶元素(删除)
int removedMin = minHeap.poll();
// 获取堆大小
int size = minHeap.size();
// 检查是否为空
boolean isEmpty = minHeap.isEmpty();
// 删除指定元素(非堆顶)
minHeap.remove(2);
// 清空堆
minHeap.clear();

大顶堆(前 K 小,降序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建大顶堆(使用自定义比较器)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
// 或 PriorityQueue<>(Comparator.reverseOrder());
// 添加元素
maxHeap.offer(8);
maxHeap.add(4);
// 查看堆顶元素(不删除)
int max = maxHeap.peek();
// 取出堆顶元素(删除)
int removedMax = maxHeap.poll();
// 检查元素是否存在
boolean contains = maxHeap.contains(5);
// 遍历并输出元素
for (Integer num : minHeap) {
System.out.println(num);
}
// 构造按照字典序排列的优先队列
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> {
// 将 int 转换为 String 后比较字典序
String strA = String.valueOf(a);
String strB = String.valueOf(b);
return strA.compareTo(strB);
});

堆求前k大,然后存储索引(需要保存下标的写法)
堆求前k大,然后存储索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//小根堆,如果当前元素大于堆顶,则加入堆中,堆中的元素还要保存其下标
public int[] maxSubsequence(int[] nums, int k) {
PriorityQueue<int[]> heap = new PriorityQueue<>((o1,o2)->o1[0]-o2[0]);
int n = nums.length;
for(int i = 0; i < n; i++) {
if(heap.size() < k) heap.offer(new int[]{nums[i], i});
else if(nums[i] > heap.peek()[0]) {
heap.poll();
heap.offer(new int[]{nums[i], i});
}
}
int[] idx = new int[k];
for(int i = 0; i < k; i++) {
idx[i] = heap.poll()[1];
}
Arrays.sort(idx);
for(int i = 0; i < k; i++) {
idx[i] = nums[idx[i]];
}
return idx;
}

自定义对象堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自定义类
class Student {
String name;
int score;
// 构造方法等...
}
// 按分数的小顶堆
PriorityQueue<Student> studentMinHeap = new PriorityQueue<>(
(s1, s2) -> s1.score - s2.score
);
// 按分数的大顶堆
PriorityQueue<Student> studentMaxHeap = new PriorityQueue<>(
(s1, s2) -> s2.score - s1.score
);
// 添加自定义对象
studentMinHeap.offer(new Student("Alice", 85));

堆序性质
堆类型 比较条件 数学表达 Java比较器实现
小顶堆 父 ≤ 子 a ≤ b a - b 或 a.compareTo(b)
大顶堆 父 ≥ 子 a ≥ b b - a 或 b.compareTo(a)
比较器的本质
a在堆里代表父节点 b是子节点 a-b 要小于0 a优先级才高 a<b 所以是最小堆

比较器定义的是”优先级”关系:

返回负数:第一个参数(a)应该排在前面(更高优先级)

返回正数:第二个参数(b)应该排在前面

返回零:两者优先级相同

使用场景
前 K 大的元素:使用最小堆,因为堆顶是存储的最小的元素,如果新增元素比堆顶大,那只需要替换掉堆顶即可。
前 K 小的元素:使用最大堆,因为堆顶是存储的最大的元素,如果新增元素比堆顶小,那只需要替换掉堆顶即可。
默认小顶堆:升序排序。
默认大顶堆:降序排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public int maxEvents(int[][] events) {
int mx = 0;
for (int[] e : events) {
mx = Math.max(mx, e[1]);
}
// 按照开始时间分组
List<Integer>[] groups = new ArrayList[mx + 1];
Arrays.setAll(groups, i -> new ArrayList<>());
for (int[] e : events) {
groups[e[0]].add(e[1]);
}
int ans = 0;
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int i = 0; i <= mx; i++) {
// 删除过期会议
while (!pq.isEmpty() && pq.peek() < i) {
pq.poll();
}
// 新增可以参加的会议
for (int endDay : groups[i]) {
pq.offer(endDay);
}
// 参加一个结束时间最早的会议
if (!pq.isEmpty()) {
ans++;
pq.poll();
}
}
return ans;
}
}

Deque 的常用操作

Deque常用于实现队列和栈.包括维护单调栈.

创建

1
2
3
4
5
6
7
import java.util.Deque;
import java.util.ArrayDeque;
import java.util.Iterator;

// 初始化 Deque(以 ArrayDeque 为例)
Deque<String> deque = new ArrayDeque<>(); //不能插入null元素
Deque<String> deque = new LinkedList<>();

插入

1
2
3
4
5
6
7
8
// 队头插入
deque.addFirst("A"); // 插入元素到队头(容量满时抛出 IllegalStateException)
deque.offerFirst("B"); // 插入元素到队头(容量满时返回 false)
// 队尾插入
deque.addLast("C"); // 插入元素到队尾(容量满时抛出 IllegalStateException)
deque.offerLast("D"); // 插入元素到队尾(容量满时返回 false)
// 批量插入(从 Collection 继承)
deque.addAll(List.of("E", "F")); // 依次插入队尾

删除

1
2
3
4
5
6
7
8
9
10
// 队头删除
String first1 = deque.removeFirst(); // 删除并返回队头元素(空队列抛 NoSuchElementException)
String first2 = deque.pollFirst(); // 删除并返回队头元素(空队列返回 null)
// 队尾删除
String last1 = deque.removeLast(); // 删除并返回队尾元素(空队列抛异常)
String last2 = deque.pollLast(); // 删除并返回队尾元素(空队列返回 null)
// 删除指定元素(从队头开始搜索)
boolean removed1 = deque.remove("E"); // 删除第一个出现的 "E"
boolean removed2 = deque.removeFirstOccurrence("F"); // 删除队头方向第一个 "F"
boolean removed3 = deque.removeLastOccurrence("G"); // 删除队尾方向第一个 "G"

查看

1
2
3
4
5
6
7
// 查看队头
String head1 = deque.getFirst(); // 返回队头元素(空队列抛异常)
String head2 = deque.peekFirst(); // 返回队头元素(空队列返回 null)

// 查看队尾
String tail1 = deque.getLast(); // 返回队尾元素(空队列抛异常)
String tail2 = deque.peekLast(); // 返回队尾元素(空队列返回 null)

状态

1
2
3
boolean isEmpty = deque.isEmpty();  // 判断队列是否为空
int size = deque.size(); // 返回元素数量
boolean exists = deque.contains("A"); // 判断是否包含元素 "A"

遍历

1
2
3
4
5
6
7
8
9
10
11
// 迭代器(正向:队头 → 队尾)
Iterator<String> iterator = deque.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
}

// 反向迭代器(队尾 → 队头)
Iterator<String> descendingIterator = deque.descendingIterator();
while (descendingIterator.hasNext()) {
String element = descendingIterator.next();
}

其他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 清空队列
deque.clear();
// 转换为数组
Object[] array1 = deque.toArray(); // 返回 Object[]
String[] array2 = deque.toArray(new String[0]); // 指定类型数组

// ================== 栈操作(Deque 兼容的额外方法)==================
deque.push("X"); // 等效于 addFirst()
String popped = deque.pop(); // 等效于 removeFirst()


// ================== 容量限制队列(如 LinkedBlockingDeque)==================
// 阻塞操作示例(需使用线程安全 Deque,此处仅展示方法)
/*
deque.offerFirst("W", 1, TimeUnit.SECONDS); // 等待1秒尝试插入队头
deque.offerLast("Z", 1, TimeUnit.SECONDS); // 等待1秒尝试插入队尾
String item = deque.pollFirst(1, TimeUnit.SECONDS); // 等待1秒尝试取出队头
*/

TreeMap 的常用操作

创建

1
2
3
4
5
6
// 默认按key升序排列
TreeMap<Integer, String> map = new TreeMap<>();
// 按key降序排列
TreeMap<String, Integer> map = new TreeMap<>(Collections.reverseOrder());
// 自定义比较器,按key升序排列
TreeMap<String, Integer> map = new TreeMap<>((a, b) -> a.compareTo(b));

添加/修改

1
2
3
4
// 添加键值对,如果key已存在则更新value
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");

删除

1
2
3
4
5
6
7
8
// 删除指定key的键值对,返回被删除的value
String removed = map.remove(1);
// 删除并返回第一个(最小的)键值对
Map.Entry<Integer, String> first = map.pollFirstEntry();
// 删除并返回最后一个(最大的)键值对
Map.Entry<Integer, String> last = map.pollLastEntry();
// 清空所有键值对
map.clear();

遍历

1
2
3
4
5
6
7
8
9
10
11
12
// 遍历键值对
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
// 遍历key(按升序)
for (Integer key : map.keySet()) {
System.out.println(key);
}
// 遍历value(按对应key的升序)
for (String value : map.values()) {
System.out.println(value);
}

有序操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取第一个(最小的)键值对,不删除
Map.Entry<Integer, String> first = map.firstEntry();
// 获取最后一个(最大的)键值对,不删除
Map.Entry<Integer, String> last = map.lastEntry();
// 获取第一个(最小的)key
Integer firstKey = map.firstKey();
// 获取最后一个(最大的)key
Integer lastKey = map.lastKey();
// 获取小于指定key的最大键值对
Map.Entry<Integer, String> lower = map.lowerEntry(5);
// 获取大于指定key的最小键值对
Map.Entry<Integer, String> higher = map.higherEntry(5);
// 获取小于等于指定key的最大键值对
Map.Entry<Integer, String> floor = map.floorEntry(5);
// 获取大于等于指定key的最小键值对
Map.Entry<Integer, String> ceiling = map.ceilingEntry(5);

其他

1
2
3
4
5
6
// 获取键值对数量
int size = map.size();
// 判断是否为空
boolean isEmpty = map.isEmpty();
// 获取降序排列的视图
NavigableMap<Integer, String> descMap = map.descendingMap();

并发编程

继承Thread类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 1. 自定义一个类,继承 java.lang.Thread 类
class MyThread extends Thread {
private String name;

public MyThread(String name) {
this.name = name;
}

// 2. 重写 run() 方法:此方法是线程的入口点,包含了要并发执行的代码逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " 运行: " + i);
try {
// 睡眠一段时间,模拟任务执行时间,使线程交替效果更明显
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

// 测试
public class ThreadDemo {
public static void main(String[] args) {
// 3. 创建线程对象
MyThread t1 = new MyThread("线程A");
MyThread t2 = new MyThread("线程B");

// 4. 启动线程:调用 start() 方法,注意不是直接调用 run()!
t1.start(); // JVM 会创建一个新线程来执行 run() 方法
t2.start();

// 如果调用 t1.run(),则只是普通方法调用,仍在 main 线程中顺序执行。
}
}

Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 1. 自定义一个类,实现 java.lang.Runnable 接口
class MyRunnable implements Runnable {
private String name;

public MyRunnable(String name) {
this.name = name;
}

// 2. 实现 run() 方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + " 运行: " + i);
try {
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

// 测试
public class RunnableDemo {
public static void main(String[] args) {
// 3. 创建任务对象(线程体)
Runnable task1 = new MyRunnable("线程-1");
Runnable task2 = new MyRunnable("线程-2");

// 4. 创建线程对象,并将任务对象作为参数传入
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);

// 5. 启动线程
t1.start();
t2.start();

// 简洁写法:使用匿名内部类 + Lambda 表达式
new Thread(() -> {
System.out.println("Lambda线程运行中");
}).start();
}
}

上锁

synchronized

同步代码块

1
2
3
4
5
6
7
8
9
// 锁对象可以是任意 Object,但所有竞争线程必须使用同一个锁对象
private final Object lock = new Object();
private int count = 0;

public void add() {
synchronized (lock) { // 获取 lock 对象的锁
count++; // 临界区
} // 释放锁
}

同步实例方法

1
2
3
4
5
6
7
8
9
public synchronized void add() {
count++;
}
// 等价于
public void add() {
synchronized (this) {
count++;
}
}

同步静态方法​​

1
2
3
4
5
6
7
8
9
public static synchronized void staticAdd() {
// ...
}
// 等价于
public static void staticAdd() {
synchronized (MyClass.class) {
// ...
}
}

Lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
private final Lock lock = new ReentrantLock(); // 创建可重入锁
private int count = 0;

public void add() {
lock.lock(); // 获取锁,如果锁被占用,则等待
try {
count++; // 临界区
// 其他可能抛出异常的代码...
} finally {
lock.unlock(); // 必须在 finally 块中释放锁,确保锁一定被释放!
}
}

// 尝试获取锁,避免死等
public void tryAdd() {
if (lock.tryLock()) { // 尝试获取锁,立即返回 true/false
try {
count++;
} finally {
lock.unlock();
}
} else {
// 没拿到锁,做其他事情
System.out.println("获取锁失败,执行备用逻辑");
}
}
}

20251104100612

题目

  1. 两个线程交替输出1A2B3C4D5E
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//synchronized+ wait()/notify()

public class AlternatePrintBasic {
private static final Object lock = new Object();
private static boolean numTurn = true; // 控制该谁执行:true-数字线程,false-字母线程

public static void main(String[] args) {
Thread numThread = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 5; i++) {
// 如果不该数字线程执行,就等待
while (!numTurn) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.print(i); // 输出数字
numTurn = false; // 切换权限给字母线程
lock.notify(); // 唤醒字母线程
}
}
});

Thread letterThread = new Thread(() -> {
synchronized (lock) {
for (char c = 'A'; c <= 'E'; c++) {
// 如果不该字母线程执行,就等待
while (numTurn) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.print(c); // 输出字母
numTurn = true; // 切换权限给数字线程
lock.notify(); // 唤醒数字线程
}
}
});

numThread.start();
letterThread.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class AlternatePrintWithLock {
private static ReentrantLock lock = new ReentrantLock();
private static Condition numCondition = lock.newCondition(); // 数字线程的条件队列
private static Condition letterCondition = lock.newCondition(); // 字母线程的条件队列
private static boolean numTurn = true;

public static void main(String[] args) {
Thread numThread = new Thread(() -> {
lock.lock();
try {
for (int i = 1; i <= 5; i++) {
while (!numTurn) {
numCondition.await(); // 数字线程等待
}

System.out.print(i);
numTurn = false;
letterCondition.signal(); // 精确唤醒字母线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});

Thread letterThread = new Thread(() -> {
lock.lock();
try {
for (char c = 'A'; c <= 'E'; c++) {
while (numTurn) {
letterCondition.await(); // 字母线程等待
}

System.out.print(c);
numTurn = true;
numCondition.signal(); // 精确唤醒数字线程
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});

numThread.start();
letterThread.start();
}
}

线程池

简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* 简单线程池实现
*/
public class SimpleThreadPool {

// 工作线程队列
private final List<WorkerThread> workers;
// 任务队列
private final BlockingQueue<Runnable> taskQueue;
// 线程池状态
private final AtomicBoolean isShutdown;
// 线程池大小
private final int poolSize;

public SimpleThreadPool(int poolSize) {
this.poolSize = poolSize;
this.taskQueue = new LinkedBlockingQueue<>();
this.workers = new ArrayList<>(poolSize);
this.isShutdown = new AtomicBoolean(false);

// 初始化工作线程
initializeWorkers();
}
public boolean isShutdown() {
return isShutdown.get();
}
/**
* 初始化工作线程
*/
private void initializeWorkers() {
for (int i = 0; i < poolSize; i++) {
WorkerThread worker = new WorkerThread("Pool-Thread-" + i);
worker.start();
workers.add(worker);
}
}

/**
* 提交任务到线程池
*/
public void execute(Runnable task) {
if (isShutdown.get()) {
throw new IllegalStateException("线程池已关闭,无法提交新任务");
}
if (task == null) {
throw new NullPointerException("任务不能为null");
}

try {
taskQueue.put(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

/**
* 关闭线程池(等待所有任务完成)
*/
public void shutdown() {
isShutdown.set(true);
System.out.println("线程池开始关闭...");
}

/**
* 立即关闭线程池(不等待任务完成)
*/
public void shutdownNow() {
isShutdown.set(true);
// 中断所有工作线程
for (WorkerThread worker : workers) {
worker.interrupt();
}
// 清空任务队列
taskQueue.clear();
}

/**
* 等待所有任务完成
*/
public void awaitTermination() throws InterruptedException {
for (WorkerThread worker : workers) {
worker.join();
}
}

/**
* 获取等待执行的任务数量
*/
public int getPendingTaskCount() {
return taskQueue.size();
}

/**
* 工作线程内部类
*/
private class WorkerThread extends Thread {
public WorkerThread(String name) {
super(name);
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始工作");

while (!isShutdown.get() || !taskQueue.isEmpty()) {
try {
// 从任务队列获取任务(阻塞式)
Runnable task = taskQueue.take();
System.out.println(Thread.currentThread().getName() + " 执行任务");

// 执行任务
task.run();

} catch (InterruptedException e) {
// 响应中断,退出循环
if (isShutdown.get()) {
System.out.println(Thread.currentThread().getName() + " 被中断,准备退出");
break;
}
} catch (Exception e) {
// 捕获任务执行中的异常,避免工作线程退出
System.err.println("任务执行异常: " + e.getMessage());
e.printStackTrace();
}
}

System.out.println(Thread.currentThread().getName() + " 结束工作");
}
}
}

简单线程池的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 测试手写线程池
*/
public class ThreadPoolTest {

public static void main(String[] args) throws InterruptedException {
// 创建线程池(3个线程)
SimpleThreadPool threadPool = new SimpleThreadPool(3);

System.out.println("=== 提交10个任务到线程池 ===");

// 提交10个任务
for (int i = 1; i <= 10; i++) {
final int taskId = i;
threadPool.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() +
" 正在执行任务 " + taskId);
// 模拟任务执行时间
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() +
" 完成任务 " + taskId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}

// 监控任务队列
new Thread(() -> {
while (true) {
try {
Thread.sleep(500);
int pendingTasks = threadPool.getPendingTaskCount();
System.out.println("当前等待任务数: " + pendingTasks);
if (pendingTasks == 0 && threadPool.isShutdown()) {
break;
}
} catch (InterruptedException e) {
break;
}
}
}).start();

// 等待5秒后关闭线程池
Thread.sleep(5000);
System.out.println("\n=== 准备关闭线程池 ===");

threadPool.shutdown();
threadPool.awaitTermination();

System.out.println("所有任务执行完毕,程序退出");
}
}

带返回值的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
* 增强版线程池(支持Callable和Future)
*/
public class EnhancedThreadPool {
private final SimpleThreadPool threadPool;

public EnhancedThreadPool(int poolSize) {
this.threadPool = new SimpleThreadPool(poolSize);
}

/**
* 提交Callable任务,返回Future
*/
public <T> Future<T> submit(Callable<T> task) {
FutureTask<T> futureTask = new FutureTask<>(task);
threadPool.execute(futureTask);
return futureTask;
}

/**
* 提交Runnable任务,返回Future
*/
public Future<?> submit(Runnable task) {
FutureTask<?> futureTask = new FutureTask<>(task, null);
threadPool.execute(futureTask);
return futureTask;
}

public void execute(Runnable task) {
threadPool.execute(task);
}

public void shutdown() {
threadPool.shutdown();
}

public void shutdownNow() {
threadPool.shutdownNow();
}

public void awaitTermination() throws InterruptedException {
threadPool.awaitTermination();
}
}

/**
* 测试增强版线程池
*/
class EnhancedThreadPoolTest {
public static void main(String[] args) throws Exception {
EnhancedThreadPool pool = new EnhancedThreadPool(2);

// 提交有返回值的任务
Future<String> future1 = pool.submit(() -> {
Thread.sleep(1000);
return "任务1结果";
});

Future<Integer> future2 = pool.submit(() -> {
Thread.sleep(2000);
return 42;
});

// 获取结果
System.out.println("任务1结果: " + future1.get());
System.out.println("任务2结果: " + future2.get());

pool.shutdown();
}
}

模运算恒等式/费马小定理/组合数

灵神的模运算帖子.respect.
https://leetcode.cn/discuss/post/3584387/fen-xiang-gun-mo-yun-suan-de-shi-jie-dan-7xgu/

Java的取模(mod)和取余(% rem),发现我们常用的基本都是正数取余或取模,那带有负数的要怎么计算呢。
当x和y的正负相同,取余和取模结果相同,当x和y正负不同,取余结果的符号和x相同,取模结果的符号和y的符号相同。
假设:被除数 a 除数 b 商c 余数d 公式 a/b=c…d 可以变形为 d=a-b*c
那么关键就在于这个c取什么值。
举个栗子:a=5,b=-2 ,那么 5÷(-2)=-2.5

取模的时候,因为mod 函数采用了 floor 函数,floor函数是向下取整的,所以-2.5向下取整就是-3,那么d=5-(-2)*(-3)=5-6=-1。

取余(%)的时候,因为rem 函数采用 fix 函数,fix函数是向0取整的,所以-2.5向0取整就是-2,那么d=5-(-2)*(-2)=5-4=1。

前言
某些题目,由于要计算的答案非常大(超出 64 位整数的范围),会要求把答案对 10e9+7 取模。如果没有处理得当的话,会 WA(错误)或者 TLE(超时)。
例如计算一堆数字的乘积,如果没有及时取模,乘法会溢出(例如计算结果超出 C++ 中 long long 的最大值),从而得到和预期不符的答案。
对于 Python 来说,虽然没有溢出的问题,但大整数(big integer)之间的运算并不是 O(1) 的,可能会导致 TLE。

如何正确的取模呢?

加法和乘法的取模
如果让你计算 1234×6789 的个位数,你会如何计算?

由于只有个位数会影响到乘积的个位数,那么 4×9=36 的个位数 6 就是答案。

对于 1234+6789 的个位数,同理,4+9=13 的个位数 3 就是答案。

你能把这个结论抽象成数学等式吗?

一般涉及到取模的题目,会用到如下两个恒等式,其中 mod 表示取模运算(modulo),即编程语言中的 %。上面计算的是 m=10 的情况。

20250617103247

20250617104155
根据这两个恒等式,我们可以在计算过程中(例如循环),对加法和乘法的结果取模,而不是在循环结束后再取模。
注:如果涉及到幂运算,指数是不能随意取模的。如果指数在 64 位整数的范围内,可以用快速幂计算,原理见一张图秒懂快速幂;如果指数超出 64 位整数的范围,见欧拉降幂。

如果计算过程中有减法,可能会产生负数,处理不当也会导致 WA。如何正确处理这种情况呢?
同余
20250617105425

同余式的移项
同余式中的加减法可以移项
20250617105921
负数和减法的取模
20250617110526

除法的取模
20250617112637
证明:
20250617112834

20250617113153

20250617113256

求模运算总结

代码实现时,上面的加减乘除通常是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MOD = 1_000_000_007

// 加
(a + b) % MOD

// 减,b 在 [0,MOD-1] 中
(a - b + MOD) % MOD

// 把任意整数 a 取模到 [0,MOD-1] 中,无论 a 是正是负
(a % MOD + MOD) % MOD

// 乘(注意使用 64 位整数)
a * b % MOD

// 多个数相乘,要步步取模,防止溢出
a * b % MOD * c % MOD

// 除(MOD 是质数且 b 不是 MOD 的倍数)
a * qpow(b, MOD - 2, MOD) % MOD

其中 qpow 为快速幂.

总之,如果发现解答错误,可以检查下代码,看看是不是哪里漏掉取模了。
附:组合数计算
20250617140917
模板代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
private static final int MOD = 1_000_000_007;
private static final int MX = 100_001; // 根据题目数据范围修改

private static final long[] F = new long[MX]; // F[i] = i!
private static final long[] INV_F = new long[MX]; // INV_F[i] = i!^-1 = pow(i!, MOD-2)

static {
F[0] = 1;
for (int i = 1; i < MX; i++) {
F[i] = F[i - 1] * i % MOD;
}

INV_F[MX - 1] = pow(F[MX - 1], MOD - 2);
for (int i = MX - 1; i > 0; i--) {
INV_F[i - 1] = INV_F[i] * i % MOD;
}
}

private static long pow(long x, int n) {
long res = 1;
for (; n > 0; n /= 2) {
if (n % 2 > 0) {
res = res * x % MOD;
}
x = x * x % MOD;
}
return res;
}

// 从 n 个数中选 m 个数的方案数
private long comb(int n, int m) {
return m < 0 || m > n ? 0 : F[n] * INV_F[m] % MOD * INV_F[n - m] % MOD;
}

public int solve(int[] nums) {
// 预处理的逻辑写在 static 块中,这样只会初始化一次
}
}

快速幂

一图流(灵神):
20250617141433

代码实现时,注意 n=−2^31的情况,取反后 n=2^31超出 int 最大值。可以转成 64 位 int 解决。
模版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public double myPow(double x, int N) {
double ans = 1;
long n = N;
if (n < 0) { // x^-n = (1/x)^n
n = -n;
x = 1 / x;
}
while (n != 0) { // 从低到高枚举 n 的每个比特位
if ((n & 1) == 1) { // 这个比特位是 1
ans *= x; // 把 x 乘到 ans 中
}
x *= x; // x 自身平方
n >>= 1; // 继续枚举下一个比特位
}
return ans;
}
}

二进制

从集合论到位运算,常见位运算技巧分类总结

20250628202415

20250628202517

1
2
3
      s = 101100
s-1 = 101011 // 最低位的 1 变成 0,同时 1 右边的 0 都取反,变成 1
s&(s-1) = 101000

特别地,如果 s 是 2 的幂,那么 s&(s−1)=0。

此外,编程语言提供了一些和二进制有关的库函数,例如:

  • 计算二进制中的 1 的个数,也就是集合大小;
  • 计算二进制长度,减一后得到集合最大元素;
  • 计算二进制尾零个数,也就是集合最小元素。

调用这些函数的时间复杂度都是 O(1)。
20250628202626

1
2
3
4
     s = 101100
~s = 010011
(~s)+1 = 010100 // 根据补码的定义,这就是 -s => s 的最低 1 左侧取反,右侧不变
s & -s = 000100 // lowbit

遍历集合

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < n; i++) {
if (((s >> i) & 1) == 1) { // i 在 s 中
// 处理 i 的逻辑
}
}
for (int t = s; t > 0; t &= t - 1) {
int i = Integer.numberOfTrailingZeros(t);
// 处理 i 的逻辑
}

20250628202741

四、枚举集合
§4.1 枚举所有集合
设元素范围从 0 到 n−1,从空集 ∅ 枚举到全集 U:

1
2
3
for (int s = 0; s < (1 << n); s++) {
// 处理 s 的逻辑
}

§4.2 枚举非空子集
设集合为 s,从大到小枚举 s 的所有非空子集 sub:

1
2
3
for (int sub = s; sub > 0; sub = (sub - 1) & s) {
// 处理 sub 的逻辑
}

为什么要写成 sub = (sub - 1) & s 呢?

暴力做法是从 s 出发,不断减一,直到 0。但这样做,中途会遇到很多并不是 s 的子集的情况。例如 s=10101 时,减一得到 10100,这是 s 的子集。但再减一就得到 10011 了,这并不是 s 的子集,下一个子集应该是 10001。

把所有的合法子集按顺序列出来,会发现我们做的相当于「压缩版」的二进制减法,例如

10101→10100→10001→10000→00101→⋯
如果忽略掉 10101 中的两个 0,数字的变化和二进制减法是一样的,即

111→110→101→100→011→⋯
如何快速跳到下一个子集呢?比如,怎么从 10100 跳到 10001?

普通的二进制减法,是 10100−1=10011,也就是把最低位的 1 变成 0,同时把最低位的 1 右边的 0 都变成 1。
压缩版的二进制减法也是类似的,对于 10100→10001,也会把最低位的 1 变成 0,对于最低位的 1 右边的 0,并不是都变成 1,只有在 s=10101 中的 1 才会变成 1。怎么做到?减一后 & 10101 就行,也就是 (10100−1) & 10101=10001。
§4.3 枚举子集(包含空集)
如果要从大到小枚举 s 的所有子集 sub(从 s 枚举到空集 ∅),可以这样写:

1
2
3
4
5
int sub = s;
do {
// 处理 sub 的逻辑
sub = (sub - 1) & s;
} while (sub != s);

其中 Java 和 C++ 的原理是,当 sub=0 时(空集),再减一就得到 −1,对应的二进制为 111⋯1,再 &s 就得到了 s。所以当循环到 sub=s 时,说明最后一次循环的 sub=0(空集),s 的所有子集都枚举到了,退出循环。

注:还可以枚举全集 U 的所有大小恰好为 k 的子集,这一技巧叫做 Gosper’s Hack,具体请看 视频讲解。

§4.4 枚举超集
如果 T 是 S 的子集,那么称 S 是 T 的超集(superset)。

枚举超集的原理和上文枚举子集是类似的,这里通过或运算保证枚举的集合 S 一定包含集合 T 中的所有元素。

枚举 S,满足 S 是 T 的超集,也是全集 U={0,1,2,…,n−1} 的子集。

1
2
3
for (int s = t; s < (1 << n); s = (s + 1) | t) {
// 处理 s 的逻辑
}

数组

20250607192928

快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class Solution {
private static final Random rand = new Random();

public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}

// 快速排序子数组 [left, right]
private void quickSort(int[] nums, int left, int right) {
// 优化:如果子数组已是升序,直接返回
boolean ordered = true;
for (int i = left; i < right; i++) {
if (nums[i] > nums[i + 1]) {
ordered = false;
break;
}
}
if (ordered) {
return;
}

int i = partition(nums, left, right); // 划分子数组
quickSort(nums, left, i - 1); // 排序在 pivot 左侧的元素
quickSort(nums, i + 1, right); // 排序在 pivot 右侧的元素
}

// 在子数组 [left, right] 中随机选择一个基准元素 pivot
// 根据 pivot 重新排列子数组 [left, right]
// 重新排列后,<= pivot 的元素都在 pivot 的左侧,>= pivot 的元素都在 pivot 的右侧
// 返回 pivot 在重新排列后的 nums 中的下标
// 特别地,如果子数组的所有元素都等于 pivot,我们会返回子数组的中心下标,避免退化
private int partition(int[] nums, int left, int right) {
// 1. 在子数组 [left, right] 中随机选择一个基准元素 pivot
int i = left + rand.nextInt(right - left + 1);
int pivot = nums[i];
// 把 pivot 与子数组第一个元素交换,避免 pivot 干扰后续划分,从而简化实现逻辑
swap(nums, i, left);

// 2. 相向双指针遍历子数组 [left + 1, right]
// 循环不变量:在循环过程中,子数组的数据分布始终如下图
// [ pivot | <=pivot | 尚未遍历 | >=pivot ]
// ^ ^ ^ ^
// left i j right

i = left + 1;
int j = right;
while (true) {
while (i <= j && nums[i] < pivot) {
i++;
}
// 此时 nums[i] >= pivot

while (i <= j && nums[j] > pivot) {
j--;
}
// 此时 nums[j] <= pivot

if (i >= j) {
break;
}

// 维持循环不变量
swap(nums, i, j);
i++;
j--;
}

// 循环结束后
// [ pivot | <=pivot | >=pivot ]
// ^ ^ ^ ^
// left j i right

// 3. 把 pivot 与 nums[j] 交换,完成划分(partition)
// 为什么与 j 交换?
// 如果与 i 交换,可能会出现 i = right + 1 的情况,已经下标越界了,无法交换
// 另一个原因是如果 nums[i] > pivot,交换会导致一个大于 pivot 的数出现在子数组最左边,不是有效划分
// 与 j 交换,即使 j = left,交换也不会出错
swap(nums, left, j);

// 交换后
// [ <=pivot | pivot | >=pivot ]
// ^
// j

// 返回 pivot 的下标
return j;
}

// 交换 nums[i] 与 nums[j]
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}

二分查找

https://leetcode.cn/problems/binary-search/
通用模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

class Solution {
public int search(int[] nums, int target) {
int i = lowerBound(nums, target); // 选择其中一种写法即可
return i < nums.length && nums[i] == target ? i : -1;
}

// 【下面列了三种写法,选一种自己喜欢的就行】

// lowerBound 返回最小的满足 nums[i] >= target 的 i
// 如果数组为空,或者所有数都 < target,则返回 nums.length
// 要求 nums 是非递减的,即 nums[i] <= nums[i + 1]

// 闭区间写法
private int lowerBound(int[] nums, int target) {
int left = 0, right = nums.length - 1; // 闭区间 [left, right]
while (left <= right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right+1] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1; // 范围缩小到 [mid+1, right]
else
right = mid - 1; // 范围缩小到 [left, mid-1]
}
return left; // 或者 right+1
}

// 左闭右开区间写法
private int lowerBound2(int[] nums, int target) {
int left = 0, right = nums.length; // 左闭右开区间 [left, right)
while (left < right) { // 区间不为空
// 循环不变量:
// nums[left-1] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid + 1; // 范围缩小到 [mid+1, right)
else
right = mid; // 范围缩小到 [left, mid)
}
return left; // 或者 right
}

// 开区间写法
private int lowerBound3(int[] nums, int target) {
int left = -1, right = nums.length; // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
// 循环不变量:
// nums[left] < target
// nums[right] >= target
int mid = left + (right - left) / 2;
if (nums[mid] < target)
left = mid; // 范围缩小到 (mid, right)
else
right = mid; // 范围缩小到 (left, mid)
}
return right; // 或者 left+1
}
}

二分法
数组:每次遇到二分法,都是一看就会,一写就废

  • 暴力解法时间复杂度:O(n)
  • 二分法时间复杂度:O(logn)

双指针

双指针法

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

  • 暴力解法时间复杂度:O(n^2)
  • 双指针时间复杂度:O(n)

滑动窗口

滑动窗口

滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。

恰好包含k个
对于这种恰好包含k个的题目,可以考虑使用滑动窗口来解决.
比如恰好等于k个的,可以通过至少包含k个的,然后减去至少包含k-1个的,就是恰好包含k个的.

模拟行为

在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。

相信大家有遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,拆了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点。

前缀和

快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class Solution {
private static final Random rand = new Random();

public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}

// 快速排序子数组 [left, right]
private void quickSort(int[] nums, int left, int right) {
// 优化:如果子数组已是升序,直接返回
boolean ordered = true;
for (int i = left; i < right; i++) {
if (nums[i] > nums[i + 1]) {
ordered = false;
break;
}
}
if (ordered) {
return;
}

int i = partition(nums, left, right); // 划分子数组
quickSort(nums, left, i - 1); // 排序在 pivot 左侧的元素
quickSort(nums, i + 1, right); // 排序在 pivot 右侧的元素
}

// 在子数组 [left, right] 中随机选择一个基准元素 pivot
// 根据 pivot 重新排列子数组 [left, right]
// 重新排列后,<= pivot 的元素都在 pivot 的左侧,>= pivot 的元素都在 pivot 的右侧
// 返回 pivot 在重新排列后的 nums 中的下标
// 特别地,如果子数组的所有元素都等于 pivot,我们会返回子数组的中心下标,避免退化
private int partition(int[] nums, int left, int right) {
// 1. 在子数组 [left, right] 中随机选择一个基准元素 pivot
int i = left + rand.nextInt(right - left + 1);
int pivot = nums[i];
// 把 pivot 与子数组第一个元素交换,避免 pivot 干扰后续划分,从而简化实现逻辑
swap(nums, i, left);

// 2. 相向双指针遍历子数组 [left + 1, right]
// 循环不变量:在循环过程中,子数组的数据分布始终如下图
// [ pivot | <=pivot | 尚未遍历 | >=pivot ]
// ^ ^ ^ ^
// left i j right

i = left + 1;
int j = right;
while (true) {
while (i <= j && nums[i] < pivot) {
i++;
}
// 此时 nums[i] >= pivot

while (i <= j && nums[j] > pivot) {
j--;
}
// 此时 nums[j] <= pivot

if (i >= j) {
break;
}

// 维持循环不变量
swap(nums, i, j);
i++;
j--;
}

// 循环结束后
// [ pivot | <=pivot | >=pivot ]
// ^ ^ ^ ^
// left j i right

// 3. 把 pivot 与 nums[j] 交换,完成划分(partition)
// 为什么与 j 交换?
// 如果与 i 交换,可能会出现 i = right + 1 的情况,已经下标越界了,无法交换
// 另一个原因是如果 nums[i] > pivot,交换会导致一个大于 pivot 的数出现在子数组最左边,不是有效划分
// 与 j 交换,即使 j = left,交换也不会出错
swap(nums, left, j);

// 交换后
// [ <=pivot | pivot | >=pivot ]
// ^
// j

// 返回 pivot 的下标
return j;
}

// 交换 nums[i] 与 nums[j]
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}

差分数组

二维差分:
20251114101959

链表

20250607193122
JAVA版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}

相交链表

题目链接
20251013203908

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA;
ListNode q = headB;
while (p != q) {
p = p != null ? p.next : headB;
q = q != null ? q.next : headA;
}
return p;
}
}

反转链表

1
2
3
4
5
6
7
8
9
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null)return head;
ListNode last = reverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
}

递归流程图:
20251013210813

移除链表元素
设计链表
反转链表
原地反转,只需要把每个节点间的指向反转就可以
尝试递归调用
两两交换链表中的节点
删除链表的倒数第N个节点
链表相交
环形链表II
环形链表
快慢指针一同走 相遇后差一圈 结论a = c
从起点和相遇点同时走 就是入口点
相似题: https://leetcode.cn/problems/find-the-duplicate-number
这个是数组模拟环形链表,非常有意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int findDuplicate(int[] nums) {
int slow = 0;
int fast = 0;
slow = nums[slow];
fast = nums[nums[fast]];
while(slow != fast){
slow = nums[slow];
fast = nums[nums[fast]];
}
int pre1 = 0;
int pre2 = slow;
while(pre1 != pre2){
pre1 = nums[pre1];
pre2 = nums[pre2];
}
return pre1;
}
}

哈希表

题目

242.有效的字母异位词
力扣题目链接
349.两个数组的交集
力扣题目链接
第202题. 快乐数
力扣题目链接
两数之和
力扣题目链接
第454题.四数相加II
力扣题目链接
赎金信
力扣题目链接
第15题. 三数之和
力扣题目链接
第18题. 四数之和
力扣题目链接

字符串

KMP算法

介绍:
KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」。

上述的朴素解法,不考虑剪枝的话复杂度是 O(m∗n) 的,而 KMP 算法的复杂度为 O(m+n)。

KMP 之所以能够在 O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。
个人理解:
通过构造一个next数组,使的前缀一样的部分能够快速跳转,前缀一样,但是后面的一个不一样,那就可以直接通过next数组跳转到上一个前缀一样的位置的下标,然后继续匹配.
所以这个算法的关键就是构造next数组.
看到的一个比较好的实现方式就是在主串和匹配串前面都加上一个空格,这样就可以保证next数组的下标从1开始,这样就可以避免很多边界问题.

先贴一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Solution {
// KMP 算法
// ss: 原串(string) pp: 匹配串(pattern)
public int strStr(String ss, String pp) {
if (pp.isEmpty()) return 0;

// 分别读取原串和匹配串的长度
int n = ss.length(), m = pp.length();
// 原串和匹配串前面都加空格,使其下标从 1 开始
ss = " " + ss;
pp = " " + pp;

char[] s = ss.toCharArray();
char[] p = pp.toCharArray();

// 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相关的)
int[] next = new int[m + 1];
// 构造过程 i = 2,j = 0 开始,i 小于等于匹配串长度 【构造 i 从 2 开始】
for (int i = 2, j = 0; i <= m; i++) {
// 匹配不成功的话,j = next(j)
while (j > 0 && p[i] != p[j + 1]) j = next[j];
// 匹配成功的话,先让 j++
if (p[i] == p[j + 1]) j++;
// 更新 next[i],结束本次循环,i++
next[i] = j;
}

// 匹配过程,i = 1,j = 0 开始,i 小于等于原串长度 【匹配 i 从 1 开始】
for (int i = 1, j = 0; i <= n; i++) {
// 匹配不成功 j = next(j)
while (j > 0 && s[i] != p[j + 1]) j = next[j];
// 匹配成功的话,先让 j++,结束本次循环后 i++
if (s[i] == p[j + 1]) j++;
// 整一段匹配成功,直接返回下标
if (j == m) return i - m;
}

return -1;
}
}

next数组构造:
20250609185157

匹配过程:
20250609185120

题目

344.反转字符串
力扣题目链接

a=a^b: 先把a和b中,不相同的位保存到a,现在a中置1的位,代表原始的a和b不相同的位,而0,就是a和b相同的位。

b=a^b: 不相同的位是1和原始b异或,就得到原始a的那个位的值;相同的位是0和原始b异或就是原始a或者原始b的值(本来就相同)。现在得到的就是原始a的值,现在存在b中。

a=a^b:和上面相同。

a,b已经交换。

1.反转字符串II
力扣题目链接

二叉树

二叉树基础

  1. 满二叉树:
    如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。

  2. 完全二叉树:
    在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
    也就是只有最后一层右侧不满,前面都是满的.

  3. 二叉搜索树:
    二叉搜索树是一个有序树

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树
  1. 平衡二叉搜索树:
    平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。

二叉树遍历顺序
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

二叉树递归遍历

递归的三要素:
1.确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。

2.确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。

3.确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

递归三部曲

  • 确定递归函数的参数以及返回值
  • 确定终止条件
  • 确定单层递归的逻辑

参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。
再来看返回值,递归函数什么时候需要返回值?
什么时候不需要返回值?这里总结如下三点:

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 中介绍)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

终止条件
终止条件如果是判断叶子节点,递归的过程中就不要让空节点进入递归了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 前序遍历·递归·LC144_二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
preorder(root, result);
return result;
}

public void preorder(TreeNode root, List<Integer> result) {
if (root == null) {
return;
}
result.add(root.val);
preorder(root.left, result);
preorder(root.right, result);
}
}
// 中序遍历·递归·LC94_二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root, res);
return res;
}

void inorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
inorder(root.left, list);
list.add(root.val); // 注意这一句
inorder(root.right, list);
}
}
// 后序遍历·递归·LC145_二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
postorder(root, res);
return res;
}

void postorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
postorder(root.left, list);
postorder(root.right, list);
list.add(root.val); // 注意这一句
}
}

二叉树迭代遍历

144.二叉树的前序遍历
https://leetcode.cn/problems/binary-tree-preorder-traversal/
94.二叉树的中序遍历
https://leetcode.cn/problems/binary-tree-inorder-traversal/
145.二叉树的后序遍历
https://leetcode.cn/problems/binary-tree-postorder-traversal/

统一写法!!!!!!!!
思路:将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
实现方式:

  1. 方法一:就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法可以叫做空指针标记法。
  2. 方法二:加一个 boolean 值跟随每个节点,false (默认值) 表示需要为该节点和它的左右儿子安排在栈中的位次,true 表示该节点的位次之前已经安排过了,可以收割节点了。 这种方法可以叫做boolean 标记法。 这种方法更容易理解,在面试中更容易写出来。
    前序遍历代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> result = new LinkedList<>();
    Deque<TreeNode> st = new LinkedList<>();
    if (root != null) st.push(root);
    while (!st.isEmpty()) {
    TreeNode node = st.peek();
    if (node != null) {
    st.pop(); // 将该节点弹出,避免重复操作,下面再将右左中节点添加到栈中(前序遍历-中左右,入栈顺序右左中)
    if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
    if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
    st.push(node); // 添加中节点
    st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。

    } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
    st.pop(); // 将空节点弹出
    node = st.peek(); // 重新取出栈中元素
    st.pop();
    result.add(node.val); // 加入到结果集
    }
    }
    return result;
    }
    中序遍历代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new LinkedList<>();
    Deque<TreeNode> st = new LinkedList<>();
    if (root != null) st.push(root);
    while (!st.isEmpty()) {
    TreeNode node = st.peek();
    if (node != null) {
    st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中(中序遍历-左中右,入栈顺序右中左)
    if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
    st.push(node); // 添加中节点
    st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
    if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)
    } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
    st.pop(); // 将空节点弹出
    node = st.peek(); // 重新取出栈中元素
    st.pop();
    result.add(node.val); // 加入到结果集
    }
    }
    return result;
    }
    后序遍历代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> result = new LinkedList<>();
    Deque<TreeNode> st = new LinkedList<>();
    if (root != null) st.push(root);
    while (!st.isEmpty()) {
    TreeNode node = st.peek();
    if (node != null) {
    st.pop(); // 将该节点弹出,避免重复操作,下面再将中右左节点添加到栈中(后序遍历-左右中,入栈顺序中右左)
    st.push(node); // 添加中节点
    st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。
    if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈)
    if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈)

    } else { // 只有遇到空节点的时候,才将下一个节点放进结果集
    st.pop(); // 将空节点弹出
    node = st.peek(); // 重新取出栈中元素
    st.pop();
    result.add(node.val); // 加入到结果集
    }
    }
    return result;
    }

直接用栈模拟的写法:
前序 中序 后序的写法不一样.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return result;
}
}

// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
if (cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
}

// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
Collections.reverse(result);
return result;
}
}

二叉树的层序遍历(BFS)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 102.二叉树的层序遍历
class Solution {
public List<List<Integer>> resList = new ArrayList<List<Integer>>();

public List<List<Integer>> levelOrder(TreeNode root) {
//checkFun01(root,0);
checkFun02(root);

return resList;
}

//BFS--递归方式
public void checkFun01(TreeNode node, Integer deep) {
if (node == null) return;
deep++;

if (resList.size() < deep) {
//当层级增加时,list的Item也增加,利用list的索引值进行层级界定
List<Integer> item = new ArrayList<Integer>();
resList.add(item);
}
resList.get(deep - 1).add(node.val);

checkFun01(node.left, deep);
checkFun01(node.right, deep);
}

//BFS--迭代方式--借助队列
public void checkFun02(TreeNode node) {
if (node == null) return;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(node);

while (!que.isEmpty()) {
List<Integer> itemList = new ArrayList<Integer>();
int len = que.size();

while (len > 0) {
TreeNode tmpNode = que.poll();
itemList.add(tmpNode.val);

if (tmpNode.left != null) que.offer(tmpNode.left);
if (tmpNode.right != null) que.offer(tmpNode.right);
len--;
}

resList.add(itemList);
}

}
}

Morris遍历

学习了一个Morris遍历方式,这种方式可以在O(1)的空间复杂度下完成二叉树的遍历,并且不需要使用栈来存储节点.
贴一个自己画的伪代码:
Morris中序遍历伪代码
前序和中序可以通过调整代码实现,后序可以通过向右的前序然后结果翻转来实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//前序
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
TreeNode cur = root;
while(cur != null){
if(cur.left == null){
res.add(cur.val);
cur =cur.right; //向右走
}else{
TreeNode pre = cur.left;
while(pre.right != null && pre.right != cur){
pre = pre.right; //开始建立索引
}
if(pre.right == null){
//是一个第一次来的节点
pre.right = cur;
res.add(cur.val);
cur = cur.left;
}else{
//并不是第一次来的节点
pre.right = null;
cur = cur.right;
}
}
}
return res;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//中序
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res =new LinkedList<>();
TreeNode cur = root;
while(cur != null){
if(cur.left == null){
res.add(cur.val);
cur = cur.right;
}else{
TreeNode pre = cur.left;
while(pre.right != null && pre.right != cur){
pre = pre.right;
}
if(pre.right == null){
pre.right = cur;
cur = cur.left;
}else{
pre.right = null;
res.add(cur.val);
cur = cur.right;
}
}
}
return res;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//后序
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}

public class MorrisPostorderTraversal {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
TreeNode curr = root;
TreeNode prev = null;

while (curr != null) {
if (curr.right == null) {
res.add(curr.val); // 右子树为空,直接访问
curr = curr.left; // 转向左子树
} else {
// 找到右子树的最左节点(即后继节点)
prev = curr.right;
while (prev.left != null && prev.left != curr) {
prev = prev.left;
}

if (prev.left == null) {
prev.left = curr; // 建立线索
res.add(curr.val); // 访问当前节点(前序遍历位置)
curr = curr.right; // 转向右子树
} else {
prev.left = null; // 恢复树结构
curr = curr.left; // 转向左子树
}
}
}
Collections.reverse(res); // 反转结果,得到后序遍历
return res;
}
}

二叉树题目

翻转二叉树
dfs或者bfs都可做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//DFS递归
class Solution {
/**
* 前后序遍历都可以
* 中序不行,因为先左孩子交换孩子,再根交换孩子(做完后,右孩子已经变成了原来的左孩子),再右孩子交换孩子(此时其实是对原来的左孩子做交换)
*/
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
invertTree(root.left);
invertTree(root.right);
swapChildren(root);
return root;
}

private void swapChildren(TreeNode root) {
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
}
}
//BFS
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {return null;}
ArrayDeque<TreeNode> deque = new ArrayDeque<>();
deque.offer(root);
while (!deque.isEmpty()) {
int size = deque.size();
while (size-- > 0) {
TreeNode node = deque.poll();
swap(node);
if (node.left != null) deque.offer(node.left);
if (node.right != null) deque.offer(node.right);
}
}
return root;
}

public void swap(TreeNode root) {
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}

对称二叉树
这道题递归调用很简单,迭代调用用一个队列就可以,退出条件可以注意一下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 递归法
*/
public boolean isSymmetric1(TreeNode root) {
return compare(root.left, root.right);
}

private boolean compare(TreeNode left, TreeNode right) {

if (left == null && right != null) {
return false;
}
if (left != null && right == null) {
return false;
}

if (left == null && right == null) {
return true;
}
if (left.val != right.val) {
return false;
}
// 比较外侧
boolean compareOutside = compare(left.left, right.right);
// 比较内侧
boolean compareInside = compare(left.right, right.left);
return compareOutside && compareInside;
}

/**
* 迭代法
* 使用双端队列,相当于两个栈
*/
public boolean isSymmetric2(TreeNode root) {
Deque<TreeNode> deque = new LinkedList<>();
deque.offerFirst(root.left);
deque.offerLast(root.right);
while (!deque.isEmpty()) {
TreeNode leftNode = deque.pollFirst();
TreeNode rightNode = deque.pollLast();
if (leftNode == null && rightNode == null) {
continue;
}
// if (leftNode == null && rightNode != null) {
// return false;
// }
// if (leftNode != null && rightNode == null) {
// return false;
// }
// if (leftNode.val != rightNode.val) {
// return false;
// }
// 以上三个判断条件合并
if (leftNode == null || rightNode == null || leftNode.val != rightNode.val) {
return false;
}
deque.offerFirst(leftNode.left);
deque.offerFirst(leftNode.right);
deque.offerLast(rightNode.right);
deque.offerLast(rightNode.left);
}
return true;
}

/**
* 迭代法
* 使用普通队列
*/
//
public boolean isSymmetric3(TreeNode root) {
Queue<TreeNode> deque = new LinkedList<>();
deque.offer(root.left);
deque.offer(root.right);
while (!deque.isEmpty()) {
TreeNode leftNode = deque.poll();
TreeNode rightNode = deque.poll();
if (leftNode == null && rightNode == null) {
continue;
}
// if (leftNode == null && rightNode != null) {
// return false;
// }
// if (leftNode != null && rightNode == null) {
// return false;
// }
// if (leftNode.val != rightNode.val) {
// return false;
// }
// 以上三个判断条件合并
if (leftNode == null || rightNode == null || leftNode.val != rightNode.val) {
return false;
}
// 这里顺序与使用Deque不同
deque.offer(leftNode.left);
deque.offer(rightNode.right);
deque.offer(leftNode.right);
deque.offer(rightNode.left);
}
return true;
}

二叉树最大深度
104.二叉树的最大深度

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
/**
* 递归法
*/
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
/**
* 递归法(求深度法)
*/
//定义最大深度
int maxnum = 0;

public int maxDepth(TreeNode root) {
ans(root,0);
return maxnum;
}

//递归求解最大深度
void ans(TreeNode tr,int tmp){
if(tr==null) return;
tmp++;
maxnum = maxnum<tmp?tmp:maxnum;
ans(tr.left,tmp);
ans(tr.right,tmp);
tmp--;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
/**
* 迭代法,使用层序遍历
*/
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
int depth = 0;
while (!deque.isEmpty()) {
int size = deque.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode node = deque.poll();
if (node.left != null) {
deque.offer(node.left);
}
if (node.right != null) {
deque.offer(node.right);
}
}
}
return depth;
}
}

二叉树的所有路径

https://leetcode.cn/problems/binary-tree-paths/
递归和回溯.
PS:递归和回溯永远要放在一起!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//解法一

//方式一
class Solution {
/**
* 递归法
*/
public List<String> binaryTreePaths(TreeNode root) {
List<String> res = new ArrayList<>();// 存最终的结果
if (root == null) {
return res;
}
List<Integer> paths = new ArrayList<>();// 作为结果中的路径
traversal(root, paths, res);
return res;
}

private void traversal(TreeNode root, List<Integer> paths, List<String> res) {
paths.add(root.val);// 前序遍历,中
// 遇到叶子结点
if (root.left == null && root.right == null) {
// 输出
StringBuilder sb = new StringBuilder();// StringBuilder用来拼接字符串,速度更快
for (int i = 0; i < paths.size() - 1; i++) {
sb.append(paths.get(i)).append("->");
}
sb.append(paths.get(paths.size() - 1));// 记录最后一个节点
res.add(sb.toString());// 收集一个路径
return;
}
// 递归和回溯是同时进行,所以要放在同一个花括号里
if (root.left != null) { // 左
traversal(root.left, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
if (root.right != null) { // 右
traversal(root.right, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
}
}

//方式二
class Solution {

List<String> result = new ArrayList<>();

public List<String> binaryTreePaths(TreeNode root) {
deal(root, "");
return result;
}

public void deal(TreeNode node, String s) {
if (node == null)
return;
if (node.left == null && node.right == null) {
result.add(new StringBuilder(s).append(node.val).toString());
return;
}
String tmp = new StringBuilder(s).append(node.val).append("->").toString();
deal(node.left, tmp);
deal(node.right, tmp);
}
}

迭代法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 解法二
class Solution {
/**
* 迭代法
*/
public List<String> binaryTreePaths(TreeNode root) {
List<String> result = new ArrayList<>();
if (root == null)
return result;
Stack<Object> stack = new Stack<>();
// 节点和路径同时入栈
stack.push(root);
stack.push(root.val + "");
while (!stack.isEmpty()) {
// 节点和路径同时出栈
String path = (String) stack.pop();
TreeNode node = (TreeNode) stack.pop();
// 若找到叶子节点
if (node.left == null && node.right == null) {
result.add(path);
}
//右子节点不为空
if (node.right != null) {
stack.push(node.right);
stack.push(path + "->" + node.right.val);
}
//左子节点不为空
if (node.left != null) {
stack.push(node.left);
stack.push(path + "->" + node.left.val);
}
}
return result;
}
}

404.左叶子之和
https://leetcode.cn/problems/sum-of-left-leaves/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if (root == null) return 0;
int leftValue = sumOfLeftLeaves(root.left); // 左
int rightValue = sumOfLeftLeaves(root.right); // 右

int midValue = 0;
if (root.left != null && root.left.left == null && root.left.right == null) {
midValue = root.left.val;
}
int sum = midValue + leftValue + rightValue; // 中
return sum;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if (root == null) return 0;
Stack<TreeNode> stack = new Stack<> ();
stack.add(root);
int result = 0;
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node.left != null && node.left.left == null && node.left.right == null) {
result += node.left.val;
}
if (node.right != null) stack.add(node.right);
if (node.left != null) stack.add(node.left);
}
return result;
}
}

513.找树左下角的值
https://leetcode.cn/problems/find-bottom-left-tree-value/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 递归法
class Solution {
private int Deep = -1;
private int value = 0;
public int findBottomLeftValue(TreeNode root) {
value = root.val;
findLeftValue(root,0);
return value;
}

private void findLeftValue (TreeNode root,int deep) {
if (root == null) return;
if (root.left == null && root.right == null) {
if (deep > Deep) {
value = root.val;
Deep = deep;
}
}
if (root.left != null) findLeftValue(root.left,deep + 1);
if (root.right != null) findLeftValue(root.right,deep + 1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//迭代法
class Solution {

public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int res = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode poll = queue.poll();
if (i == 0) {
res = poll.val;
}
if (poll.left != null) {
queue.offer(poll.left);
}
if (poll.right != null) {
queue.offer(poll.right);
}
}
}
return res;
}
}
  1. 路径总和
    https://leetcode.cn/problems/path-sum/

106.从中序与后序遍历序列构造二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
Map<Integer, Integer> map; // 方便根据数值查找位置
public TreeNode buildTree(int[] inorder, int[] postorder) {
map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) { // 用map保存中序序列的数值对应位置
map.put(inorder[i], i);
}

return findNode(inorder, 0, inorder.length, postorder,0, postorder.length); // 前闭后开
}

public TreeNode findNode(int[] inorder, int inBegin, int inEnd, int[] postorder, int postBegin, int postEnd) {
// 参数里的范围都是前闭后开
if (inBegin >= inEnd || postBegin >= postEnd) { // 不满足左闭右开,说明没有元素,返回空树
return null;
}
int rootIndex = map.get(postorder[postEnd - 1]); // 找到后序遍历的最后一个元素在中序遍历中的位置
TreeNode root = new TreeNode(inorder[rootIndex]); // 构造结点
int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定后序数列的个数
root.left = findNode(inorder, inBegin, rootIndex,
postorder, postBegin, postBegin + lenOfLeft);
root.right = findNode(inorder, rootIndex + 1, inEnd,
postorder, postBegin + lenOfLeft, postEnd - 1);

return root;
}
}

106.从前序与后序遍历序列构造二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
Map<Integer,Integer> mp = new HashMap<>();
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
int n = preorder.length;
for(int i=0 ; i<n ; ++i){
mp.put(postorder[i],i);
}
return dfs(preorder,0,n-1,postorder,0,n-1);
}
private TreeNode dfs(int[] preorder , int pre_beg,int pre_end ,int[] postorder, int pos_beg , int pos_end){
//闭区间 先判断是否终止
if(pre_beg>pre_end || pos_beg>pos_end){
return null;
}
int mid = preorder[pre_beg];
if(pre_beg == pre_end || pos_beg == pos_end){
return new TreeNode(mid);
}
int nx_l = preorder[pre_beg+1];
int ind_nxl = mp.get(nx_l);
int lenOfLeft = ind_nxl - pos_beg + 1;
TreeNode root = new TreeNode(mid);
root.left = dfs(preorder,pre_beg+1,pre_beg+lenOfLeft,postorder,pos_beg,pos_beg+lenOfLeft-1);
root.right = dfs(preorder,pre_beg+lenOfLeft+1,pre_end,postorder,ind_nxl+1,pos_end-1);
return root;
}
}

617.合并二叉树
https://leetcode.cn/problems/merge-two-binary-trees/

700.二叉搜索树中的搜索
https://leetcode.cn/problems/search-in-a-binary-search-tree/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Solution {
// 递归,普通二叉树
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
TreeNode left = searchBST(root.left, val);
if (left != null) {
return left;
}
return searchBST(root.right, val);
}
}

class Solution {
// 递归,利用二叉搜索树特点,优化
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
if (val < root.val) {
return searchBST(root.left, val);
} else {
return searchBST(root.right, val);
}
}
}
class Solution {
// 迭代,普通二叉树
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || root.val == val) {
return root;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode pop = stack.pop();
if (pop.val == val) {
return pop;
}
if (pop.right != null) {
stack.push(pop.right);
}
if (pop.left != null) {
stack.push(pop.left);
}
}
return null;
}
}
class Solution {
// 迭代,利用二叉搜索树特点,优化,可以不需要栈
public TreeNode searchBST(TreeNode root, int val) {
while (root != null)
if (val < root.val) root = root.left;
else if (val > root.val) root = root.right;
else return root;
return null;
}
}

98.验证二叉搜索树
记住最重要的就是:二叉搜索树的中序遍历是有序的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//使用統一迭代法
class Solution {
public boolean isValidBST(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode pre = null;
if(root != null)
stack.add(root);
while(!stack.isEmpty()){
TreeNode curr = stack.peek();
if(curr != null){
stack.pop();
if(curr.right != null)
stack.add(curr.right);
stack.add(curr);
stack.add(null);
if(curr.left != null)
stack.add(curr.left);
}else{
stack.pop();
TreeNode temp = stack.pop();
if(pre != null && pre.val >= temp.val)
return false;
pre = temp;
}
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class Solution {
// 递归
TreeNode max;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
// 左
boolean left = isValidBST(root.left);
if (!left) {
return false;
}
// 中
if (max != null && root.val <= max.val) {
return false;
}
max = root;
// 右
boolean right = isValidBST(root.right);
return right;
}
}

class Solution {
// 迭代
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode pre = null;
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;// 左
}
// 中,处理
TreeNode pop = stack.pop();
if (pre != null && pop.val <= pre.val) {
return false;
}
pre = pop;

root = pop.right;// 右
}
return true;
}
}

// 简洁实现·递归解法
class Solution {
public boolean isValidBST(TreeNode root) {
return validBST(Long.MIN_VALUE, Long.MAX_VALUE, root);
}
boolean validBST(long lower, long upper, TreeNode root) {
if (root == null) return true;
if (root.val <= lower || root.val >= upper) return false;
return validBST(lower, root.val, root.left) && validBST(root.val, upper, root.right);
}
}
// 简洁实现·中序遍历
class Solution {
private long prev = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if (root == null) {
return true;
}
if (!isValidBST(root.left)) {
return false;
}
if (root.val <= prev) { // 不满足二叉搜索树条件
return false;
}
prev = root.val;
return isValidBST(root.right);
}
}
  1. 二叉树的最近公共祖先
    https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) { // 递归结束条件
return root;
}

// 后序遍历
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);

if(left == null && right == null) { // 若未找到节点 p 或 q
return null;
}else if(left == null && right != null) { // 若找到一个节点
return right;
}else if(left != null && right == null) { // 若找到一个节点
return left;
}else { // 若找到两个节点
return root;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

//迭代

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
int max = Integer.MAX_VALUE;
Stack<TreeNode> st = new Stack<>();
TreeNode cur = root, pre = null;
while (cur != null || !st.isEmpty()) {
while (cur != null) {
st.push(cur);
cur = cur.left;
}
cur = st.pop();
if (cur.right == null || cur.right == pre) {
// p/q是 中/左 或者 中/右 , 返回中
if (cur == p || cur == q) {
if ((cur.left != null && cur.left.val == max) || (cur.right != null && cur.right.val == max)) {
return cur;
}
cur.val = max;
}
// p/q是 左/右 , 返回中
if (cur.left != null && cur.left.val == max && cur.right != null && cur.right.val == max) {
return cur;
}
// MAX_VALUE 往上传递
if ((cur.left != null && cur.left.val == max) || (cur.right != null && cur.right.val == max)) {
cur.val = max;
}
pre = cur;
cur = null;
} else {
st.push(cur);
cur = cur.right;
}
}
return null;
}

  1. 二叉搜索树的最近公共祖先
    https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/

450.删除二叉搜索树中的节点
https://leetcode.cn/problems/delete-node-in-a-bst/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 解法1(最好理解的版本)
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return root;
if (root.val == key) {
if (root.left == null) {
return root.right;
} else if (root.right == null) {
return root.left;
} else {
TreeNode cur = root.right;
while (cur.left != null) {
cur = cur.left;
}
cur.left = root.left;
root = root.right;
return root;
}
}
if (root.val > key) root.left = deleteNode(root.left, key);
if (root.val < key) root.right = deleteNode(root.right, key);
return root;
}
}

修剪二叉搜索树
https://leetcode.cn/problems/trim-a-binary-search-tree/

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (root == nullptr) return nullptr;
if (root->val < low) return trimBST(root->right, low, high);
if (root->val > high) return trimBST(root->left, low, high);
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};

回溯算法

20250624192141

回溯的理论基础

什么是回溯法
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
回溯是递归的副产品,只要有递归就会有回溯

因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

组合是不强调元素顺序的,排列是强调元素顺序
记住组合无序,排列有序,就可以了。

回溯法模板

  • 回溯函数模板返回值以及参数

在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。

回溯算法中函数返回值一般为void。
再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数
回溯函数伪代码如下:
void backtracking(参数)

  • 回溯函数终止条件

既然是树形结构,那么我们在讲解二叉树的递归 (opens new window)的时候,就知道遍历树形结构一定要有终止条件。
所以回溯也有要终止条件。
什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
所以回溯函数终止条件伪代码如下:

1
2
3
4
if (终止条件) {
存放结果;
return;
}
  • 回溯搜索的遍历过程

在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。

1
2
3
4
5
6
7
8
9
10
11
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}

回溯题目

第77题. 组合

贪心算法

20250709191956

选取每阶段的局部最优,最终达到全局最优.
怎么判断成不成立呢?最好的办法是举反例,如果举不出来,那就可以尝试一下贪心算法.

解题步骤:
贪心算法一般分为如下四步:

  1. 将问题分解为若干个子问题
  2. 找出适合的贪心策略
  3. 求解每一个子问题的最优解
  4. 将局部最优解堆叠成全局最优解

动态规划

20250709191701
如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的.

解题步骤:
对于动态规划问题,拆解为如下五步曲:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

背包问题

20250710201132
20250723200543

0-1 背包

由于每个物品只能选一次,所以遍历背包容量时防止重复选择,只能倒序背包容量进行选择.

完全背包

由于每个物品没有数量限制,所以可以正序遍历背包容量.

动态规划中排列和组合的区别:

  • 首先从方法论的角度理解:

​排列问题(背包在外层循环)

当外层循环是背包容量,内层循环是物品时,相当于对于每个容量,我们尝试所有可能的物品。
这意味着同一个物品可以在不同的位置被多次使用,顺序不同被视为不同的排列。
例如:nums = [1,2], target = 3
排列有:(1,1,1), (1,2), (2,1)
顺序不同被视为不同解.

​​组合问题(物品在外层循环)​​:
当外层循环是物品,内层循环是背包容量时,我们固定了物品的选择顺序。
这意味着我们只考虑物品的某种特定顺序的组合,不考虑顺序变化。
例如:nums = [1,2], target = 3
组合有:(1,1,1), (1,2)
(2,1)不会被计入,因为2已经在1之后考虑了

  • 然后从代码来理解一下:
    排列问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target + 1];
    dp[0] = 1;
    for (int i = 0; i <= target; i++) { // 外层循环是背包容量
    for (int j = 0; j < nums.length; j++) { // 内层循环是物品
    if (i >= nums[j]) {
    dp[i] += dp[i - nums[j]];
    }
    }
    }
    return dp[target];
    }

组合问题

1
2
3
4
5
6
7
8
9
10
public int combinationSum(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int j = 0; j < nums.length; j++) { // 外层循环是物品
for (int i = nums[j]; i <= target; i++) { // 内层循环是背包容量
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}

多重背包

N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
直接上图:
20250723192116
把上面的数量直接展开成一个0-1背包就可以了其实.
20250723192128

数位DP

单调栈

图论

图论基础

图的种类
整体上一般分为 有向图 和 无向图。加权有向图,就是图中边是有权值的


无向图中有几条边连接该节点,该节点就有几度。

在有向图中,每个节点有出度和入度。

  • 出度:从该节点出发的边的个数。
  • 入度:指向该节点边的个数。

连通图
在无向图中,任何两个节点都是可以到达的,我们称之为连通图

强连通图
在有向图中,任何两个节点是可以相互到达的

连通分量
在无向图中的极大连通子图称之为该图的一个连通分量。

强连通分量
在有向图中极大强连通子图称之为该图的强连通分量。

图的构造
一般使用邻接表邻接矩阵 或者用来表示。

DFS

三色标记法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<Integer>[] g = new ArrayList[numCourses];
//三色标记法 0 没学过 1 正在学 2 已学过
int[] learned = new int[numCourses];
Arrays.setAll(g , i -> new ArrayList<>());
for(int[] p : prerequisites){
int stu = p[0];
int dep = p[1];
g[stu].add(dep);
}
for(int i = 0 ; i < numCourses ; ++i){
if(learned[i] == 0 && dfs(g,learned,i)){
return false;
}
}
return true;

}
//判断是否有环 有的话直接返回true
private boolean dfs(List<Integer>[] g , int[] learned , int i ){
if(learned[i] == 1){
return true;
}
learned[i] = 1;
boolean flag = false;
for(Integer num : g[i]){
if(learned[num] == 1 || learned[num] == 0 && dfs(g,learned,num)){
return true;
}
}
learned[i] = 2;
return false;
}
}

字典树 前缀树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Trie {
//节点类
private static class Node{
Node[] son = new Node[26];
boolean isEnd;
}
private Node root;
public Trie() {
root = new Node();
}

public void insert(String word) {
Node temp = root;
for(char c : word.toCharArray()){
int pos = c - 'a';
if(temp.son[pos] == null){
temp.son[pos] = new Node();
}
temp = temp.son[pos];
}
temp.isEnd = true;//代表确实有一个词在这里
}

public boolean search(String word) {
Node temp = root;
for(char c : word.toCharArray()){
int pos = c-'a';
if(temp.son[pos] == null){
return false;
}
temp = temp.son[pos];
}
return temp.isEnd;
}

public boolean startsWith(String prefix) {
Node temp = root;
for(char c : prefix.toCharArray()){
int pos = c-'a';
if(temp.son[pos] == null){
return false;
}
temp = temp.son[pos];
}
return true;
}
}

/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/

字典序

字典序

字典树:
我觉得比较关键的点是:

  1. 字典树左子树字典序一定比右子树小.
  2. 字典树想要到右侧兄弟节点,直接num++就可以.
  3. 关键就是要找到以当前数字为根的十叉树的元素总个数.
    看着这个图就比较好理解了:
    20250609104923

这张图也很好:
20250609112906

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution {

/**
* 以当前数字为根的十叉树的元素总个数 (包括当前数字)
*
* @param num 当前数字 (需要先 cast 成 long, 因为 num*10 可能导致 int 溢出)
* @param n 数字的最大值
* @return
*/
private int count(long num, int n) {
int cnt = 0; // 元素总个数
int width = 1; // 当前层数的宽度, 第一层只有 num 一个元素, 所以第一层宽度为 1
while (true) {
if (num + width - 1 <= n) { // n 的值大于等于当前层的最大值, 说明当前层数的个数可以全部添加
cnt += width;
num *= 10;
width *= 10;
} else { // n 的值小于当前层的最大值则只能添加部分个数或者不添加, 并跳出循环
if (n - num >= 0) {
cnt += n - num + 1;
}
break;
}
}
return cnt;
}

public int findKthNumber(int n, int k) {
int cnt = 0; // 已经经过的元素个数, 开始一个元素都没有经过, 所以个数为 0
int num = 1; // 第一个元素 (经过 i 个元素, 当前 num 是第 i + 1 元素)

// 要找到第 k 个元素, 需要经过 k - 1 个元素
while (true) {
if (cnt == k - 1) { // 经过了 k - 1 个元素找到了第 k 个元素
break;
}
int temp = count((long) num, n); // 以 num 为根, 以 n 为最大值的十叉树的元素总个数
if (cnt + temp >= k) { // 以 num 为根的十叉树内有第 k 个元素
num *= 10;
cnt++;
} else if (cnt + temp < k) { // 以 num 为根的十叉树内没有第 k 个元素
num++;
cnt += temp;
}
}
return num;
}
}

LRU缓存

20251013194516
贴一个实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class LRUCache {
private static class Node {
int key, value;
Node prev, next;

Node(int k, int v) {
key = k;
value = v;
}
}

private final int capacity;
private final Node dummy = new Node(0, 0); // 哨兵节点
private final Map<Integer, Node> keyToNode = new HashMap<>();

public LRUCache(int capacity) {
this.capacity = capacity;
dummy.prev = dummy;
dummy.next = dummy;
}

public int get(int key) {
Node node = getNode(key);
return node != null ? node.value : -1;
}

public void put(int key, int value) {
Node node = getNode(key);
if (node != null) { // 有这本书
node.value = value; // 更新 value
return;
}
node = new Node(key, value); // 新书
keyToNode.put(key, node);
pushFront(node); // 放在最上面
if (keyToNode.size() > capacity) { // 书太多了
Node backNode = dummy.prev;
keyToNode.remove(backNode.key);
remove(backNode); // 去掉最后一本书
}
}

private Node getNode(int key) {
if (!keyToNode.containsKey(key)) { // 没有这本书
return null;
}
Node node = keyToNode.get(key); // 有这本书
remove(node); // 把这本书抽出来
pushFront(node); // 放在最上面
return node;
}

// 删除一个节点(抽出一本书)
private void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
}

// 在链表头添加一个节点(把一本书放在最上面)
private void pushFront(Node x) {
x.prev = dummy;
x.next = dummy.next;
x.prev.next = x;
x.next.prev = x;
}
}